commit
120b5bf83c
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -32,6 +32,7 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
||||||
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
||||||
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
||||||
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`.
|
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`.
|
||||||
|
- **apps/flutter/** — Flutter GUI. Dart frontend in `lib/src/`, Rust backend in `rust/` via flutter_rust_bridge FFI into `onyx-core`.
|
||||||
|
|
||||||
### Key patterns
|
### Key patterns
|
||||||
|
|
||||||
|
|
@ -55,11 +56,11 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- **Kebab menus**: Tasks, lists, and workspaces all use kebab → submenu pattern for delete.
|
- **Kebab menus**: Tasks, lists, and workspaces all use kebab → submenu pattern for delete.
|
||||||
- **New task**: FAB button opens bottom toast sheet (outside sliding container for fixed positioning).
|
- **New task**: FAB button opens bottom toast sheet (outside sliding container for fixed positioning).
|
||||||
|
|
||||||
### Current state (2026-03-30)
|
### Current state (2026-03-31)
|
||||||
|
|
||||||
- **Phase 1** (Core + CLI): Complete
|
- **Phase 1** (Core + CLI): Complete
|
||||||
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI partially wired (empty credentials issue)
|
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
|
||||||
- **Phase 3** (GUI MVP): In progress — core task CRUD working, UI polished with animations
|
- **Phase 3** (GUI MVP): Near complete — core features working, both Tauri and Flutter GUIs maintained
|
||||||
|
|
||||||
### GUI features done
|
### GUI features done
|
||||||
|
|
||||||
|
|
@ -72,17 +73,23 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- Workspace switcher drop-up with add/remove
|
- Workspace switcher drop-up with add/remove
|
||||||
- Dark mode (GNOME-style neutral grays, cyan-blue accent)
|
- Dark mode (GNOME-style neutral grays, cyan-blue accent)
|
||||||
- Completed tasks section with animated show/hide
|
- Completed tasks section with animated show/hide
|
||||||
|
- Due date picker/editor (DateTimePicker in new task + task detail)
|
||||||
|
- Move task between lists (kebab menu → "Move to..." submenu)
|
||||||
|
- List rename (inline input via list kebab menu)
|
||||||
|
- Group-by-due-date toggle per list (list kebab menu)
|
||||||
|
- Keyboard shortcuts (Escape priority chain: settings → detail → drawer → menus)
|
||||||
|
- WebDAV setup flow (settings auto-populates URL/credentials from config + keychain)
|
||||||
|
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
|
||||||
|
- Setup screen with window dragging + "Open Existing Folder" option
|
||||||
|
|
||||||
### GUI features NOT yet done (CLI has these)
|
### GUI features NOT yet done
|
||||||
|
|
||||||
- Due date editing (model supports it, not exposed in UI)
|
|
||||||
- WebDAV setup flow (GUI passes empty credentials)
|
|
||||||
- Push-only / pull-only sync modes
|
- Push-only / pull-only sync modes
|
||||||
- Sync status view
|
- Sync status view/indicators
|
||||||
- Workspace retarget/migrate
|
- Workspace retarget/migrate
|
||||||
- Group-by-due-date toggle
|
|
||||||
- Subtask hierarchy (data model exists, not used anywhere)
|
- Subtask hierarchy (data model exists, not used anywhere)
|
||||||
- List/workspace rename
|
- Search/filter tasks
|
||||||
|
- Desktop packaging (Windows, Linux, macOS)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|
|
||||||
68
Cargo.lock
generated
68
Cargo.lock
generated
|
|
@ -104,40 +104,6 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "onyx-cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"onyx-core",
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"colored",
|
|
||||||
"fs_extra",
|
|
||||||
"rpassword",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "onyx-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"directories",
|
|
||||||
"keyring",
|
|
||||||
"quick-xml",
|
|
||||||
"reqwest",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml",
|
|
||||||
"sha2",
|
|
||||||
"tempfile",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
"wiremock",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
|
@ -1002,6 +968,40 @@ version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"colored",
|
||||||
|
"fs_extra",
|
||||||
|
"onyx-core",
|
||||||
|
"rpassword",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"directories",
|
||||||
|
"keyring",
|
||||||
|
"quick-xml",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
"wiremock",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
||||||
13
PLAN.md
13
PLAN.md
|
|
@ -715,18 +715,19 @@ WorkspaceConfig {
|
||||||
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
||||||
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
||||||
- [x] Animated completed section show/hide
|
- [x] Animated completed section show/hide
|
||||||
- [ ] Move task between lists (needs `move_task(from_list, to_list, task_id)` added to onyx-core + Tauri command, then wire into task detail kebab menu)
|
- [x] Move task between lists (kebab menu → "Move to..." submenu in task detail view)
|
||||||
- [ ] Optional time on due dates (backend `due_date` is `DateTime<Utc>` — needs a separate `due_time` field or a nullable time component so date-only tasks don't default to midnight; currently the GUI uses `hours == 0 && minutes == 0` as a heuristic for "no time set" which breaks for actual midnight times)
|
- [ ] Optional time on due dates (backend `due_date` is `DateTime<Utc>` — needs a separate `due_time` field or a nullable time component so date-only tasks don't default to midnight; currently the GUI uses `hours == 0 && minutes == 0` as a heuristic for "no time set" which breaks for actual midnight times)
|
||||||
- [ ] Due date picker/editor (backend supports it, needs date input in new task toast + inline editing)
|
- [x] Due date picker/editor (DateTimePicker component in both new task toast + task detail view)
|
||||||
- [ ] WebDAV setup flow with credentials (settings panel has fields, triggerSync needs to pull creds from config)
|
- [x] WebDAV setup flow with credentials (settings auto-populates URL/username/password from config + keychain on open)
|
||||||
- [ ] List/workspace rename (needs `rename_list` added to onyx-core first)
|
- [x] List rename (inline input via list kebab menu in drawer)
|
||||||
- [ ] Keyboard shortcuts (Escape to close drawers/menus, tab navigation, Enter behaviors)
|
- [x] Keyboard shortcuts (Escape closes settings → detail → drawer → menus in priority order)
|
||||||
- [ ] Sync status indicators (per workspace)
|
- [ ] Sync status indicators (per workspace)
|
||||||
- [ ] Push/pull sync mode selection
|
- [ ] Push/pull sync mode selection
|
||||||
- [ ] Group-by-due-date toggle per list
|
- [x] Group-by-due-date toggle per list (checkmark toggle in list kebab menu)
|
||||||
- [ ] Subtask hierarchy (data model exists, needs UI)
|
- [ ] Subtask hierarchy (data model exists, needs UI)
|
||||||
- [ ] Search/filter tasks
|
- [ ] Search/filter tasks
|
||||||
- [ ] Desktop packaging (Windows, Linux, macOS)
|
- [ ] Desktop packaging (Windows, Linux, macOS)
|
||||||
|
- [x] File watcher (notify crate, 500ms debounce, auto-reloads UI on external file changes)
|
||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
|
|
||||||
|
|
|
||||||
9
apps/flutter/.gitignore
vendored
9
apps/flutter/.gitignore
vendored
|
|
@ -27,12 +27,21 @@ migrate_working_dir/
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
**/ios/Flutter/.last_build_id
|
**/ios/Flutter/.last_build_id
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
|
# Generated plugin registrants
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import 'frb_generated.dart';
|
import 'frb_generated.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `config_to_dto`, `ensure_repo`, `task_to_dto`
|
// These functions are ignored because they are not marked as `pub`: `config_to_dto`, `ensure_repo`, `mute_watcher`, `task_to_dto`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AppState`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AppState`
|
||||||
|
|
||||||
Future<AppConfigDto> getConfig() => RustLib.instance.api.crateApiGetConfig();
|
Future<AppConfigDto> getConfig() => RustLib.instance.api.crateApiGetConfig();
|
||||||
|
|
@ -63,6 +63,33 @@ Future<void> reorderTask({
|
||||||
newPosition: newPosition,
|
newPosition: newPosition,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<void> moveTask({
|
||||||
|
required String fromListId,
|
||||||
|
required String toListId,
|
||||||
|
required String taskId,
|
||||||
|
}) => RustLib.instance.api.crateApiMoveTask(
|
||||||
|
fromListId: fromListId,
|
||||||
|
toListId: toListId,
|
||||||
|
taskId: taskId,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> renameList({required String listId, required String newName}) =>
|
||||||
|
RustLib.instance.api.crateApiRenameList(listId: listId, newName: newName);
|
||||||
|
|
||||||
|
Future<void> setGroupByDueDate({
|
||||||
|
required String listId,
|
||||||
|
required bool enabled,
|
||||||
|
}) => RustLib.instance.api.crateApiSetGroupByDueDate(
|
||||||
|
listId: listId,
|
||||||
|
enabled: enabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool> getGroupByDueDate({required String listId}) =>
|
||||||
|
RustLib.instance.api.crateApiGetGroupByDueDate(listId: listId);
|
||||||
|
|
||||||
|
Future<Stream<void>> watchWorkspaceChanges({required String path}) =>
|
||||||
|
RustLib.instance.api.crateApiWatchWorkspaceChanges(path: path);
|
||||||
|
|
||||||
Future<String> greet({required String name}) =>
|
Future<String> greet({required String name}) =>
|
||||||
RustLib.instance.api.crateApiGreet(name: name);
|
RustLib.instance.api.crateApiGreet(name: name);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
String get codegenVersion => '2.11.1';
|
String get codegenVersion => '2.11.1';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => 1511441297;
|
int get rustContentHash => -75020133;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -97,6 +97,8 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<AppConfigDto> crateApiGetConfig();
|
Future<AppConfigDto> crateApiGetConfig();
|
||||||
|
|
||||||
|
Future<bool> crateApiGetGroupByDueDate({required String listId});
|
||||||
|
|
||||||
Future<List<TaskListDto>> crateApiGetLists();
|
Future<List<TaskListDto>> crateApiGetLists();
|
||||||
|
|
||||||
Future<String> crateApiGreet({required String name});
|
Future<String> crateApiGreet({required String name});
|
||||||
|
|
@ -105,8 +107,19 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<List<TaskDto>> crateApiListTasks({required String listId});
|
Future<List<TaskDto>> crateApiListTasks({required String listId});
|
||||||
|
|
||||||
|
Future<void> crateApiMoveTask({
|
||||||
|
required String fromListId,
|
||||||
|
required String toListId,
|
||||||
|
required String taskId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> crateApiRemoveWorkspace({required String name});
|
Future<void> crateApiRemoveWorkspace({required String name});
|
||||||
|
|
||||||
|
Future<void> crateApiRenameList({
|
||||||
|
required String listId,
|
||||||
|
required String newName,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> crateApiReorderTask({
|
Future<void> crateApiReorderTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
required String taskId,
|
required String taskId,
|
||||||
|
|
@ -115,6 +128,11 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<void> crateApiSetCurrentWorkspace({required String name});
|
Future<void> crateApiSetCurrentWorkspace({required String name});
|
||||||
|
|
||||||
|
Future<void> crateApiSetGroupByDueDate({
|
||||||
|
required String listId,
|
||||||
|
required bool enabled,
|
||||||
|
});
|
||||||
|
|
||||||
Future<TaskDto> crateApiToggleTask({
|
Future<TaskDto> crateApiToggleTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
required String taskId,
|
required String taskId,
|
||||||
|
|
@ -124,6 +142,8 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required String listId,
|
required String listId,
|
||||||
required TaskDto task,
|
required TaskDto task,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<Stream<void>> crateApiWatchWorkspaceChanges({required String path});
|
||||||
}
|
}
|
||||||
|
|
||||||
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
|
@ -321,6 +341,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
TaskConstMeta get kCrateApiGetConfigConstMeta =>
|
TaskConstMeta get kCrateApiGetConfigConstMeta =>
|
||||||
const TaskConstMeta(debugName: "get_config", argNames: []);
|
const TaskConstMeta(debugName: "get_config", argNames: []);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> crateApiGetGroupByDueDate({required String listId}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(listId, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 7,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_bool,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiGetGroupByDueDateConstMeta,
|
||||||
|
argValues: [listId],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiGetGroupByDueDateConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "get_group_by_due_date",
|
||||||
|
argNames: ["listId"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<TaskListDto>> crateApiGetLists() {
|
Future<List<TaskListDto>> crateApiGetLists() {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
|
|
@ -330,7 +380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 7,
|
funcId: 8,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -358,7 +408,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 8,
|
funcId: 9,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -386,7 +436,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 9,
|
funcId: 10,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -414,7 +464,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 10,
|
funcId: 11,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -432,6 +482,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
TaskConstMeta get kCrateApiListTasksConstMeta =>
|
TaskConstMeta get kCrateApiListTasksConstMeta =>
|
||||||
const TaskConstMeta(debugName: "list_tasks", argNames: ["listId"]);
|
const TaskConstMeta(debugName: "list_tasks", argNames: ["listId"]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiMoveTask({
|
||||||
|
required String fromListId,
|
||||||
|
required String toListId,
|
||||||
|
required String taskId,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(fromListId, serializer);
|
||||||
|
sse_encode_String(toListId, serializer);
|
||||||
|
sse_encode_String(taskId, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 12,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiMoveTaskConstMeta,
|
||||||
|
argValues: [fromListId, toListId, taskId],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiMoveTaskConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "move_task",
|
||||||
|
argNames: ["fromListId", "toListId", "taskId"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateApiRemoveWorkspace({required String name}) {
|
Future<void> crateApiRemoveWorkspace({required String name}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
|
|
@ -442,7 +528,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 11,
|
funcId: 13,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -460,6 +546,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
TaskConstMeta get kCrateApiRemoveWorkspaceConstMeta =>
|
TaskConstMeta get kCrateApiRemoveWorkspaceConstMeta =>
|
||||||
const TaskConstMeta(debugName: "remove_workspace", argNames: ["name"]);
|
const TaskConstMeta(debugName: "remove_workspace", argNames: ["name"]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiRenameList({
|
||||||
|
required String listId,
|
||||||
|
required String newName,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(listId, serializer);
|
||||||
|
sse_encode_String(newName, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 14,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiRenameListConstMeta,
|
||||||
|
argValues: [listId, newName],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiRenameListConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "rename_list",
|
||||||
|
argNames: ["listId", "newName"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateApiReorderTask({
|
Future<void> crateApiReorderTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
|
|
@ -476,7 +596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 12,
|
funcId: 15,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -506,7 +626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 13,
|
funcId: 16,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -527,6 +647,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["name"],
|
argNames: ["name"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiSetGroupByDueDate({
|
||||||
|
required String listId,
|
||||||
|
required bool enabled,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(listId, serializer);
|
||||||
|
sse_encode_bool(enabled, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 17,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSetGroupByDueDateConstMeta,
|
||||||
|
argValues: [listId, enabled],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSetGroupByDueDateConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "set_group_by_due_date",
|
||||||
|
argNames: ["listId", "enabled"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TaskDto> crateApiToggleTask({
|
Future<TaskDto> crateApiToggleTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
|
|
@ -541,7 +695,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 14,
|
funcId: 18,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -575,7 +729,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 15,
|
funcId: 19,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -595,6 +749,54 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["listId", "task"],
|
argNames: ["listId", "task"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Stream<void>> crateApiWatchWorkspaceChanges({
|
||||||
|
required String path,
|
||||||
|
}) async {
|
||||||
|
final sink = RustStreamSink<void>();
|
||||||
|
await handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(path, serializer);
|
||||||
|
sse_encode_StreamSink_unit_Sse(sink, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 20,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiWatchWorkspaceChangesConstMeta,
|
||||||
|
argValues: [path, sink],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return sink.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiWatchWorkspaceChangesConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "watch_workspace_changes",
|
||||||
|
argNames: ["path", "sink"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException dco_decode_AnyhowException(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return AnyhowException(raw as String);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> dco_decode_StreamSink_unit_Sse(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw) {
|
String dco_decode_String(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
|
@ -720,6 +922,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var inner = sse_decode_String(deserializer);
|
||||||
|
return AnyhowException(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> sse_decode_StreamSink_unit_Sse(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
throw UnimplementedError('Unreachable ()');
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer) {
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -886,6 +1103,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return deserializer.buffer.getInt32();
|
return deserializer.buffer.getInt32();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_AnyhowException(
|
||||||
|
AnyhowException self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_String(self.message, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_StreamSink_unit_Sse(
|
||||||
|
RustStreamSink<void> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_String(
|
||||||
|
self.setupAndSerialize(
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
serializer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer) {
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
required super.portManager,
|
required super.portManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> dco_decode_StreamSink_unit_Sse(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -63,6 +69,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
|
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> sse_decode_StreamSink_unit_Sse(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -113,6 +127,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_i_32(SseDeserializer deserializer);
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_AnyhowException(
|
||||||
|
AnyhowException self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_StreamSink_unit_Sse(
|
||||||
|
RustStreamSink<void> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
required super.portManager,
|
required super.portManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> dco_decode_StreamSink_unit_Sse(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -65,6 +71,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
|
WorkspaceEntry dco_decode_workspace_entry(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<void> sse_decode_StreamSink_unit_Sse(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -115,6 +129,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_i_32(SseDeserializer deserializer);
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_AnyhowException(
|
||||||
|
AnyhowException self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_StreamSink_unit_Sse(
|
||||||
|
RustStreamSink<void> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,8 @@ class _TasksScreenState extends State<TasksScreen> with SingleTickerProviderStat
|
||||||
_closeDrawer();
|
_closeDrawer();
|
||||||
},
|
},
|
||||||
onDelete: () => state.deleteList(list.id),
|
onDelete: () => state.deleteList(list.id),
|
||||||
|
onRename: (newName) => state.renameList(list.id, newName),
|
||||||
|
onToggleGroupByDueDate: () => state.setGroupByDueDate(list.id, !list.groupByDueDate),
|
||||||
),
|
),
|
||||||
// New list button / input
|
// New list button / input
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -651,8 +653,10 @@ class _ListTile extends StatefulWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
|
final void Function(String newName) onRename;
|
||||||
|
final VoidCallback onToggleGroupByDueDate;
|
||||||
|
|
||||||
const _ListTile({required this.list, required this.isActive, required this.onTap, required this.onDelete});
|
const _ListTile({required this.list, required this.isActive, required this.onTap, required this.onDelete, required this.onRename, required this.onToggleGroupByDueDate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ListTile> createState() => _ListTileState();
|
State<_ListTile> createState() => _ListTileState();
|
||||||
|
|
@ -661,6 +665,38 @@ class _ListTile extends StatefulWidget {
|
||||||
class _ListTileState extends State<_ListTile> {
|
class _ListTileState extends State<_ListTile> {
|
||||||
bool _hovering = false;
|
bool _hovering = false;
|
||||||
|
|
||||||
|
void _showRenameDialog(BuildContext context) {
|
||||||
|
final controller = TextEditingController(text: widget.list.title);
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Rename list'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(hintText: 'List name'),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
if (value.trim().isNotEmpty && value.trim() != widget.list.title) {
|
||||||
|
widget.onRename(value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
final name = controller.text.trim();
|
||||||
|
if (name.isNotEmpty && name != widget.list.title) widget.onRename(name);
|
||||||
|
},
|
||||||
|
child: const Text('Rename'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
@ -674,6 +710,20 @@ class _ListTileState extends State<_ListTile> {
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0),
|
position: RelativeRect.fromLTRB(details.globalPosition.dx, details.globalPosition.dy, 0, 0),
|
||||||
items: [
|
items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => _showRenameDialog(context),
|
||||||
|
child: const Text('Rename', style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: widget.onToggleGroupByDueDate,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text('Group by due date', style: const TextStyle(fontSize: 13))),
|
||||||
|
if (widget.list.groupByDueDate)
|
||||||
|
const Icon(Icons.check, size: 16, color: AppTheme.primary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: widget.onDelete,
|
onTap: widget.onDelete,
|
||||||
child: const Text('Delete', style: TextStyle(color: AppTheme.danger, fontSize: 13)),
|
child: const Text('Delete', style: TextStyle(color: AppTheme.danger, fontSize: 13)),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../rust/api.dart' as api;
|
import '../rust/api.dart' as api;
|
||||||
|
|
||||||
|
|
@ -8,6 +9,7 @@ class AppState extends ChangeNotifier {
|
||||||
String? activeListId;
|
String? activeListId;
|
||||||
List<api.TaskDto> tasks = [];
|
List<api.TaskDto> tasks = [];
|
||||||
bool darkMode = true;
|
bool darkMode = true;
|
||||||
|
StreamSubscription? _watcherSub;
|
||||||
bool syncing = false;
|
bool syncing = false;
|
||||||
String? error;
|
String? error;
|
||||||
|
|
||||||
|
|
@ -26,12 +28,22 @@ class AppState extends ChangeNotifier {
|
||||||
api.TaskDto? get selectedTask =>
|
api.TaskDto? get selectedTask =>
|
||||||
selectedTaskId == null ? null : tasks.cast<api.TaskDto?>().firstWhere((t) => t?.id == selectedTaskId, orElse: () => null);
|
selectedTaskId == null ? null : tasks.cast<api.TaskDto?>().firstWhere((t) => t?.id == selectedTaskId, orElse: () => null);
|
||||||
|
|
||||||
|
Future<void> _startWatcher(String path) async {
|
||||||
|
_watcherSub?.cancel();
|
||||||
|
try {
|
||||||
|
final stream = await api.watchWorkspaceChanges(path: path);
|
||||||
|
_watcherSub = stream.listen((_) => loadLists());
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadConfig() async {
|
Future<void> loadConfig() async {
|
||||||
try {
|
try {
|
||||||
config = await api.getConfig();
|
config = await api.getConfig();
|
||||||
if (hasWorkspace) {
|
if (hasWorkspace) {
|
||||||
screen = 'tasks';
|
screen = 'tasks';
|
||||||
await loadLists();
|
await loadLists();
|
||||||
|
final ws = config!.workspaces.firstWhere((w) => w.name == config!.currentWorkspace);
|
||||||
|
_startWatcher(ws.path);
|
||||||
} else {
|
} else {
|
||||||
screen = 'setup';
|
screen = 'setup';
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +60,7 @@ class AppState extends ChangeNotifier {
|
||||||
await api.addWorkspace(name: name, path: path);
|
await api.addWorkspace(name: name, path: path);
|
||||||
config = await api.getConfig();
|
config = await api.getConfig();
|
||||||
await loadLists();
|
await loadLists();
|
||||||
|
_startWatcher(path);
|
||||||
screen = 'tasks';
|
screen = 'tasks';
|
||||||
error = null;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -62,6 +75,8 @@ class AppState extends ChangeNotifier {
|
||||||
config = await api.getConfig();
|
config = await api.getConfig();
|
||||||
activeListId = null;
|
activeListId = null;
|
||||||
await loadLists();
|
await loadLists();
|
||||||
|
final ws = config!.workspaces.firstWhere((w) => w.name == name);
|
||||||
|
_startWatcher(ws.path);
|
||||||
error = null;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e.toString();
|
error = e.toString();
|
||||||
|
|
@ -199,6 +214,39 @@ class AppState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> moveTask(String taskId, String targetListId) async {
|
||||||
|
if (activeListId == null) return;
|
||||||
|
try {
|
||||||
|
await api.moveTask(fromListId: activeListId!, toListId: targetListId, taskId: taskId);
|
||||||
|
tasks = tasks.where((t) => t.id != taskId).toList();
|
||||||
|
if (selectedTaskId == taskId) selectedTaskId = null;
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> renameList(String listId, String newName) async {
|
||||||
|
try {
|
||||||
|
await api.renameList(listId: listId, newName: newName);
|
||||||
|
await loadLists();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setGroupByDueDate(String listId, bool enabled) async {
|
||||||
|
try {
|
||||||
|
await api.setGroupByDueDate(listId: listId, enabled: enabled);
|
||||||
|
await loadLists();
|
||||||
|
if (listId == activeListId) await loadTasks();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteTask(String taskId) async {
|
Future<void> deleteTask(String taskId) async {
|
||||||
if (activeListId == null) return;
|
if (activeListId == null) return;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,39 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
return '$day, ${pad(local.day)}/${pad(local.month)}$timePart';
|
return '$day, ${pad(local.day)}/${pad(local.month)}$timePart';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showMoveToSheet(BuildContext context, AppState state) {
|
||||||
|
final otherLists = state.lists.where((l) => l.id != state.activeListId).toList();
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (_) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Text('Move to...', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
for (final list in otherLists)
|
||||||
|
ListTile(
|
||||||
|
title: Text(list.title, style: const TextStyle(fontSize: 14)),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
state.moveTask(widget.task.id, list.id);
|
||||||
|
state.selectTask(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
@ -317,6 +350,15 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
state.selectTask(null);
|
state.selectTask(null);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (state.lists.where((l) => l.id != state.activeListId).isNotEmpty)
|
||||||
|
_KebabMenuItem(
|
||||||
|
icon: Icons.drive_file_move_outline,
|
||||||
|
label: 'Move to...',
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _showMenu = false);
|
||||||
|
_showMoveToSheet(context, state);
|
||||||
|
},
|
||||||
|
),
|
||||||
_KebabMenuItem(
|
_KebabMenuItem(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
|
|
|
||||||
268
apps/flutter/rust/Cargo.lock
generated
268
apps/flutter/rust/Cargo.lock
generated
|
|
@ -109,32 +109,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onyx-core"
|
name = "bitflags"
|
||||||
version = "0.1.0"
|
version = "1.3.2"
|
||||||
dependencies = [
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
"chrono",
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
"directories",
|
|
||||||
"keyring",
|
|
||||||
"quick-xml",
|
|
||||||
"reqwest",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml",
|
|
||||||
"sha2",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "onyx-flutter"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"onyx-core",
|
|
||||||
"chrono",
|
|
||||||
"flutter_rust_bridge",
|
|
||||||
"once_cell",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
|
|
@ -394,6 +372,23 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -457,6 +452,15 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsevent-sys"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -884,6 +888,35 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify-sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
|
|
@ -931,6 +964,26 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||||
|
dependencies = [
|
||||||
|
"kqueue-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue-sys"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -964,9 +1017,18 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
"plain",
|
||||||
|
"redox_syscall 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
@ -1026,10 +1088,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"filetime",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio",
|
||||||
|
"notify-types",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-debouncer-mini"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"notify",
|
||||||
|
"notify-types",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-types"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -1064,6 +1167,36 @@ version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"directories",
|
||||||
|
"keyring",
|
||||||
|
"quick-xml",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"sha2",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-flutter"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"flutter_rust_bridge",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-mini",
|
||||||
|
"once_cell",
|
||||||
|
"onyx-core",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1099,7 +1232,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
@ -1128,6 +1261,12 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plain"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
|
|
@ -1291,7 +1430,16 @@ version = "0.5.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1398,6 +1546,19 @@ version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.37"
|
version = "0.23.37"
|
||||||
|
|
@ -1445,6 +1606,15 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -1457,7 +1627,7 @@ version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1470,7 +1640,7 @@ version = "3.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1653,6 +1823,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|
@ -1786,7 +1969,7 @@ version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -1901,6 +2084,16 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -2021,7 +2214,7 @@ version = "0.244.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"semver",
|
"semver",
|
||||||
|
|
@ -2056,6 +2249,15 @@ dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
|
|
@ -2404,7 +2606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,5 @@ onyx-core = { path = "../../../crates/onyx-core" }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
notify = "7"
|
||||||
|
notify-debouncer-mini = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use flutter_rust_bridge::frb;
|
||||||
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use onyx_core::{
|
use onyx_core::{
|
||||||
config::{AppConfig, WorkspaceConfig},
|
config::{AppConfig, WorkspaceConfig},
|
||||||
models::{Task, TaskList, TaskStatus},
|
models::{Task, TaskStatus},
|
||||||
repository::TaskRepository,
|
repository::TaskRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -158,6 +161,7 @@ pub fn get_lists() -> Result<Vec<TaskListDto>, String> {
|
||||||
pub fn create_list(name: String) -> Result<TaskListDto, String> {
|
pub fn create_list(name: String) -> Result<TaskListDto, String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let list = s.repo.as_mut().unwrap().create_list(name).map_err(|e| e.to_string())?;
|
let list = s.repo.as_mut().unwrap().create_list(name).map_err(|e| e.to_string())?;
|
||||||
Ok(TaskListDto {
|
Ok(TaskListDto {
|
||||||
id: list.id.to_string(),
|
id: list.id.to_string(),
|
||||||
|
|
@ -171,6 +175,7 @@ pub fn create_list(name: String) -> Result<TaskListDto, String> {
|
||||||
pub fn delete_list(list_id: String) -> Result<(), String> {
|
pub fn delete_list(list_id: String) -> Result<(), String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
s.repo.as_mut().unwrap().delete_list(id).map_err(|e| e.to_string())
|
s.repo.as_mut().unwrap().delete_list(id).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
@ -188,6 +193,7 @@ pub fn list_tasks(list_id: String) -> Result<Vec<TaskDto>, String> {
|
||||||
pub fn create_task(list_id: String, title: String, description: String) -> Result<TaskDto, String> {
|
pub fn create_task(list_id: String, title: String, description: String) -> Result<TaskDto, String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let mut task = Task::new(title);
|
let mut task = Task::new(title);
|
||||||
if !description.is_empty() {
|
if !description.is_empty() {
|
||||||
|
|
@ -200,6 +206,7 @@ pub fn create_task(list_id: String, title: String, description: String) -> Resul
|
||||||
pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
|
pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task.id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task.id).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
|
@ -218,6 +225,7 @@ pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
|
||||||
pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
|
pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
s.repo.as_mut().unwrap().delete_task(lid, tid).map_err(|e| e.to_string())
|
s.repo.as_mut().unwrap().delete_task(lid, tid).map_err(|e| e.to_string())
|
||||||
|
|
@ -226,6 +234,7 @@ pub fn delete_task(list_id: String, task_id: String) -> Result<(), String> {
|
||||||
pub fn toggle_task(list_id: String, task_id: String) -> Result<TaskDto, String> {
|
pub fn toggle_task(list_id: String, task_id: String) -> Result<TaskDto, String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
let repo = s.repo.as_mut().unwrap();
|
let repo = s.repo.as_mut().unwrap();
|
||||||
|
|
@ -241,6 +250,7 @@ pub fn toggle_task(list_id: String, task_id: String) -> Result<TaskDto, String>
|
||||||
pub fn reorder_task(list_id: String, task_id: String, new_position: u32) -> Result<(), String> {
|
pub fn reorder_task(list_id: String, task_id: String, new_position: u32) -> Result<(), String> {
|
||||||
let mut s = STATE.lock().unwrap();
|
let mut s = STATE.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
s.repo
|
s.repo
|
||||||
|
|
@ -250,6 +260,79 @@ pub fn reorder_task(list_id: String, task_id: String, new_position: u32) -> Resu
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Move / rename / grouping ───────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn move_task(from_list_id: String, to_list_id: String, task_id: String) -> Result<(), String> {
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
|
let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?;
|
||||||
|
let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?;
|
||||||
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo.as_mut().unwrap().move_task(from, to, tid).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rename_list(list_id: String, new_name: String) -> Result<(), String> {
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo.as_mut().unwrap().rename_list(id, new_name).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_group_by_due_date(list_id: String, enabled: bool) -> Result<(), String> {
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher();
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo.as_mut().unwrap().set_group_by_due_date(id, enabled).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_group_by_due_date(list_id: String) -> Result<bool, String> {
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo.as_ref().unwrap().get_group_by_due_date(id).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── File watcher ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
|
||||||
|
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
|
||||||
|
|
||||||
|
fn mute_watcher() {
|
||||||
|
*LAST_WRITE.lock().unwrap() = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[frb(stream_dart_await)]
|
||||||
|
pub fn watch_workspace_changes(path: String, sink: crate::frb_generated::StreamSink<()>) {
|
||||||
|
let debouncer = new_debouncer(
|
||||||
|
Duration::from_millis(500),
|
||||||
|
move |events: Result<Vec<notify_debouncer_mini::DebouncedEvent>, notify::Error>| {
|
||||||
|
let Ok(events) = events else { return };
|
||||||
|
let has_data_change = events.iter().any(|e| {
|
||||||
|
if e.kind != DebouncedEventKind::Any { return false; }
|
||||||
|
let p = e.path.to_string_lossy();
|
||||||
|
p.ends_with(".md") || p.ends_with(".json")
|
||||||
|
});
|
||||||
|
if !has_data_change { return; }
|
||||||
|
if let Some(t) = *LAST_WRITE.lock().unwrap() {
|
||||||
|
if t.elapsed() < Duration::from_secs(1) { return; }
|
||||||
|
}
|
||||||
|
let _ = sink.add(());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match debouncer {
|
||||||
|
Ok(mut d) => {
|
||||||
|
let _ = d.watcher().watch(&PathBuf::from(&path), notify::RecursiveMode::Recursive);
|
||||||
|
*WATCHER.lock().unwrap() = Some(d);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to start file watcher: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Test function ───────────────────────────────────────────────────
|
// ── Test function ───────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn greet(name: String) -> String {
|
pub fn greet(name: String) -> String {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1511441297;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -75020133;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -247,6 +247,39 @@ fn wire__crate__api__get_config_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__get_group_by_due_date_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "get_group_by_due_date",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_list_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::get_group_by_due_date(api_list_id)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__get_lists_impl(
|
fn wire__crate__api__get_lists_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -378,6 +411,42 @@ fn wire__crate__api__list_tasks_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__move_task_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "move_task",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_from_list_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_to_list_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_task_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok =
|
||||||
|
crate::api::move_task(api_from_list_id, api_to_list_id, api_task_id)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__remove_workspace_impl(
|
fn wire__crate__api__remove_workspace_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -411,6 +480,40 @@ fn wire__crate__api__remove_workspace_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__rename_list_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rename_list",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_list_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_new_name = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::rename_list(api_list_id, api_new_name)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__reorder_task_impl(
|
fn wire__crate__api__reorder_task_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -480,6 +583,40 @@ fn wire__crate__api__set_current_workspace_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__set_group_by_due_date_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "set_group_by_due_date",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_list_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_enabled = <bool>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::set_group_by_due_date(api_list_id, api_enabled)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__toggle_task_impl(
|
fn wire__crate__api__toggle_task_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -548,9 +685,64 @@ fn wire__crate__api__update_task_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__watch_workspace_changes_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "watch_workspace_changes",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_sink =
|
||||||
|
<StreamSink<(), flutter_rust_bridge::for_generated::SseCodec>>::sse_decode(
|
||||||
|
&mut deserializer,
|
||||||
|
);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok({
|
||||||
|
crate::api::watch_workspace_changes(api_path, api_sink);
|
||||||
|
})?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Section: dart2rust
|
// Section: dart2rust
|
||||||
|
|
||||||
|
impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut inner = <String>::sse_decode(deserializer);
|
||||||
|
return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for StreamSink<(), flutter_rust_bridge::for_generated::SseCodec> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut inner = <String>::sse_decode(deserializer);
|
||||||
|
return StreamSink::deserialize(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for String {
|
impl SseDecode for String {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -736,15 +928,20 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
4 => wire__crate__api__delete_list_impl(port, ptr, rust_vec_len, data_len),
|
4 => wire__crate__api__delete_list_impl(port, ptr, rust_vec_len, data_len),
|
||||||
5 => wire__crate__api__delete_task_impl(port, ptr, rust_vec_len, data_len),
|
5 => wire__crate__api__delete_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
6 => wire__crate__api__get_config_impl(port, ptr, rust_vec_len, data_len),
|
6 => wire__crate__api__get_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
7 => wire__crate__api__get_lists_impl(port, ptr, rust_vec_len, data_len),
|
7 => wire__crate__api__get_group_by_due_date_impl(port, ptr, rust_vec_len, data_len),
|
||||||
8 => wire__crate__api__greet_impl(port, ptr, rust_vec_len, data_len),
|
8 => wire__crate__api__get_lists_impl(port, ptr, rust_vec_len, data_len),
|
||||||
9 => wire__crate__api__init_workspace_impl(port, ptr, rust_vec_len, data_len),
|
9 => wire__crate__api__greet_impl(port, ptr, rust_vec_len, data_len),
|
||||||
10 => wire__crate__api__list_tasks_impl(port, ptr, rust_vec_len, data_len),
|
10 => wire__crate__api__init_workspace_impl(port, ptr, rust_vec_len, data_len),
|
||||||
11 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len),
|
11 => wire__crate__api__list_tasks_impl(port, ptr, rust_vec_len, data_len),
|
||||||
12 => wire__crate__api__reorder_task_impl(port, ptr, rust_vec_len, data_len),
|
12 => wire__crate__api__move_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
13 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len),
|
13 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len),
|
||||||
14 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len),
|
14 => wire__crate__api__rename_list_impl(port, ptr, rust_vec_len, data_len),
|
||||||
15 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len),
|
15 => wire__crate__api__reorder_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
16 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
17 => wire__crate__api__set_group_by_due_date_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
18 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
19 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
20 => wire__crate__api__watch_workspace_changes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -839,6 +1036,20 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::WorkspaceEntry> for crate::ap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<String>::sse_encode(format!("{:?}", self), serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for StreamSink<(), flutter_rust_bridge::for_generated::SseCodec> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
unimplemented!("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for String {
|
impl SseEncode for String {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
|
||||||
|
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
|
||||||
#include <window_manager/window_manager_plugin.h>
|
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|
||||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
|
||||||
WindowManagerPluginRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
//
|
|
||||||
// Generated file. Do not edit.
|
|
||||||
//
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
|
|
||||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
#define GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
||||||
#include <flutter/plugin_registry.h>
|
|
||||||
|
|
||||||
// Registers Flutter plugins.
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
|
||||||
|
|
||||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#
|
|
||||||
# Generated file, do not edit.
|
|
||||||
#
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
|
||||||
screen_retriever_windows
|
|
||||||
window_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
|
||||||
)
|
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
|
||||||
|
|
||||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
|
||||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
|
||||||
endforeach(plugin)
|
|
||||||
|
|
||||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
|
||||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
|
||||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
|
||||||
endforeach(ffi_plugin)
|
|
||||||
213
apps/tauri/src-tauri/Cargo.lock
generated
213
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -94,39 +94,6 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "onyx-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"directories",
|
|
||||||
"keyring",
|
|
||||||
"quick-xml 0.36.2",
|
|
||||||
"reqwest 0.12.28",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml",
|
|
||||||
"sha2",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "onyx-tauri"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"onyx-core",
|
|
||||||
"chrono",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-build",
|
|
||||||
"tauri-plugin-dialog",
|
|
||||||
"tauri-plugin-os",
|
|
||||||
"tokio",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -842,6 +809,17 @@ dependencies = [
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -912,6 +890,15 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsevent-sys"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -1661,6 +1648,35 @@ dependencies = [
|
||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify-sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
|
|
@ -1786,6 +1802,26 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||||
|
dependencies = [
|
||||||
|
"kqueue-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue-sys"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kuchikiki"
|
name = "kuchikiki"
|
||||||
version = "0.8.8-speedreader"
|
version = "0.8.8-speedreader"
|
||||||
|
|
@ -1859,7 +1895,10 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
"libc",
|
"libc",
|
||||||
|
"plain",
|
||||||
|
"redox_syscall 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1981,6 +2020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
@ -2060,6 +2100,46 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"filetime",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio",
|
||||||
|
"notify-types",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-debouncer-mini"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"notify",
|
||||||
|
"notify-types",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-types"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2299,6 +2379,41 @@ version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"directories",
|
||||||
|
"keyring",
|
||||||
|
"quick-xml 0.36.2",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"sha2",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onyx-tauri"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-mini",
|
||||||
|
"onyx-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-os",
|
||||||
|
"tokio",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2364,7 +2479,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
@ -2580,6 +2695,12 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plain"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plist"
|
name = "plist"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|
@ -2933,6 +3054,15 @@ dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
@ -3612,7 +3742,7 @@ dependencies = [
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
|
@ -4131,6 +4261,19 @@ dependencies = [
|
||||||
"toml 0.9.12+spec-1.1.0",
|
"toml 0.9.12+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tendril"
|
name = "tendril"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ onyx-core = { path = "../../../crates/onyx-core" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
notify = "7"
|
||||||
|
notify-debouncer-mini = "0.5"
|
||||||
|
|
||||||
[package.metadata.tauri]
|
[package.metadata.tauri]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::State;
|
use tauri::{Emitter, Manager, State};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use onyx_core::{
|
use onyx_core::{
|
||||||
|
|
@ -13,6 +15,13 @@ use onyx_core::{
|
||||||
webdav,
|
webdav,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Active file watcher stored globally so it lives for the app lifetime.
|
||||||
|
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
|
||||||
|
/// Shared mute timestamp — set before writes, checked by the watcher.
|
||||||
|
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
|
||||||
|
|
||||||
/// Shared application state behind a mutex.
|
/// Shared application state behind a mutex.
|
||||||
struct AppState {
|
struct AppState {
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
|
|
@ -43,6 +52,11 @@ impl From<CoreSyncResult> for SyncResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Suppress file watcher events for the next second (call before writes).
|
||||||
|
fn mute_watcher(_state: &mut AppState) {
|
||||||
|
*LAST_WRITE.lock().unwrap() = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper: get or open a TaskRepository for the current workspace.
|
/// Helper: get or open a TaskRepository for the current workspace.
|
||||||
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
|
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
|
||||||
if state.repo.is_some() {
|
if state.repo.is_some() {
|
||||||
|
|
@ -151,6 +165,7 @@ fn create_list(
|
||||||
) -> Result<TaskList, String> {
|
) -> Result<TaskList, String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
s.repo
|
s.repo
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -165,6 +180,7 @@ fn delete_list(
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
s.repo
|
s.repo
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
|
@ -199,6 +215,7 @@ fn create_task(
|
||||||
) -> Result<Task, String> {
|
) -> Result<Task, String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let mut task = Task::new(title);
|
let mut task = Task::new(title);
|
||||||
if let Some(desc) = description.filter(|d| !d.is_empty()) {
|
if let Some(desc) = description.filter(|d| !d.is_empty()) {
|
||||||
|
|
@ -219,6 +236,7 @@ fn update_task(
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
s.repo
|
s.repo
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
|
@ -235,6 +253,7 @@ fn delete_task(
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
s.repo
|
s.repo
|
||||||
|
|
@ -252,6 +271,7 @@ fn toggle_task(
|
||||||
) -> Result<Task, String> {
|
) -> Result<Task, String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
let repo = s.repo.as_mut().unwrap();
|
let repo = s.repo.as_mut().unwrap();
|
||||||
|
|
@ -274,6 +294,7 @@ fn reorder_task(
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
s.repo
|
s.repo
|
||||||
|
|
@ -283,6 +304,77 @@ fn reorder_task(
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Move / rename / grouping ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn move_task(
|
||||||
|
from_list_id: String,
|
||||||
|
to_list_id: String,
|
||||||
|
task_id: String,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
|
let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?;
|
||||||
|
let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?;
|
||||||
|
let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.move_task(from, to, tid)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn rename_list(
|
||||||
|
list_id: String,
|
||||||
|
new_name: String,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.rename_list(id, new_name)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_group_by_due_date(
|
||||||
|
list_id: String,
|
||||||
|
enabled: bool,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
mute_watcher(&mut s);
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.set_group_by_due_date(id, enabled)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_group_by_due_date(
|
||||||
|
list_id: String,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
ensure_repo(&mut s)?;
|
||||||
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
|
s.repo
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_group_by_due_date(id)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sync commands ────────────────────────────────────────────────────
|
// ── Sync commands ────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -348,6 +440,43 @@ async fn sync_workspace(
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── File watcher ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
|
||||||
|
let handle = handle.clone();
|
||||||
|
let debouncer = new_debouncer(
|
||||||
|
std::time::Duration::from_millis(500),
|
||||||
|
move |events: Result<Vec<notify_debouncer_mini::DebouncedEvent>, notify::Error>| {
|
||||||
|
let Ok(events) = events else { return };
|
||||||
|
// Only care about data file changes
|
||||||
|
let has_data_change = events.iter().any(|e| {
|
||||||
|
if e.kind != DebouncedEventKind::Any { return false; }
|
||||||
|
let p = e.path.to_string_lossy();
|
||||||
|
p.ends_with(".md") || p.ends_with(".json")
|
||||||
|
});
|
||||||
|
if !has_data_change { return; }
|
||||||
|
// Skip if we wrote recently (self-change suppression)
|
||||||
|
if let Some(t) = *LAST_WRITE.lock().unwrap() {
|
||||||
|
if t.elapsed() < std::time::Duration::from_secs(1) { return; }
|
||||||
|
}
|
||||||
|
let _ = handle.emit("fs-changed", ());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
match debouncer {
|
||||||
|
Ok(mut d) => {
|
||||||
|
let _ = d.watcher().watch(&path, notify::RecursiveMode::Recursive);
|
||||||
|
*WATCHER.lock().unwrap() = Some(d);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to start file watcher: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
start_watcher(app_handle, PathBuf::from(path));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ── App entry ────────────────────────────────────────────────────────
|
// ── App entry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
|
@ -360,6 +489,18 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.manage(Mutex::new(AppState { config, repo: None }))
|
.manage(Mutex::new(AppState { config, repo: None }))
|
||||||
|
.setup(|app| {
|
||||||
|
let handle = app.handle().clone();
|
||||||
|
let state: State<'_, Mutex<AppState>> = app.state();
|
||||||
|
let workspace_path = {
|
||||||
|
let s = state.lock().unwrap();
|
||||||
|
s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone())
|
||||||
|
};
|
||||||
|
if let Some(path) = workspace_path {
|
||||||
|
start_watcher(handle, path);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_config,
|
get_config,
|
||||||
save_config,
|
save_config,
|
||||||
|
|
@ -376,11 +517,16 @@ pub fn run() {
|
||||||
delete_task,
|
delete_task,
|
||||||
toggle_task,
|
toggle_task,
|
||||||
reorder_task,
|
reorder_task,
|
||||||
|
move_task,
|
||||||
|
rename_list,
|
||||||
|
set_group_by_due_date,
|
||||||
|
get_group_by_due_date,
|
||||||
set_webdav_config,
|
set_webdav_config,
|
||||||
store_credentials,
|
store_credentials,
|
||||||
load_credentials,
|
load_credentials,
|
||||||
test_webdav_connection,
|
test_webdav_connection,
|
||||||
sync_workspace,
|
sync_workspace,
|
||||||
|
watch_workspace,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@
|
||||||
let title = $state(task.title);
|
let title = $state(task.title);
|
||||||
let description = $state(task.description);
|
let description = $state(task.description);
|
||||||
let showMenu = $state(false);
|
let showMenu = $state(false);
|
||||||
|
let showMoveSubmenu = $state(false);
|
||||||
let menuEl = $state<HTMLDivElement | null>(null);
|
let menuEl = $state<HTMLDivElement | null>(null);
|
||||||
let showDatePicker = $state(false);
|
let showDatePicker = $state(false);
|
||||||
let saveTimer: ReturnType<typeof setTimeout>;
|
let saveTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
let otherLists = $derived(app.lists.filter((l) => l.id !== app.activeListId));
|
||||||
|
|
||||||
function handleHeaderMouseDown(e: MouseEvent) {
|
function handleHeaderMouseDown(e: MouseEvent) {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if ((e.target as HTMLElement).closest("button")) return;
|
if ((e.target as HTMLElement).closest("button")) return;
|
||||||
|
|
@ -126,6 +129,34 @@
|
||||||
</svg>
|
</svg>
|
||||||
{isCompleted ? "Restore task" : "Mark as completed"}
|
{isCompleted ? "Restore task" : "Mark as completed"}
|
||||||
</button>
|
</button>
|
||||||
|
{#if otherLists.length > 0}
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
onclick={() => (showMoveSubmenu = !showMoveSubmenu)}
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
|
||||||
|
</svg>
|
||||||
|
Move to...
|
||||||
|
<svg class="ml-auto h-3 w-3 opacity-40" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if showMoveSubmenu}
|
||||||
|
<div class="absolute left-full top-0 z-50 ml-1 min-w-[160px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||||
|
{#each otherLists as list}
|
||||||
|
<button
|
||||||
|
onclick={async () => { showMenu = false; showMoveSubmenu = false; await app.moveTask(task.id, list.id); onback(); }}
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{list.title}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<button
|
<button
|
||||||
onclick={handleDelete}
|
onclick={handleDelete}
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@
|
||||||
let webdavPass = $state("");
|
let webdavPass = $state("");
|
||||||
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const ws = app.config?.current_workspace;
|
||||||
|
if (!ws) return;
|
||||||
|
const cfg = app.config?.workspaces[ws];
|
||||||
|
if (cfg?.webdav_url) {
|
||||||
|
webdavUrl = cfg.webdav_url;
|
||||||
|
try {
|
||||||
|
const domain = new URL(cfg.webdav_url).hostname;
|
||||||
|
invoke<[string, string]>("load_credentials", { domain }).then(([u, p]) => {
|
||||||
|
webdavUser = u;
|
||||||
|
webdavPass = p;
|
||||||
|
}).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
testStatus = "testing";
|
testStatus = "testing";
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { app } from "../stores/app.svelte";
|
import { app } from "../stores/app.svelte";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
|
const appWindow = getCurrentWindow();
|
||||||
|
const currentPlatform = platform();
|
||||||
|
const isDesktop = currentPlatform === "linux" || currentPlatform === "windows";
|
||||||
|
const isWindows = currentPlatform === "windows";
|
||||||
|
|
||||||
let name = $state("");
|
let name = $state("");
|
||||||
let path = $state("");
|
let path = $state("");
|
||||||
|
|
@ -14,51 +21,109 @@
|
||||||
if (!name.trim() || !path.trim()) return;
|
if (!name.trim() || !path.trim()) return;
|
||||||
await app.addWorkspace(name.trim(), path.trim());
|
await app.addWorkspace(name.trim(), path.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleOpen() {
|
||||||
|
const selected = await open({ directory: true, multiple: false });
|
||||||
|
if (!selected) return;
|
||||||
|
const folder = selected as string;
|
||||||
|
// Derive workspace name from folder name
|
||||||
|
const parts = folder.replace(/\\/g, "/").split("/");
|
||||||
|
const wsName = parts[parts.length - 1] || "workspace";
|
||||||
|
await app.addWorkspace(wsName, folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrag(e: MouseEvent) {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if ((e.target as HTMLElement).closest("button, input")) return;
|
||||||
|
if (isDesktop) appWindow.startDragging();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full items-center justify-center p-6">
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div class="flex h-full flex-col" onmousedown={handleDrag}>
|
||||||
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
<!-- Title bar area with window controls -->
|
||||||
>
|
<header class="flex h-11 shrink-0 items-center justify-end px-2">
|
||||||
<h1 class="mb-1 text-2xl font-bold">Onyx</h1>
|
{#if isDesktop}
|
||||||
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
<div class="flex items-center gap-0.5">
|
||||||
Create or open a workspace to get started.
|
{#if isWindows}
|
||||||
</p>
|
<button
|
||||||
|
onclick={() => appWindow.minimize()}
|
||||||
|
class="rounded p-1.5 opacity-50 hover:bg-black/10 hover:opacity-80 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M4 10a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
onclick={() => appWindow.close()}
|
||||||
|
class="rounded p-1.5 opacity-50 hover:bg-danger/20 hover:opacity-100 hover:text-danger dark:hover:bg-danger/20"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
<label class="mb-1 block text-sm font-medium">
|
<div class="flex flex-1 items-center justify-center p-6">
|
||||||
Workspace name
|
<div
|
||||||
<input
|
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
||||||
type="text"
|
>
|
||||||
bind:value={name}
|
<h1 class="mb-1 text-2xl font-bold">Onyx</h1>
|
||||||
placeholder="My Tasks"
|
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
class="mt-1 mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm font-normal outline-none focus:border-primary dark:border-border-dark"
|
Create a new workspace or open an existing one.
|
||||||
/>
|
</p>
|
||||||
</label>
|
|
||||||
|
<label class="mb-1 block text-sm font-medium">
|
||||||
|
Workspace name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
placeholder="My Tasks"
|
||||||
|
class="mt-1 mb-4 w-full rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm font-normal outline-none focus:border-primary dark:border-border-dark"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||||
|
<label class="mb-1 block text-sm font-medium">Folder</label>
|
||||||
|
<div class="mb-6 flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={path}
|
||||||
|
readonly
|
||||||
|
placeholder="Select a folder…"
|
||||||
|
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm dark:border-border-dark"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={pickFolder}
|
||||||
|
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
||||||
<label class="mb-1 block text-sm font-medium">Folder</label>
|
|
||||||
<div class="mb-6 flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={path}
|
|
||||||
readonly
|
|
||||||
placeholder="Select a folder…"
|
|
||||||
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm dark:border-border-dark"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
onclick={pickFolder}
|
onclick={handleCreate}
|
||||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
disabled={!name.trim() || !path.trim()}
|
||||||
|
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Browse
|
Create Workspace
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="my-4 flex items-center gap-3">
|
||||||
|
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
||||||
|
<span class="text-xs opacity-40">or</span>
|
||||||
|
<div class="h-px flex-1 bg-border-light dark:bg-border-dark"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={handleOpen}
|
||||||
|
class="w-full rounded-lg border border-border-light py-2.5 text-sm font-medium hover:bg-black/5 dark:border-border-dark dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Open Existing Folder
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
onclick={handleCreate}
|
|
||||||
disabled={!name.trim() || !path.trim()}
|
|
||||||
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
|
||||||
>
|
|
||||||
Create Workspace
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@
|
||||||
let completedVisible = $state(false);
|
let completedVisible = $state(false);
|
||||||
let listMenuId = $state<string | null>(null);
|
let listMenuId = $state<string | null>(null);
|
||||||
let wsMenuName = $state<string | null>(null);
|
let wsMenuName = $state<string | null>(null);
|
||||||
|
let renamingListId = $state<string | null>(null);
|
||||||
|
let renameValue = $state("");
|
||||||
let dragId = $state<string | null>(null);
|
let dragId = $state<string | null>(null);
|
||||||
let dragOverId = $state<string | null>(null);
|
let dragOverId = $state<string | null>(null);
|
||||||
let resizing = $state(false);
|
let resizing = $state(false);
|
||||||
|
|
@ -77,6 +79,40 @@
|
||||||
await app.deleteList(id);
|
await app.deleteList(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startRenameList(id: string) {
|
||||||
|
listMenuId = null;
|
||||||
|
const list = app.lists.find(l => l.id === id);
|
||||||
|
if (!list) return;
|
||||||
|
renamingListId = id;
|
||||||
|
renameValue = list.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRenameList() {
|
||||||
|
if (!renamingListId || !renameValue.trim()) { renamingListId = null; return; }
|
||||||
|
const list = app.lists.find(l => l.id === renamingListId);
|
||||||
|
if (renameValue.trim() !== list?.title) {
|
||||||
|
await app.renameList(renamingListId, renameValue.trim());
|
||||||
|
}
|
||||||
|
renamingListId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleGroupByDueDate(id: string) {
|
||||||
|
listMenuId = null;
|
||||||
|
const list = app.lists.find(l => l.id === id);
|
||||||
|
if (!list) return;
|
||||||
|
await app.setGroupByDueDate(id, !list.group_by_due_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key !== "Escape") return;
|
||||||
|
if (showSettings) { showSettings = false; return; }
|
||||||
|
if (selectedTaskId) { selectedTaskId = null; return; }
|
||||||
|
if (showDrawer) { closeDrawer(); return; }
|
||||||
|
if (listMenuId) { listMenuId = null; return; }
|
||||||
|
if (wsMenuName) { wsMenuName = null; return; }
|
||||||
|
if (showWorkspacePicker) { showWorkspacePicker = false; return; }
|
||||||
|
}
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent, taskId: string) {
|
function handleDragStart(e: DragEvent, taskId: string) {
|
||||||
dragId = taskId;
|
dragId = taskId;
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
|
|
@ -148,6 +184,8 @@
|
||||||
let translateX = $derived(showDrawer ? '0' : '-80cqi');
|
let translateX = $derived(showDrawer ? '0' : '-80cqi');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<!-- Viewport clip -->
|
<!-- Viewport clip -->
|
||||||
<div class="h-full w-full overflow-hidden">
|
<div class="h-full w-full overflow-hidden">
|
||||||
<!-- Sliding container: left drawer + main content -->
|
<!-- Sliding container: left drawer + main content -->
|
||||||
|
|
@ -238,17 +276,30 @@
|
||||||
<div class="flex-1 overflow-y-auto py-2">
|
<div class="flex-1 overflow-y-auto py-2">
|
||||||
{#each app.lists as list (list.id)}
|
{#each app.lists as list (list.id)}
|
||||||
<div class="group relative flex items-center px-2 hover:bg-black/5 dark:hover:bg-white/10">
|
<div class="group relative flex items-center px-2 hover:bg-black/5 dark:hover:bg-white/10">
|
||||||
<button
|
{#if renamingListId === list.id}
|
||||||
onclick={() => { app.selectList(list.id); closeDrawer(); }}
|
<div class="flex flex-1 items-center px-3 py-1">
|
||||||
class="flex flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm {list.id === app.activeListId ? 'font-bold' : ''}"
|
<input
|
||||||
>
|
type="text"
|
||||||
{#if list.id === app.activeListId}
|
bind:value={renameValue}
|
||||||
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
class="w-full rounded border border-primary bg-transparent px-2 py-1.5 text-sm outline-none"
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
onkeydown={(e) => { if (e.key === "Enter") handleRenameList(); if (e.key === "Escape") renamingListId = null; }}
|
||||||
</svg>
|
onblur={handleRenameList}
|
||||||
{/if}
|
autofocus
|
||||||
<span>{list.title}</span>
|
/>
|
||||||
</button>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
onclick={() => { app.selectList(list.id); closeDrawer(); }}
|
||||||
|
class="flex flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm {list.id === app.activeListId ? 'font-bold' : ''}"
|
||||||
|
>
|
||||||
|
{#if list.id === app.activeListId}
|
||||||
|
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span>{list.title}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
<div class="relative shrink-0" data-list-menu>
|
<div class="relative shrink-0" data-list-menu>
|
||||||
<button
|
<button
|
||||||
onclick={() => (listMenuId = listMenuId === list.id ? null : list.id)}
|
onclick={() => (listMenuId = listMenuId === list.id ? null : list.id)}
|
||||||
|
|
@ -259,7 +310,30 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if listMenuId === list.id}
|
{#if listMenuId === list.id}
|
||||||
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
<div class="absolute right-0 top-full z-40 mt-1 min-w-[180px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
|
||||||
|
<button
|
||||||
|
onclick={() => startRenameList(list.id)}
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||||
|
</svg>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => handleToggleGroupByDueDate(list.id)}
|
||||||
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Group by due date
|
||||||
|
{#if list.group_by_due_date}
|
||||||
|
<svg class="ml-auto h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleDeleteList(list.id)}
|
onclick={() => handleDeleteList(list.id)}
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import type {
|
import type {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
Task,
|
Task,
|
||||||
|
|
@ -7,6 +8,11 @@ import type {
|
||||||
SyncResult,
|
SyncResult,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
// Listen for file system changes from the backend watcher
|
||||||
|
listen("fs-changed", () => {
|
||||||
|
loadLists();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Reactive state ───────────────────────────────────────────────────
|
// ── Reactive state ───────────────────────────────────────────────────
|
||||||
|
|
||||||
let screen = $state<Screen>("setup");
|
let screen = $state<Screen>("setup");
|
||||||
|
|
@ -54,6 +60,7 @@ async function addWorkspace(name: string, path: string) {
|
||||||
await invoke("add_workspace", { name, path });
|
await invoke("add_workspace", { name, path });
|
||||||
config = await invoke<AppConfig>("get_config");
|
config = await invoke<AppConfig>("get_config");
|
||||||
await loadLists();
|
await loadLists();
|
||||||
|
invoke("watch_workspace", { path }).catch(() => {});
|
||||||
screen = "tasks";
|
screen = "tasks";
|
||||||
error = null;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -67,6 +74,8 @@ async function switchWorkspace(name: string) {
|
||||||
config = await invoke<AppConfig>("get_config");
|
config = await invoke<AppConfig>("get_config");
|
||||||
activeListId = null;
|
activeListId = null;
|
||||||
await loadLists();
|
await loadLists();
|
||||||
|
const ws = config?.workspaces[name];
|
||||||
|
if (ws) invoke("watch_workspace", { path: ws.path }).catch(() => {});
|
||||||
error = null;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e);
|
error = String(e);
|
||||||
|
|
@ -206,6 +215,43 @@ async function deleteTask(taskId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function moveTask(taskId: string, targetListId: string) {
|
||||||
|
if (!activeListId) return;
|
||||||
|
try {
|
||||||
|
await invoke("move_task", {
|
||||||
|
fromListId: activeListId,
|
||||||
|
toListId: targetListId,
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
tasks = tasks.filter((t) => t.id !== taskId);
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameList(listId: string, newName: string) {
|
||||||
|
try {
|
||||||
|
await invoke("rename_list", { listId, newName });
|
||||||
|
lists = lists.map((l) =>
|
||||||
|
l.id === listId ? { ...l, title: newName } : l,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setGroupByDueDate(listId: string, enabled: boolean) {
|
||||||
|
try {
|
||||||
|
await invoke("set_group_by_due_date", { listId, enabled });
|
||||||
|
lists = lists.map((l) =>
|
||||||
|
l.id === listId ? { ...l, group_by_due_date: enabled } : l,
|
||||||
|
);
|
||||||
|
if (listId === activeListId) await loadTasks();
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function triggerSync() {
|
async function triggerSync() {
|
||||||
if (!config?.current_workspace) return;
|
if (!config?.current_workspace) return;
|
||||||
const ws = config.workspaces[config.current_workspace];
|
const ws = config.workspaces[config.current_workspace];
|
||||||
|
|
@ -300,6 +346,9 @@ export const app = {
|
||||||
updateTask,
|
updateTask,
|
||||||
reorderTask,
|
reorderTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
|
moveTask,
|
||||||
|
renameList,
|
||||||
|
setGroupByDueDate,
|
||||||
triggerSync,
|
triggerSync,
|
||||||
toggleDarkMode,
|
toggleDarkMode,
|
||||||
setScreen,
|
setScreen,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,17 @@ impl TaskRepository {
|
||||||
self.storage.delete_list(list_id)
|
self.storage.delete_list(list_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> {
|
||||||
|
self.storage.rename_list(list_id, new_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_task(&mut self, from_list_id: Uuid, to_list_id: Uuid, task_id: Uuid) -> Result<()> {
|
||||||
|
let task = self.storage.read_task(from_list_id, task_id)?;
|
||||||
|
self.storage.write_task(to_list_id, &task)?;
|
||||||
|
self.storage.delete_task(from_list_id, task_id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Task ordering
|
// Task ordering
|
||||||
pub fn reorder_task(&mut self, list_id: Uuid, task_id: Uuid, new_position: usize) -> Result<()> {
|
pub fn reorder_task(&mut self, list_id: Uuid, task_id: Uuid, new_position: usize) -> Result<()> {
|
||||||
let mut metadata = self.storage.read_list_metadata(list_id)?;
|
let mut metadata = self.storage.read_list_metadata(list_id)?;
|
||||||
|
|
@ -320,6 +331,54 @@ mod tests {
|
||||||
assert!(lists.is_empty());
|
assert!(lists.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_task_between_lists() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
||||||
|
|
||||||
|
let list_a = repo.create_list("List A".to_string()).unwrap();
|
||||||
|
let list_b = repo.create_list("List B".to_string()).unwrap();
|
||||||
|
let task = repo.create_task(list_a.id, Task::new("Movable".to_string())).unwrap();
|
||||||
|
|
||||||
|
repo.move_task(list_a.id, list_b.id, task.id).unwrap();
|
||||||
|
|
||||||
|
let tasks_a = repo.list_tasks(list_a.id).unwrap();
|
||||||
|
assert_eq!(tasks_a.len(), 0);
|
||||||
|
|
||||||
|
let tasks_b = repo.list_tasks(list_b.id).unwrap();
|
||||||
|
assert_eq!(tasks_b.len(), 1);
|
||||||
|
assert_eq!(tasks_b[0].title, "Movable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rename_list() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
||||||
|
|
||||||
|
let list = repo.create_list("Old Name".to_string()).unwrap();
|
||||||
|
repo.rename_list(list.id, "New Name".to_string()).unwrap();
|
||||||
|
|
||||||
|
let renamed = repo.get_list(list.id).unwrap();
|
||||||
|
assert_eq!(renamed.title, "New Name");
|
||||||
|
|
||||||
|
// Old directory should be gone
|
||||||
|
assert!(!temp_dir.path().join("Old Name").exists());
|
||||||
|
assert!(temp_dir.path().join("New Name").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rename_list_duplicate_name() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
||||||
|
|
||||||
|
repo.create_list("A".to_string()).unwrap();
|
||||||
|
let list_b = repo.create_list("B".to_string()).unwrap();
|
||||||
|
|
||||||
|
let result = repo.rename_list(list_b.id, "A".to_string());
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), Error::InvalidData(_)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_delete_list_removes_from_root_metadata() {
|
fn test_delete_list_removes_from_root_metadata() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ pub trait Storage {
|
||||||
fn read_root_metadata(&self) -> Result<RootMetadata>;
|
fn read_root_metadata(&self) -> Result<RootMetadata>;
|
||||||
fn write_root_metadata(&mut self, metadata: &RootMetadata) -> Result<()>;
|
fn write_root_metadata(&mut self, metadata: &RootMetadata) -> Result<()>;
|
||||||
|
|
||||||
|
fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()>;
|
||||||
|
|
||||||
fn read_list_metadata(&self, list_id: Uuid) -> Result<ListMetadata>;
|
fn read_list_metadata(&self, list_id: Uuid) -> Result<ListMetadata>;
|
||||||
fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>;
|
fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
@ -464,6 +466,27 @@ impl Storage for FileSystemStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> {
|
||||||
|
let old_dir = self.list_dir_path(list_id)?;
|
||||||
|
let new_dir = self.list_dir_path_by_name(&new_name);
|
||||||
|
|
||||||
|
if new_dir.exists() {
|
||||||
|
return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(&old_dir, &new_dir)?;
|
||||||
|
|
||||||
|
// Update metadata timestamp
|
||||||
|
let metadata_path = new_dir.join(".listdata.json");
|
||||||
|
let content = fs::read_to_string(&metadata_path)?;
|
||||||
|
let mut metadata: ListMetadata = serde_json::from_str(&content)?;
|
||||||
|
metadata.updated_at = Utc::now();
|
||||||
|
let json = serde_json::to_string_pretty(&metadata)?;
|
||||||
|
fs::write(&metadata_path, json)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn read_root_metadata(&self) -> Result<RootMetadata> {
|
fn read_root_metadata(&self) -> Result<RootMetadata> {
|
||||||
self.read_root_metadata_internal()
|
self.read_root_metadata_internal()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue