Merge pull request #16 from SteelDynamite/sync-selective-modes-last-sync-has-time
sync-selective-modes-last-sync-has-time
This commit is contained in:
commit
6aa87b6df9
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -56,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-31)
|
### Current state (2026-04-01)
|
||||||
|
|
||||||
- **Phase 1** (Core + CLI): Complete
|
- **Phase 1** (Core + CLI): Complete
|
||||||
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
|
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
|
||||||
- **Phase 3** (GUI MVP): Near complete — core features working, both Tauri and Flutter GUIs maintained
|
- **Phase 3** (GUI MVP): Complete — both Tauri and Flutter GUIs at feature parity
|
||||||
|
|
||||||
### GUI features done
|
### GUI features done
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ 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)
|
- Due date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set
|
||||||
- Move task between lists (kebab menu → "Move to..." submenu)
|
- Move task between lists (kebab menu → "Move to..." submenu)
|
||||||
- List rename (inline input via list kebab menu)
|
- List rename (inline input via list kebab menu)
|
||||||
- Group-by-due-date toggle per list (list kebab menu)
|
- Group-by-due-date toggle per list (list kebab menu)
|
||||||
|
|
@ -81,15 +81,17 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- WebDAV setup flow (settings auto-populates URL/credentials from config + keychain)
|
- WebDAV setup flow (settings auto-populates URL/credentials from config + keychain)
|
||||||
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
|
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
|
||||||
- Setup screen with window dragging + "Open Existing Folder" option
|
- Setup screen with window dragging + "Open Existing Folder" option
|
||||||
|
- Sync status indicators (last-sync time + upload/download counts chip)
|
||||||
|
- Push/pull/full sync mode selection (session-only, in settings)
|
||||||
|
- Desktop packaging (Linux: AppImage + .deb)
|
||||||
|
- Flutter GUI at full parity with Tauri (WebDAV UI, has_time, sync status, sync mode)
|
||||||
|
|
||||||
### GUI features NOT yet done
|
### GUI features NOT yet done
|
||||||
|
|
||||||
- Push-only / pull-only sync modes
|
|
||||||
- Sync status view/indicators
|
|
||||||
- Workspace retarget/migrate
|
- Workspace retarget/migrate
|
||||||
- Subtask hierarchy (data model exists, not used anywhere)
|
- Subtask hierarchy (data model exists, not used anywhere)
|
||||||
- Search/filter tasks
|
- Search/filter tasks
|
||||||
- Desktop packaging (Windows, Linux, macOS)
|
- Desktop packaging for Windows and macOS
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|
|
||||||
9
PLAN.md
9
PLAN.md
|
|
@ -716,17 +716,17 @@ WorkspaceConfig {
|
||||||
- [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
|
||||||
- [x] Move task between lists (kebab menu → "Move to..." submenu in task detail view)
|
- [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)
|
- [x] Optional time on due dates (`has_time: bool` field on Task with `#[serde(default)]` for backward compat; replaces the hours==0 heuristic)
|
||||||
- [x] Due date picker/editor (DateTimePicker component in both new task toast + task detail view)
|
- [x] Due date picker/editor (DateTimePicker component in both new task toast + task detail view)
|
||||||
- [x] WebDAV setup flow with credentials (settings auto-populates URL/username/password from config + keychain on open)
|
- [x] WebDAV setup flow with credentials (settings auto-populates URL/username/password from config + keychain on open)
|
||||||
- [x] List rename (inline input via list kebab menu in drawer)
|
- [x] List rename (inline input via list kebab menu in drawer)
|
||||||
- [x] Keyboard shortcuts (Escape closes settings → detail → drawer → menus in priority order)
|
- [x] Keyboard shortcuts (Escape closes settings → detail → drawer → menus in priority order)
|
||||||
- [ ] Sync status indicators (per workspace)
|
- [x] Sync status indicators (last-sync time + upload/download counts chip in TasksScreen)
|
||||||
- [ ] Push/pull sync mode selection
|
- [x] Push/pull sync mode selection (session-only sync direction selector in SettingsScreen)
|
||||||
- [x] Group-by-due-date toggle per list (checkmark toggle in list kebab menu)
|
- [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)
|
- [x] Desktop packaging (Linux: AppImage + .deb; Windows/macOS not yet verified)
|
||||||
- [x] File watcher (notify crate, 500ms debounce, auto-reloads UI on external file changes)
|
- [x] File watcher (notify crate, 500ms debounce, auto-reloads UI on external file changes)
|
||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
|
|
@ -735,6 +735,7 @@ WorkspaceConfig {
|
||||||
- [ ] Sub-300ms startup time (not yet measured/optimized)
|
- [ ] Sub-300ms startup time (not yet measured/optimized)
|
||||||
- [x] Clean, minimal UI
|
- [x] Clean, minimal UI
|
||||||
- [ ] Feature parity with CLI
|
- [ ] Feature parity with CLI
|
||||||
|
- [x] Flutter GUI at feature parity with Tauri (WebDAV, has_time, sync status, sync mode)
|
||||||
|
|
||||||
### Build & Release
|
### Build & Release
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,53 @@ Future<void> setGroupByDueDate({
|
||||||
Future<bool> getGroupByDueDate({required String listId}) =>
|
Future<bool> getGroupByDueDate({required String listId}) =>
|
||||||
RustLib.instance.api.crateApiGetGroupByDueDate(listId: listId);
|
RustLib.instance.api.crateApiGetGroupByDueDate(listId: listId);
|
||||||
|
|
||||||
|
Future<void> storeCredentials({
|
||||||
|
required String domain,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
}) => RustLib.instance.api.crateApiStoreCredentials(
|
||||||
|
domain: domain,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<String>> loadCredentials({required String domain}) =>
|
||||||
|
RustLib.instance.api.crateApiLoadCredentials(domain: domain);
|
||||||
|
|
||||||
|
Future<void> setWebdavConfig({
|
||||||
|
required String workspaceName,
|
||||||
|
required String webdavUrl,
|
||||||
|
}) => RustLib.instance.api.crateApiSetWebdavConfig(
|
||||||
|
workspaceName: workspaceName,
|
||||||
|
webdavUrl: webdavUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> testWebdavConnection({
|
||||||
|
required String url,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
}) => RustLib.instance.api.crateApiTestWebdavConnection(
|
||||||
|
url: url,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<SyncResultDto> syncWorkspaceCmd({
|
||||||
|
required String workspaceName,
|
||||||
|
required String workspacePath,
|
||||||
|
required String webdavUrl,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String mode,
|
||||||
|
}) => RustLib.instance.api.crateApiSyncWorkspaceCmd(
|
||||||
|
workspaceName: workspaceName,
|
||||||
|
workspacePath: workspacePath,
|
||||||
|
webdavUrl: webdavUrl,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
mode: mode,
|
||||||
|
);
|
||||||
|
|
||||||
Future<Stream<void>> watchWorkspaceChanges({required String path}) =>
|
Future<Stream<void>> watchWorkspaceChanges({required String path}) =>
|
||||||
RustLib.instance.api.crateApiWatchWorkspaceChanges(path: path);
|
RustLib.instance.api.crateApiWatchWorkspaceChanges(path: path);
|
||||||
|
|
||||||
|
|
@ -111,12 +158,52 @@ class AppConfigDto {
|
||||||
currentWorkspace == other.currentWorkspace;
|
currentWorkspace == other.currentWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SyncResultDto {
|
||||||
|
final int uploaded;
|
||||||
|
final int downloaded;
|
||||||
|
final int deletedLocal;
|
||||||
|
final int deletedRemote;
|
||||||
|
final int conflicts;
|
||||||
|
final List<String> errors;
|
||||||
|
|
||||||
|
const SyncResultDto({
|
||||||
|
required this.uploaded,
|
||||||
|
required this.downloaded,
|
||||||
|
required this.deletedLocal,
|
||||||
|
required this.deletedRemote,
|
||||||
|
required this.conflicts,
|
||||||
|
required this.errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
uploaded.hashCode ^
|
||||||
|
downloaded.hashCode ^
|
||||||
|
deletedLocal.hashCode ^
|
||||||
|
deletedRemote.hashCode ^
|
||||||
|
conflicts.hashCode ^
|
||||||
|
errors.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SyncResultDto &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
uploaded == other.uploaded &&
|
||||||
|
downloaded == other.downloaded &&
|
||||||
|
deletedLocal == other.deletedLocal &&
|
||||||
|
deletedRemote == other.deletedRemote &&
|
||||||
|
conflicts == other.conflicts &&
|
||||||
|
errors == other.errors;
|
||||||
|
}
|
||||||
|
|
||||||
class TaskDto {
|
class TaskDto {
|
||||||
final String id;
|
final String id;
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final String status;
|
final String status;
|
||||||
final String? dueDate;
|
final String? dueDate;
|
||||||
|
final bool hasTime;
|
||||||
final String createdAt;
|
final String createdAt;
|
||||||
final String updatedAt;
|
final String updatedAt;
|
||||||
final String? parentId;
|
final String? parentId;
|
||||||
|
|
@ -127,6 +214,7 @@ class TaskDto {
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.dueDate,
|
this.dueDate,
|
||||||
|
required this.hasTime,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.parentId,
|
this.parentId,
|
||||||
|
|
@ -139,6 +227,7 @@ class TaskDto {
|
||||||
description.hashCode ^
|
description.hashCode ^
|
||||||
status.hashCode ^
|
status.hashCode ^
|
||||||
dueDate.hashCode ^
|
dueDate.hashCode ^
|
||||||
|
hasTime.hashCode ^
|
||||||
createdAt.hashCode ^
|
createdAt.hashCode ^
|
||||||
updatedAt.hashCode ^
|
updatedAt.hashCode ^
|
||||||
parentId.hashCode;
|
parentId.hashCode;
|
||||||
|
|
@ -153,6 +242,7 @@ class TaskDto {
|
||||||
description == other.description &&
|
description == other.description &&
|
||||||
status == other.status &&
|
status == other.status &&
|
||||||
dueDate == other.dueDate &&
|
dueDate == other.dueDate &&
|
||||||
|
hasTime == other.hasTime &&
|
||||||
createdAt == other.createdAt &&
|
createdAt == other.createdAt &&
|
||||||
updatedAt == other.updatedAt &&
|
updatedAt == other.updatedAt &&
|
||||||
parentId == other.parentId;
|
parentId == other.parentId;
|
||||||
|
|
|
||||||
|
|
@ -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 => -75020133;
|
int get rustContentHash => -1094746925;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -107,6 +107,8 @@ abstract class RustLibApi extends BaseApi {
|
||||||
|
|
||||||
Future<List<TaskDto>> crateApiListTasks({required String listId});
|
Future<List<TaskDto>> crateApiListTasks({required String listId});
|
||||||
|
|
||||||
|
Future<List<String>> crateApiLoadCredentials({required String domain});
|
||||||
|
|
||||||
Future<void> crateApiMoveTask({
|
Future<void> crateApiMoveTask({
|
||||||
required String fromListId,
|
required String fromListId,
|
||||||
required String toListId,
|
required String toListId,
|
||||||
|
|
@ -133,6 +135,32 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required bool enabled,
|
required bool enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<void> crateApiSetWebdavConfig({
|
||||||
|
required String workspaceName,
|
||||||
|
required String webdavUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> crateApiStoreCredentials({
|
||||||
|
required String domain,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<SyncResultDto> crateApiSyncWorkspaceCmd({
|
||||||
|
required String workspaceName,
|
||||||
|
required String workspacePath,
|
||||||
|
required String webdavUrl,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> crateApiTestWebdavConnection({
|
||||||
|
required String url,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
Future<TaskDto> crateApiToggleTask({
|
Future<TaskDto> crateApiToggleTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
required String taskId,
|
required String taskId,
|
||||||
|
|
@ -482,6 +510,34 @@ 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<List<String>> crateApiLoadCredentials({required String domain}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(domain, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 12,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_list_String,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiLoadCredentialsConstMeta,
|
||||||
|
argValues: [domain],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiLoadCredentialsConstMeta =>
|
||||||
|
const TaskConstMeta(debugName: "load_credentials", argNames: ["domain"]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> crateApiMoveTask({
|
Future<void> crateApiMoveTask({
|
||||||
required String fromListId,
|
required String fromListId,
|
||||||
|
|
@ -498,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 12,
|
funcId: 13,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -528,7 +584,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 13,
|
funcId: 14,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -560,7 +616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 14,
|
funcId: 15,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -596,7 +652,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 15,
|
funcId: 16,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -626,7 +682,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 16,
|
funcId: 17,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -661,7 +717,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 17,
|
funcId: 18,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -681,6 +737,169 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["listId", "enabled"],
|
argNames: ["listId", "enabled"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiSetWebdavConfig({
|
||||||
|
required String workspaceName,
|
||||||
|
required String webdavUrl,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(workspaceName, serializer);
|
||||||
|
sse_encode_String(webdavUrl, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 19,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSetWebdavConfigConstMeta,
|
||||||
|
argValues: [workspaceName, webdavUrl],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSetWebdavConfigConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "set_webdav_config",
|
||||||
|
argNames: ["workspaceName", "webdavUrl"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiStoreCredentials({
|
||||||
|
required String domain,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(domain, serializer);
|
||||||
|
sse_encode_String(username, serializer);
|
||||||
|
sse_encode_String(password, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 20,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiStoreCredentialsConstMeta,
|
||||||
|
argValues: [domain, username, password],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiStoreCredentialsConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "store_credentials",
|
||||||
|
argNames: ["domain", "username", "password"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SyncResultDto> crateApiSyncWorkspaceCmd({
|
||||||
|
required String workspaceName,
|
||||||
|
required String workspacePath,
|
||||||
|
required String webdavUrl,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String mode,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(workspaceName, serializer);
|
||||||
|
sse_encode_String(workspacePath, serializer);
|
||||||
|
sse_encode_String(webdavUrl, serializer);
|
||||||
|
sse_encode_String(username, serializer);
|
||||||
|
sse_encode_String(password, serializer);
|
||||||
|
sse_encode_String(mode, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 21,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_sync_result_dto,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSyncWorkspaceCmdConstMeta,
|
||||||
|
argValues: [
|
||||||
|
workspaceName,
|
||||||
|
workspacePath,
|
||||||
|
webdavUrl,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
mode,
|
||||||
|
],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSyncWorkspaceCmdConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "sync_workspace_cmd",
|
||||||
|
argNames: [
|
||||||
|
"workspaceName",
|
||||||
|
"workspacePath",
|
||||||
|
"webdavUrl",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"mode",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiTestWebdavConnection({
|
||||||
|
required String url,
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(url, serializer);
|
||||||
|
sse_encode_String(username, serializer);
|
||||||
|
sse_encode_String(password, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 22,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_String,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiTestWebdavConnectionConstMeta,
|
||||||
|
argValues: [url, username, password],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiTestWebdavConnectionConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "test_webdav_connection",
|
||||||
|
argNames: ["url", "username", "password"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TaskDto> crateApiToggleTask({
|
Future<TaskDto> crateApiToggleTask({
|
||||||
required String listId,
|
required String listId,
|
||||||
|
|
@ -695,7 +914,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 18,
|
funcId: 23,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -729,7 +948,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 19,
|
funcId: 24,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -763,7 +982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 20,
|
funcId: 25,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -827,6 +1046,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return dco_decode_task_dto(raw);
|
return dco_decode_task_dto(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> dco_decode_list_String(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return (raw as List<dynamic>).map(dco_decode_String).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
|
@ -857,21 +1082,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return raw == null ? null : dco_decode_String(raw);
|
return raw == null ? null : dco_decode_String(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto dco_decode_sync_result_dto(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
final arr = raw as List<dynamic>;
|
||||||
|
if (arr.length != 6)
|
||||||
|
throw Exception('unexpected arr length: expect 6 but see ${arr.length}');
|
||||||
|
return SyncResultDto(
|
||||||
|
uploaded: dco_decode_u_32(arr[0]),
|
||||||
|
downloaded: dco_decode_u_32(arr[1]),
|
||||||
|
deletedLocal: dco_decode_u_32(arr[2]),
|
||||||
|
deletedRemote: dco_decode_u_32(arr[3]),
|
||||||
|
conflicts: dco_decode_u_32(arr[4]),
|
||||||
|
errors: dco_decode_list_String(arr[5]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto dco_decode_task_dto(dynamic raw) {
|
TaskDto dco_decode_task_dto(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 8)
|
if (arr.length != 9)
|
||||||
throw Exception('unexpected arr length: expect 8 but see ${arr.length}');
|
throw Exception('unexpected arr length: expect 9 but see ${arr.length}');
|
||||||
return TaskDto(
|
return TaskDto(
|
||||||
id: dco_decode_String(arr[0]),
|
id: dco_decode_String(arr[0]),
|
||||||
title: dco_decode_String(arr[1]),
|
title: dco_decode_String(arr[1]),
|
||||||
description: dco_decode_String(arr[2]),
|
description: dco_decode_String(arr[2]),
|
||||||
status: dco_decode_String(arr[3]),
|
status: dco_decode_String(arr[3]),
|
||||||
dueDate: dco_decode_opt_String(arr[4]),
|
dueDate: dco_decode_opt_String(arr[4]),
|
||||||
createdAt: dco_decode_String(arr[5]),
|
hasTime: dco_decode_bool(arr[5]),
|
||||||
updatedAt: dco_decode_String(arr[6]),
|
createdAt: dco_decode_String(arr[6]),
|
||||||
parentId: dco_decode_opt_String(arr[7]),
|
updatedAt: dco_decode_String(arr[7]),
|
||||||
|
parentId: dco_decode_opt_String(arr[8]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -967,6 +1209,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return (sse_decode_task_dto(deserializer));
|
return (sse_decode_task_dto(deserializer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> sse_decode_list_String(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
||||||
|
var len_ = sse_decode_i_32(deserializer);
|
||||||
|
var ans_ = <String>[];
|
||||||
|
for (var idx_ = 0; idx_ < len_; ++idx_) {
|
||||||
|
ans_.add(sse_decode_String(deserializer));
|
||||||
|
}
|
||||||
|
return ans_;
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1025,6 +1279,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var var_uploaded = sse_decode_u_32(deserializer);
|
||||||
|
var var_downloaded = sse_decode_u_32(deserializer);
|
||||||
|
var var_deletedLocal = sse_decode_u_32(deserializer);
|
||||||
|
var var_deletedRemote = sse_decode_u_32(deserializer);
|
||||||
|
var var_conflicts = sse_decode_u_32(deserializer);
|
||||||
|
var var_errors = sse_decode_list_String(deserializer);
|
||||||
|
return SyncResultDto(
|
||||||
|
uploaded: var_uploaded,
|
||||||
|
downloaded: var_downloaded,
|
||||||
|
deletedLocal: var_deletedLocal,
|
||||||
|
deletedRemote: var_deletedRemote,
|
||||||
|
conflicts: var_conflicts,
|
||||||
|
errors: var_errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto sse_decode_task_dto(SseDeserializer deserializer) {
|
TaskDto sse_decode_task_dto(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1033,6 +1306,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
var var_description = sse_decode_String(deserializer);
|
var var_description = sse_decode_String(deserializer);
|
||||||
var var_status = sse_decode_String(deserializer);
|
var var_status = sse_decode_String(deserializer);
|
||||||
var var_dueDate = sse_decode_opt_String(deserializer);
|
var var_dueDate = sse_decode_opt_String(deserializer);
|
||||||
|
var var_hasTime = sse_decode_bool(deserializer);
|
||||||
var var_createdAt = sse_decode_String(deserializer);
|
var var_createdAt = sse_decode_String(deserializer);
|
||||||
var var_updatedAt = sse_decode_String(deserializer);
|
var var_updatedAt = sse_decode_String(deserializer);
|
||||||
var var_parentId = sse_decode_opt_String(deserializer);
|
var var_parentId = sse_decode_opt_String(deserializer);
|
||||||
|
|
@ -1042,6 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
description: var_description,
|
description: var_description,
|
||||||
status: var_status,
|
status: var_status,
|
||||||
dueDate: var_dueDate,
|
dueDate: var_dueDate,
|
||||||
|
hasTime: var_hasTime,
|
||||||
createdAt: var_createdAt,
|
createdAt: var_createdAt,
|
||||||
updatedAt: var_updatedAt,
|
updatedAt: var_updatedAt,
|
||||||
parentId: var_parentId,
|
parentId: var_parentId,
|
||||||
|
|
@ -1154,6 +1429,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
sse_encode_task_dto(self, serializer);
|
sse_encode_task_dto(self, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_String(List<String> self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_i_32(self.length, serializer);
|
||||||
|
for (final item in self) {
|
||||||
|
sse_encode_String(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_prim_u_8_strict(
|
void sse_encode_list_prim_u_8_strict(
|
||||||
Uint8List self,
|
Uint8List self,
|
||||||
|
|
@ -1207,6 +1491,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sync_result_dto(
|
||||||
|
SyncResultDto self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_u_32(self.uploaded, serializer);
|
||||||
|
sse_encode_u_32(self.downloaded, serializer);
|
||||||
|
sse_encode_u_32(self.deletedLocal, serializer);
|
||||||
|
sse_encode_u_32(self.deletedRemote, serializer);
|
||||||
|
sse_encode_u_32(self.conflicts, serializer);
|
||||||
|
sse_encode_list_String(self.errors, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_task_dto(TaskDto self, SseSerializer serializer) {
|
void sse_encode_task_dto(TaskDto self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -1215,6 +1513,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
sse_encode_String(self.description, serializer);
|
sse_encode_String(self.description, serializer);
|
||||||
sse_encode_String(self.status, serializer);
|
sse_encode_String(self.status, serializer);
|
||||||
sse_encode_opt_String(self.dueDate, serializer);
|
sse_encode_opt_String(self.dueDate, serializer);
|
||||||
|
sse_encode_bool(self.hasTime, serializer);
|
||||||
sse_encode_String(self.createdAt, serializer);
|
sse_encode_String(self.createdAt, serializer);
|
||||||
sse_encode_String(self.updatedAt, serializer);
|
sse_encode_String(self.updatedAt, serializer);
|
||||||
sse_encode_opt_String(self.parentId, serializer);
|
sse_encode_opt_String(self.parentId, serializer);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> dco_decode_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -51,6 +54,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto dco_decode_sync_result_dto(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto dco_decode_task_dto(dynamic raw);
|
TaskDto dco_decode_task_dto(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -89,6 +95,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -106,6 +115,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
|
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -151,6 +163,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_prim_u_8_strict(
|
void sse_encode_list_prim_u_8_strict(
|
||||||
Uint8List self,
|
Uint8List self,
|
||||||
|
|
@ -175,6 +190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
|
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> dco_decode_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -53,6 +56,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto dco_decode_sync_result_dto(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto dco_decode_task_dto(dynamic raw);
|
TaskDto dco_decode_task_dto(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -91,6 +97,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -108,6 +117,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
|
TaskDto sse_decode_task_dto(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -153,6 +165,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_prim_u_8_strict(
|
void sse_encode_list_prim_u_8_strict(
|
||||||
Uint8List self,
|
Uint8List self,
|
||||||
|
|
@ -177,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
|
void sse_encode_task_dto(TaskDto self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,131 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import '../rust/api.dart' as api;
|
||||||
import '../state/app_state.dart';
|
import '../state/app_state.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
|
||||||
class SettingsScreen extends StatelessWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
|
final _urlController = TextEditingController();
|
||||||
|
final _userController = TextEditingController();
|
||||||
|
final _passController = TextEditingController();
|
||||||
|
String _testStatus = 'idle'; // idle | testing | ok | fail
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_urlController.dispose();
|
||||||
|
_userController.dispose();
|
||||||
|
_passController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCredentials() async {
|
||||||
|
final state = context.read<AppState>();
|
||||||
|
final wsName = state.config?.currentWorkspace;
|
||||||
|
if (wsName == null) return;
|
||||||
|
final ws = state.config!.workspaces.cast<api.WorkspaceEntry?>().firstWhere(
|
||||||
|
(w) => w?.name == wsName, orElse: () => null);
|
||||||
|
if (ws?.webdavUrl != null) {
|
||||||
|
_urlController.text = ws!.webdavUrl!;
|
||||||
|
try {
|
||||||
|
final domain = Uri.parse(ws.webdavUrl!).host;
|
||||||
|
final creds = await api.loadCredentials(domain: domain);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_userController.text = creds[0];
|
||||||
|
_passController.text = creds[1];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _testConnection() async {
|
||||||
|
setState(() => _testStatus = 'testing');
|
||||||
|
try {
|
||||||
|
await api.testWebdavConnection(
|
||||||
|
url: _urlController.text,
|
||||||
|
username: _userController.text,
|
||||||
|
password: _passController.text,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _testStatus = 'ok');
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _testStatus = 'fail');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
final state = context.read<AppState>();
|
||||||
|
final wsName = state.config?.currentWorkspace;
|
||||||
|
if (wsName == null || _urlController.text.trim().isEmpty) return;
|
||||||
|
await api.setWebdavConfig(
|
||||||
|
workspaceName: wsName,
|
||||||
|
webdavUrl: _urlController.text.trim(),
|
||||||
|
);
|
||||||
|
if (_userController.text.isNotEmpty && _passController.text.isNotEmpty) {
|
||||||
|
final domain = Uri.parse(_urlController.text.trim()).host;
|
||||||
|
await api.storeCredentials(
|
||||||
|
domain: domain,
|
||||||
|
username: _userController.text,
|
||||||
|
password: _passController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await state.loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = context.watch<AppState>();
|
final state = context.watch<AppState>();
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final borderColor = isDark ? AppTheme.borderDark : AppTheme.borderLight;
|
||||||
|
final inputDecoration = InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(color: borderColor),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: BorderSide(color: borderColor),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: const BorderSide(color: AppTheme.primary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final wsName = state.config?.currentWorkspace;
|
||||||
|
final ws = wsName == null ? null : state.config!.workspaces.cast<api.WorkspaceEntry?>()
|
||||||
|
.firstWhere((w) => w?.name == wsName, orElse: () => null);
|
||||||
|
final lastSync = ws?.lastSync;
|
||||||
|
String? relTime;
|
||||||
|
if (lastSync != null) {
|
||||||
|
final t = DateTime.tryParse(lastSync);
|
||||||
|
if (t != null) {
|
||||||
|
final secsAgo = DateTime.now().difference(t).inSeconds;
|
||||||
|
if (secsAgo < 60) {
|
||||||
|
relTime = 'just now';
|
||||||
|
} else if (secsAgo < 3600) {
|
||||||
|
relTime = '${secsAgo ~/ 60}m ago';
|
||||||
|
} else {
|
||||||
|
relTime = '${secsAgo ~/ 3600}h ago';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => state.setScreen('tasks'),
|
onTap: () => state.setScreen('tasks'),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -20,10 +136,6 @@ class SettingsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
child: AnimatedScale(
|
|
||||||
scale: 1.0,
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
|
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
|
||||||
|
|
@ -37,11 +149,11 @@ class SettingsScreen extends StatelessWidget {
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header (matching Tauri: text-lg font-bold, border-b, px-4 py-3)
|
// Header
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
|
border: Border(bottom: BorderSide(color: borderColor, width: 0.5)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -51,9 +163,7 @@ class SettingsScreen extends StatelessWidget {
|
||||||
onTap: () => state.setScreen('tasks'),
|
onTap: () => state.setScreen('tasks'),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Icon(Icons.close, size: 20,
|
child: Icon(Icons.close, size: 20,
|
||||||
color: isDark ? AppTheme.textDark : AppTheme.textLight),
|
color: isDark ? AppTheme.textDark : AppTheme.textLight),
|
||||||
),
|
),
|
||||||
|
|
@ -61,60 +171,169 @@ class SettingsScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Scrollable content
|
// Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// WebDAV Sync section (matching Tauri order: sync first)
|
// WebDAV section header
|
||||||
Text('WEBDAV SYNC',
|
Text('WEBDAV SYNC',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600,
|
||||||
fontSize: 14, fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5),
|
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))),
|
||||||
)),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
// Credentials card
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
|
border: Border.all(color: borderColor),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Server URL', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
TextField(controller: _urlController, style: const TextStyle(fontSize: 13),
|
||||||
|
decoration: inputDecoration.copyWith(hintText: 'https://dav.example.com/tasks/')),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text('Username', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
TextField(controller: _userController, style: const TextStyle(fontSize: 13),
|
||||||
|
decoration: inputDecoration),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text('Password', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500,
|
||||||
|
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
TextField(controller: _passController, obscureText: true,
|
||||||
|
style: const TextStyle(fontSize: 13), decoration: inputDecoration),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: _urlController.text.isEmpty ? null : _testConnection,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: BorderSide(color: borderColor),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'WebDAV sync not yet available in Flutter build',
|
_testStatus == 'testing' ? 'Testing…'
|
||||||
style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight),
|
: _testStatus == 'ok' ? 'Connected'
|
||||||
|
: _testStatus == 'fail' ? 'Failed — Retry'
|
||||||
|
: 'Test Connection',
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _urlController.text.isEmpty ? null : _save,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text('Save', style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Sync direction + Sync Now
|
||||||
|
if (wsName != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: borderColor),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
|
||||||
|
),
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: state.syncMode,
|
||||||
|
isExpanded: true,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
style: TextStyle(fontSize: 13,
|
||||||
|
color: isDark ? AppTheme.textDark : AppTheme.textLight),
|
||||||
|
dropdownColor: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'full', child: Text('Sync both ways')),
|
||||||
|
DropdownMenuItem(value: 'push', child: Text('Push only')),
|
||||||
|
DropdownMenuItem(value: 'pull', child: Text('Pull only')),
|
||||||
|
],
|
||||||
|
onChanged: (v) { if (v != null) state.setSyncMode(v); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: state.syncing ? null : () => state.triggerSync(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
disabledBackgroundColor: AppTheme.primary.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
child: Text(state.syncing ? 'Syncing…' : 'Sync Now',
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (relTime != null) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('Last sync: $relTime',
|
||||||
|
style: TextStyle(fontSize: 11,
|
||||||
|
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
|
||||||
|
if (state.lastSyncResult != null) ...[
|
||||||
|
Text(' · ↑${state.lastSyncResult!.uploaded} ↓${state.lastSyncResult!.downloaded}',
|
||||||
|
style: TextStyle(fontSize: 11,
|
||||||
|
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.4))),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Appearance section
|
// Appearance
|
||||||
Text('APPEARANCE',
|
Text('APPEARANCE',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600,
|
||||||
fontSize: 14, fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5),
|
color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))),
|
||||||
)),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Dark mode toggle in bordered card (matching Tauri)
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => state.toggleDarkMode(),
|
onTap: () => state.toggleDarkMode(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight),
|
border: Border.all(color: borderColor),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Toggle switch (matching Tauri: h-6 w-11)
|
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
width: 44,
|
width: 44, height: 24,
|
||||||
height: 24,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)),
|
color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)),
|
||||||
|
|
@ -123,8 +342,7 @@ class SettingsScreen extends StatelessWidget {
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft,
|
alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 20,
|
width: 20, height: 20,
|
||||||
height: 20,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
|
decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
|
||||||
),
|
),
|
||||||
|
|
@ -136,7 +354,8 @@ class SettingsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Center(
|
Center(
|
||||||
child: Text('Flutter + Rust', style: TextStyle(fontSize: 12,
|
child: Text('Flutter + Rust',
|
||||||
|
style: TextStyle(fontSize: 12,
|
||||||
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3))),
|
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -148,7 +367,6 @@ class SettingsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,13 @@ class _TasksScreenState extends State<TasksScreen> with SingleTickerProviderStat
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleCreateTask(String title, String desc, {String? dueDate}) async {
|
Future<void> _handleCreateTask(String title, String desc, {String? dueDate, bool hasTime = false}) async {
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
final task = await state.createTask(title, desc);
|
final task = await state.createTask(title, desc);
|
||||||
if (task != null && dueDate != null) {
|
if (task != null && dueDate != null) {
|
||||||
await state.updateTask(api.TaskDto(
|
await state.updateTask(api.TaskDto(
|
||||||
id: task.id, title: task.title, description: task.description,
|
id: task.id, title: task.title, description: task.description,
|
||||||
status: task.status, dueDate: dueDate,
|
status: task.status, dueDate: dueDate, hasTime: hasTime,
|
||||||
createdAt: task.createdAt, updatedAt: task.updatedAt, parentId: task.parentId,
|
createdAt: task.createdAt, updatedAt: task.updatedAt, parentId: task.parentId,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +257,32 @@ class _TasksScreenState extends State<TasksScreen> with SingleTickerProviderStat
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Sync status indicator
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: state.syncing
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20, height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: AppTheme.primary))
|
||||||
|
: state.lastSyncResult != null
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'↑${state.lastSyncResult!.uploaded} ↓${state.lastSyncResult!.downloaded}',
|
||||||
|
style: TextStyle(fontSize: 11,
|
||||||
|
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ class AppState extends ChangeNotifier {
|
||||||
bool darkMode = true;
|
bool darkMode = true;
|
||||||
StreamSubscription? _watcherSub;
|
StreamSubscription? _watcherSub;
|
||||||
bool syncing = false;
|
bool syncing = false;
|
||||||
|
String syncMode = 'full';
|
||||||
|
api.SyncResultDto? lastSyncResult;
|
||||||
String? error;
|
String? error;
|
||||||
|
|
||||||
// Selected task for detail view
|
// Selected task for detail view
|
||||||
|
|
@ -264,6 +266,45 @@ class AppState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setSyncMode(String mode) {
|
||||||
|
syncMode = mode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> triggerSync() async {
|
||||||
|
if (config?.currentWorkspace == null) return;
|
||||||
|
final wsName = config!.currentWorkspace!;
|
||||||
|
final ws = config!.workspaces.firstWhere((w) => w.name == wsName);
|
||||||
|
if (ws.webdavUrl == null) {
|
||||||
|
error = 'No WebDAV URL configured';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncing = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
final domain = Uri.parse(ws.webdavUrl!).host;
|
||||||
|
final creds = await api.loadCredentials(domain: domain);
|
||||||
|
final result = await api.syncWorkspaceCmd(
|
||||||
|
workspaceName: wsName,
|
||||||
|
workspacePath: ws.path,
|
||||||
|
webdavUrl: ws.webdavUrl!,
|
||||||
|
username: creds[0],
|
||||||
|
password: creds[1],
|
||||||
|
mode: syncMode,
|
||||||
|
);
|
||||||
|
lastSyncResult = result;
|
||||||
|
if (result.errors.isNotEmpty) error = result.errors.join('; ');
|
||||||
|
config = await api.getConfig();
|
||||||
|
await loadLists();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
}
|
||||||
|
syncing = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void toggleDarkMode() {
|
void toggleDarkMode() {
|
||||||
darkMode = !darkMode;
|
darkMode = !darkMode;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import '../theme.dart';
|
||||||
|
|
||||||
class DateTimePicker extends StatefulWidget {
|
class DateTimePicker extends StatefulWidget {
|
||||||
final DateTime? initialDate;
|
final DateTime? initialDate;
|
||||||
final void Function(DateTime date) onDone;
|
final bool initialHasTime;
|
||||||
|
final void Function(DateTime date, bool hasTime) onDone;
|
||||||
final VoidCallback onClear;
|
final VoidCallback onClear;
|
||||||
|
|
||||||
const DateTimePicker({super.key, this.initialDate, required this.onDone, required this.onClear});
|
const DateTimePicker({super.key, this.initialDate, this.initialHasTime = false, required this.onDone, required this.onClear});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DateTimePicker> createState() => _DateTimePickerState();
|
State<DateTimePicker> createState() => _DateTimePickerState();
|
||||||
|
|
@ -27,7 +28,7 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
||||||
if (widget.initialDate != null) {
|
if (widget.initialDate != null) {
|
||||||
_hour = widget.initialDate!.hour;
|
_hour = widget.initialDate!.hour;
|
||||||
_minute = widget.initialDate!.minute;
|
_minute = widget.initialDate!.minute;
|
||||||
_showTime = _hour != 0 || _minute != 0;
|
_showTime = widget.initialHasTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
||||||
final result = _showTime
|
final result = _showTime
|
||||||
? DateTime(_selected!.year, _selected!.month, _selected!.day, _hour, _minute)
|
? DateTime(_selected!.year, _selected!.month, _selected!.day, _hour, _minute)
|
||||||
: DateTime(_selected!.year, _selected!.month, _selected!.day);
|
: DateTime(_selected!.year, _selected!.month, _selected!.day);
|
||||||
widget.onDone(result);
|
widget.onDone(result, _showTime);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,33 +138,17 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Time toggle
|
// Time section
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(top: 12),
|
padding: const EdgeInsets.only(top: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
|
border: Border(top: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: _showTime
|
||||||
children: [
|
? Row(
|
||||||
GestureDetector(
|
|
||||||
onTap: () => setState(() => _showTime = !_showTime),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.access_time, size: 16, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(_showTime ? 'Time' : 'Set time',
|
|
||||||
style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
|
||||||
const Spacer(),
|
|
||||||
Icon(_showTime ? Icons.expand_less : Icons.expand_more, size: 18,
|
|
||||||
color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_showTime) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
|
Text('Time', style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
// Hour
|
// Hour
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
|
@ -180,7 +165,7 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
||||||
onChanged: (v) => setState(() => _hour = v!),
|
onChanged: (v) => setState(() => _hour = v!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 8), child: Text(':', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 6), child: Text(':', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600))),
|
||||||
// Minute
|
// Minute
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
|
@ -197,10 +182,16 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
||||||
onChanged: (v) => setState(() => _minute = v!),
|
onChanged: (v) => setState(() => _minute = v!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _showTime = false),
|
||||||
|
child: Icon(Icons.close, size: 18, color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withAlpha(160)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
)
|
||||||
|
: GestureDetector(
|
||||||
|
onTap: () => setState(() { _showTime = true; if (_selected == null) _selected = DateTime.now(); }),
|
||||||
|
child: Text('Set time', style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Clear button
|
// Clear button
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import '../theme.dart';
|
||||||
import 'date_time_picker.dart';
|
import 'date_time_picker.dart';
|
||||||
|
|
||||||
class NewTaskInput extends StatefulWidget {
|
class NewTaskInput extends StatefulWidget {
|
||||||
final Future<void> Function(String title, String description, {String? dueDate}) onCreate;
|
final Future<void> Function(String title, String description, {String? dueDate, bool hasTime}) onCreate;
|
||||||
|
|
||||||
const NewTaskInput({super.key, required this.onCreate});
|
const NewTaskInput({super.key, required this.onCreate});
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
||||||
final _descController = TextEditingController();
|
final _descController = TextEditingController();
|
||||||
final _titleFocus = FocusNode();
|
final _titleFocus = FocusNode();
|
||||||
DateTime? _selectedDate;
|
DateTime? _selectedDate;
|
||||||
|
bool _selectedHasTime = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -34,7 +35,7 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final title = _titleController.text.trim();
|
final title = _titleController.text.trim();
|
||||||
if (title.isEmpty) return;
|
if (title.isEmpty) return;
|
||||||
await widget.onCreate(title, _descController.text.trim(), dueDate: _selectedDate?.toUtc().toIso8601String());
|
await widget.onCreate(title, _descController.text.trim(), dueDate: _selectedDate?.toUtc().toIso8601String(), hasTime: _selectedHasTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pickDate() {
|
void _pickDate() {
|
||||||
|
|
@ -47,8 +48,9 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
||||||
),
|
),
|
||||||
builder: (_) => DateTimePicker(
|
builder: (_) => DateTimePicker(
|
||||||
initialDate: _selectedDate,
|
initialDate: _selectedDate,
|
||||||
onDone: (date) => setState(() => _selectedDate = date),
|
initialHasTime: _selectedHasTime,
|
||||||
onClear: () => setState(() => _selectedDate = null),
|
onDone: (date, hasTime) => setState(() { _selectedDate = date; _selectedHasTime = hasTime; }),
|
||||||
|
onClear: () => setState(() { _selectedDate = null; _selectedHasTime = false; }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -60,8 +62,7 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
||||||
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
final day = dayNames[d.weekday % 7];
|
final day = dayNames[d.weekday % 7];
|
||||||
final pad = (int n) => n.toString().padLeft(2, '0');
|
final pad = (int n) => n.toString().padLeft(2, '0');
|
||||||
final hasTime = d.hour != 0 || d.minute != 0;
|
final timePart = _selectedHasTime ? ', ${pad(d.hour)}:${pad(d.minute)}' : '';
|
||||||
final timePart = hasTime ? ', ${pad(d.hour)}:${pad(d.minute)}' : '';
|
|
||||||
if (taskDate == today) return 'Today$timePart';
|
if (taskDate == today) return 'Today$timePart';
|
||||||
return '$day, ${pad(d.day)}/${pad(d.month)}$timePart';
|
return '$day, ${pad(d.day)}/${pad(d.month)}$timePart';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleUpdate({String? dueDate}) {
|
void _scheduleUpdate({String? dueDate, bool? hasTime}) {
|
||||||
_debounce?.cancel();
|
_debounce?.cancel();
|
||||||
_debounce = Timer(const Duration(milliseconds: 400), () {
|
_debounce = Timer(const Duration(milliseconds: 400), () {
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
|
|
@ -65,6 +65,7 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
description: _descController.text,
|
description: _descController.text,
|
||||||
status: widget.task.status,
|
status: widget.task.status,
|
||||||
dueDate: dueDate ?? widget.task.dueDate,
|
dueDate: dueDate ?? widget.task.dueDate,
|
||||||
|
hasTime: hasTime ?? widget.task.hasTime,
|
||||||
createdAt: widget.task.createdAt,
|
createdAt: widget.task.createdAt,
|
||||||
updatedAt: widget.task.updatedAt,
|
updatedAt: widget.task.updatedAt,
|
||||||
parentId: widget.task.parentId,
|
parentId: widget.task.parentId,
|
||||||
|
|
@ -72,7 +73,7 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateDueDate(String? dueDate) {
|
void _updateDueDate(String? dueDate, {bool hasTime = false}) {
|
||||||
final state = context.read<AppState>();
|
final state = context.read<AppState>();
|
||||||
state.updateTask(api.TaskDto(
|
state.updateTask(api.TaskDto(
|
||||||
id: widget.task.id,
|
id: widget.task.id,
|
||||||
|
|
@ -80,6 +81,7 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
description: _descController.text,
|
description: _descController.text,
|
||||||
status: widget.task.status,
|
status: widget.task.status,
|
||||||
dueDate: dueDate,
|
dueDate: dueDate,
|
||||||
|
hasTime: hasTime,
|
||||||
createdAt: widget.task.createdAt,
|
createdAt: widget.task.createdAt,
|
||||||
updatedAt: widget.task.updatedAt,
|
updatedAt: widget.task.updatedAt,
|
||||||
parentId: widget.task.parentId,
|
parentId: widget.task.parentId,
|
||||||
|
|
@ -96,7 +98,8 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
),
|
),
|
||||||
builder: (_) => DateTimePicker(
|
builder: (_) => DateTimePicker(
|
||||||
initialDate: widget.task.dueDate != null ? DateTime.tryParse(widget.task.dueDate!) : null,
|
initialDate: widget.task.dueDate != null ? DateTime.tryParse(widget.task.dueDate!) : null,
|
||||||
onDone: (date) => _updateDueDate(date.toUtc().toIso8601String()),
|
initialHasTime: widget.task.hasTime,
|
||||||
|
onDone: (date, hasTime) => _updateDueDate(date.toUtc().toIso8601String(), hasTime: hasTime),
|
||||||
onClear: () => _updateDueDate(null),
|
onClear: () => _updateDueDate(null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -112,8 +115,7 @@ class _TaskDetailViewState extends State<TaskDetailView> with SingleTickerProvid
|
||||||
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
final day = dayNames[local.weekday % 7];
|
final day = dayNames[local.weekday % 7];
|
||||||
final pad = (int n) => n.toString().padLeft(2, '0');
|
final pad = (int n) => n.toString().padLeft(2, '0');
|
||||||
final hasTime = local.hour != 0 || local.minute != 0;
|
final timePart = widget.task.hasTime ? ', ${pad(local.hour)}:${pad(local.minute)}' : '';
|
||||||
final timePart = hasTime ? ', ${pad(local.hour)}:${pad(local.minute)}' : '';
|
|
||||||
if (taskDate == today) return 'Today$timePart';
|
if (taskDate == today) return 'Today$timePart';
|
||||||
return '$day, ${pad(local.day)}/${pad(local.month)}$timePart';
|
return '$day, ${pad(local.day)}/${pad(local.month)}$timePart';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
apps/flutter/rust/Cargo.lock
generated
1
apps/flutter/rust/Cargo.lock
generated
|
|
@ -1194,6 +1194,7 @@ dependencies = [
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"onyx-core",
|
"onyx-core",
|
||||||
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
notify = "7"
|
notify = "7"
|
||||||
notify-debouncer-mini = "0.5"
|
notify-debouncer-mini = "0.5"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ use onyx_core::{
|
||||||
config::{AppConfig, WorkspaceConfig},
|
config::{AppConfig, WorkspaceConfig},
|
||||||
models::{Task, TaskStatus},
|
models::{Task, TaskStatus},
|
||||||
repository::TaskRepository,
|
repository::TaskRepository,
|
||||||
|
sync::{self, SyncMode},
|
||||||
|
webdav,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── State ───────────────────────────────────────────────────────────
|
// ── State ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -44,11 +46,21 @@ pub struct TaskDto {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub due_date: Option<String>,
|
pub due_date: Option<String>,
|
||||||
|
pub has_time: bool,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SyncResultDto {
|
||||||
|
pub uploaded: u32,
|
||||||
|
pub downloaded: u32,
|
||||||
|
pub deleted_local: u32,
|
||||||
|
pub deleted_remote: u32,
|
||||||
|
pub conflicts: u32,
|
||||||
|
pub errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TaskListDto {
|
pub struct TaskListDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -79,6 +91,7 @@ fn task_to_dto(t: &Task) -> TaskDto {
|
||||||
TaskStatus::Completed => "completed".into(),
|
TaskStatus::Completed => "completed".into(),
|
||||||
},
|
},
|
||||||
due_date: t.due_date.map(|d| d.to_rfc3339()),
|
due_date: t.due_date.map(|d| d.to_rfc3339()),
|
||||||
|
has_time: t.has_time,
|
||||||
created_at: t.created_at.to_rfc3339(),
|
created_at: t.created_at.to_rfc3339(),
|
||||||
updated_at: t.updated_at.to_rfc3339(),
|
updated_at: t.updated_at.to_rfc3339(),
|
||||||
parent_id: t.parent_id.map(|id| id.to_string()),
|
parent_id: t.parent_id.map(|id| id.to_string()),
|
||||||
|
|
@ -218,6 +231,7 @@ pub fn update_task(list_id: String, task: TaskDto) -> Result<(), String> {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok())
|
.and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok())
|
||||||
.map(|d| d.with_timezone(&chrono::Utc));
|
.map(|d| d.with_timezone(&chrono::Utc));
|
||||||
|
existing.has_time = task.has_time;
|
||||||
|
|
||||||
s.repo.as_mut().unwrap().update_task(lid, existing).map_err(|e| e.to_string())
|
s.repo.as_mut().unwrap().update_task(lid, existing).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +309,74 @@ pub fn get_group_by_due_date(list_id: String) -> Result<bool, String> {
|
||||||
s.repo.as_ref().unwrap().get_group_by_due_date(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 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn store_credentials(domain: String, username: String, password: String) -> Result<(), String> {
|
||||||
|
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_credentials(domain: String) -> Result<Vec<String>, String> {
|
||||||
|
let (u, p) = webdav::load_credentials(&domain).map_err(|e| e.to_string())?;
|
||||||
|
Ok(vec![u, p])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_webdav_config(workspace_name: String, webdav_url: String) -> Result<(), String> {
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
||||||
|
ws.webdav_url = Some(webdav_url);
|
||||||
|
}
|
||||||
|
let config_path = AppConfig::get_config_path();
|
||||||
|
s.config.save_to_file(&config_path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_webdav_connection(url: String, username: String, password: String) -> Result<(), String> {
|
||||||
|
let client = webdav::WebDavClient::new(&url, &username, &password);
|
||||||
|
client.test_connection().await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_workspace_cmd(
|
||||||
|
workspace_name: String,
|
||||||
|
workspace_path: String,
|
||||||
|
webdav_url: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
mode: String,
|
||||||
|
) -> Result<SyncResultDto, String> {
|
||||||
|
let sync_mode = match mode.as_str() {
|
||||||
|
"push" => SyncMode::Push,
|
||||||
|
"pull" => SyncMode::Pull,
|
||||||
|
_ => SyncMode::Full,
|
||||||
|
};
|
||||||
|
let result = sync::sync_workspace(
|
||||||
|
&PathBuf::from(&workspace_path),
|
||||||
|
&webdav_url,
|
||||||
|
&username,
|
||||||
|
&password,
|
||||||
|
sync_mode,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut s = STATE.lock().unwrap();
|
||||||
|
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
||||||
|
ws.last_sync = Some(chrono::Utc::now());
|
||||||
|
}
|
||||||
|
let config_path = AppConfig::get_config_path();
|
||||||
|
s.config.save_to_file(&config_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SyncResultDto {
|
||||||
|
uploaded: result.uploaded,
|
||||||
|
downloaded: result.downloaded,
|
||||||
|
deleted_local: result.deleted_local,
|
||||||
|
deleted_remote: result.deleted_remote,
|
||||||
|
conflicts: result.conflicts,
|
||||||
|
errors: result.errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ── File watcher ───────────────────────────────────────────────────
|
// ── File watcher ───────────────────────────────────────────────────
|
||||||
|
|
||||||
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
||||||
|
|
|
||||||
|
|
@ -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 = -75020133;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1094746925;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -411,6 +411,39 @@ fn wire__crate__api__list_tasks_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__load_credentials_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: "load_credentials",
|
||||||
|
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_domain = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok = crate::api::load_credentials(api_domain)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__api__move_task_impl(
|
fn wire__crate__api__move_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,
|
||||||
|
|
@ -617,6 +650,166 @@ fn wire__crate__api__set_group_by_due_date_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__api__set_webdav_config_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_webdav_config",
|
||||||
|
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_workspace_name = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_webdav_url = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok =
|
||||||
|
crate::api::set_webdav_config(api_workspace_name, api_webdav_url)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__store_credentials_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: "store_credentials",
|
||||||
|
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_domain = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_username = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_password = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, String>((move || {
|
||||||
|
let output_ok =
|
||||||
|
crate::api::store_credentials(api_domain, api_username, api_password)?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__sync_workspace_cmd_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_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "sync_workspace_cmd",
|
||||||
|
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_workspace_name = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_workspace_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_webdav_url = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_username = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_password = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_mode = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, String>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok = crate::api::sync_workspace_cmd(
|
||||||
|
api_workspace_name,
|
||||||
|
api_workspace_path,
|
||||||
|
api_webdav_url,
|
||||||
|
api_username,
|
||||||
|
api_password,
|
||||||
|
api_mode,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__test_webdav_connection_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_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "test_webdav_connection",
|
||||||
|
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_url = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_username = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_password = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, String>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::api::test_webdav_connection(api_url, api_username, api_password)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
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,
|
||||||
|
|
@ -770,6 +963,18 @@ impl SseDecode for bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Vec<String> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut len_ = <i32>::sse_decode(deserializer);
|
||||||
|
let mut ans_ = vec![];
|
||||||
|
for idx_ in 0..len_ {
|
||||||
|
ans_.push(<String>::sse_decode(deserializer));
|
||||||
|
}
|
||||||
|
return ans_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for Vec<u8> {
|
impl SseDecode for Vec<u8> {
|
||||||
// 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 {
|
||||||
|
|
@ -829,6 +1034,26 @@ impl SseDecode for Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for crate::api::SyncResultDto {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut var_uploaded = <u32>::sse_decode(deserializer);
|
||||||
|
let mut var_downloaded = <u32>::sse_decode(deserializer);
|
||||||
|
let mut var_deletedLocal = <u32>::sse_decode(deserializer);
|
||||||
|
let mut var_deletedRemote = <u32>::sse_decode(deserializer);
|
||||||
|
let mut var_conflicts = <u32>::sse_decode(deserializer);
|
||||||
|
let mut var_errors = <Vec<String>>::sse_decode(deserializer);
|
||||||
|
return crate::api::SyncResultDto {
|
||||||
|
uploaded: var_uploaded,
|
||||||
|
downloaded: var_downloaded,
|
||||||
|
deleted_local: var_deletedLocal,
|
||||||
|
deleted_remote: var_deletedRemote,
|
||||||
|
conflicts: var_conflicts,
|
||||||
|
errors: var_errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::api::TaskDto {
|
impl SseDecode for crate::api::TaskDto {
|
||||||
// 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 {
|
||||||
|
|
@ -837,6 +1062,7 @@ impl SseDecode for crate::api::TaskDto {
|
||||||
let mut var_description = <String>::sse_decode(deserializer);
|
let mut var_description = <String>::sse_decode(deserializer);
|
||||||
let mut var_status = <String>::sse_decode(deserializer);
|
let mut var_status = <String>::sse_decode(deserializer);
|
||||||
let mut var_dueDate = <Option<String>>::sse_decode(deserializer);
|
let mut var_dueDate = <Option<String>>::sse_decode(deserializer);
|
||||||
|
let mut var_hasTime = <bool>::sse_decode(deserializer);
|
||||||
let mut var_createdAt = <String>::sse_decode(deserializer);
|
let mut var_createdAt = <String>::sse_decode(deserializer);
|
||||||
let mut var_updatedAt = <String>::sse_decode(deserializer);
|
let mut var_updatedAt = <String>::sse_decode(deserializer);
|
||||||
let mut var_parentId = <Option<String>>::sse_decode(deserializer);
|
let mut var_parentId = <Option<String>>::sse_decode(deserializer);
|
||||||
|
|
@ -846,6 +1072,7 @@ impl SseDecode for crate::api::TaskDto {
|
||||||
description: var_description,
|
description: var_description,
|
||||||
status: var_status,
|
status: var_status,
|
||||||
due_date: var_dueDate,
|
due_date: var_dueDate,
|
||||||
|
has_time: var_hasTime,
|
||||||
created_at: var_createdAt,
|
created_at: var_createdAt,
|
||||||
updated_at: var_updatedAt,
|
updated_at: var_updatedAt,
|
||||||
parent_id: var_parentId,
|
parent_id: var_parentId,
|
||||||
|
|
@ -933,15 +1160,20 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
9 => wire__crate__api__greet_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__init_workspace_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__list_tasks_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__move_task_impl(port, ptr, rust_vec_len, data_len),
|
12 => wire__crate__api__load_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||||
13 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len),
|
13 => wire__crate__api__move_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
14 => wire__crate__api__rename_list_impl(port, ptr, rust_vec_len, data_len),
|
14 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len),
|
||||||
15 => wire__crate__api__reorder_task_impl(port, ptr, rust_vec_len, data_len),
|
15 => wire__crate__api__rename_list_impl(port, ptr, rust_vec_len, data_len),
|
||||||
16 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len),
|
16 => wire__crate__api__reorder_task_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),
|
17 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len),
|
||||||
18 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len),
|
18 => wire__crate__api__set_group_by_due_date_impl(port, ptr, rust_vec_len, data_len),
|
||||||
19 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len),
|
19 => wire__crate__api__set_webdav_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
20 => wire__crate__api__watch_workspace_changes_impl(port, ptr, rust_vec_len, data_len),
|
20 => wire__crate__api__store_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
21 => wire__crate__api__sync_workspace_cmd_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
22 => wire__crate__api__test_webdav_connection_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
23 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
24 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
25 => wire__crate__api__watch_workspace_changes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -977,6 +1209,26 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::AppConfigDto> for crate::api:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for crate::api::SyncResultDto {
|
||||||
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
|
[
|
||||||
|
self.uploaded.into_into_dart().into_dart(),
|
||||||
|
self.downloaded.into_into_dart().into_dart(),
|
||||||
|
self.deleted_local.into_into_dart().into_dart(),
|
||||||
|
self.deleted_remote.into_into_dart().into_dart(),
|
||||||
|
self.conflicts.into_into_dart().into_dart(),
|
||||||
|
self.errors.into_into_dart().into_dart(),
|
||||||
|
]
|
||||||
|
.into_dart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::SyncResultDto {}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<crate::api::SyncResultDto> for crate::api::SyncResultDto {
|
||||||
|
fn into_into_dart(self) -> crate::api::SyncResultDto {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for crate::api::TaskDto {
|
impl flutter_rust_bridge::IntoDart for crate::api::TaskDto {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
[
|
[
|
||||||
|
|
@ -985,6 +1237,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::TaskDto {
|
||||||
self.description.into_into_dart().into_dart(),
|
self.description.into_into_dart().into_dart(),
|
||||||
self.status.into_into_dart().into_dart(),
|
self.status.into_into_dart().into_dart(),
|
||||||
self.due_date.into_into_dart().into_dart(),
|
self.due_date.into_into_dart().into_dart(),
|
||||||
|
self.has_time.into_into_dart().into_dart(),
|
||||||
self.created_at.into_into_dart().into_dart(),
|
self.created_at.into_into_dart().into_dart(),
|
||||||
self.updated_at.into_into_dart().into_dart(),
|
self.updated_at.into_into_dart().into_dart(),
|
||||||
self.parent_id.into_into_dart().into_dart(),
|
self.parent_id.into_into_dart().into_dart(),
|
||||||
|
|
@ -1072,6 +1325,16 @@ impl SseEncode for bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Vec<String> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<i32>::sse_encode(self.len() as _, serializer);
|
||||||
|
for item in self {
|
||||||
|
<String>::sse_encode(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for Vec<u8> {
|
impl SseEncode for Vec<u8> {
|
||||||
// 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) {
|
||||||
|
|
@ -1122,6 +1385,18 @@ impl SseEncode for Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for crate::api::SyncResultDto {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<u32>::sse_encode(self.uploaded, serializer);
|
||||||
|
<u32>::sse_encode(self.downloaded, serializer);
|
||||||
|
<u32>::sse_encode(self.deleted_local, serializer);
|
||||||
|
<u32>::sse_encode(self.deleted_remote, serializer);
|
||||||
|
<u32>::sse_encode(self.conflicts, serializer);
|
||||||
|
<Vec<String>>::sse_encode(self.errors, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::api::TaskDto {
|
impl SseEncode for crate::api::TaskDto {
|
||||||
// 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) {
|
||||||
|
|
@ -1130,6 +1405,7 @@ impl SseEncode for crate::api::TaskDto {
|
||||||
<String>::sse_encode(self.description, serializer);
|
<String>::sse_encode(self.description, serializer);
|
||||||
<String>::sse_encode(self.status, serializer);
|
<String>::sse_encode(self.status, serializer);
|
||||||
<Option<String>>::sse_encode(self.due_date, serializer);
|
<Option<String>>::sse_encode(self.due_date, serializer);
|
||||||
|
<bool>::sse_encode(self.has_time, serializer);
|
||||||
<String>::sse_encode(self.created_at, serializer);
|
<String>::sse_encode(self.created_at, serializer);
|
||||||
<String>::sse_encode(self.updated_at, serializer);
|
<String>::sse_encode(self.updated_at, serializer);
|
||||||
<Option<String>>::sse_encode(self.parent_id, serializer);
|
<Option<String>>::sse_encode(self.parent_id, serializer);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// 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"));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// 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_
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
|
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
@ -422,21 +424,40 @@ async fn test_webdav_connection(
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn sync_workspace(
|
async fn sync_workspace(
|
||||||
|
workspace_name: String,
|
||||||
workspace_path: String,
|
workspace_path: String,
|
||||||
webdav_url: String,
|
webdav_url: String,
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
mode: String,
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
) -> Result<SyncResult, String> {
|
) -> Result<SyncResult, String> {
|
||||||
|
let sync_mode = match mode.as_str() {
|
||||||
|
"push" => SyncMode::Push,
|
||||||
|
"pull" => SyncMode::Pull,
|
||||||
|
_ => SyncMode::Full,
|
||||||
|
};
|
||||||
let result = sync::sync_workspace(
|
let result = sync::sync_workspace(
|
||||||
&PathBuf::from(workspace_path),
|
&PathBuf::from(&workspace_path),
|
||||||
&webdav_url,
|
&webdav_url,
|
||||||
&username,
|
&username,
|
||||||
&password,
|
&password,
|
||||||
SyncMode::Full,
|
sync_mode,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Persist last_sync timestamp to config
|
||||||
|
{
|
||||||
|
let mut s = state.lock().unwrap();
|
||||||
|
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
||||||
|
ws.last_sync = Some(Utc::now());
|
||||||
|
}
|
||||||
|
let config_path = AppConfig::get_config_path();
|
||||||
|
s.config.save_to_file(&config_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicegui-org/nicegui/v2/tauri-conf-schema.json",
|
|
||||||
"productName": "Onyx",
|
"productName": "Onyx",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.onyx.app",
|
"identifier": "com.onyx.app",
|
||||||
|
|
@ -29,7 +28,7 @@
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": ["appimage", "deb"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { value = null, onchange, onclose }: {
|
let { value = null, has_time = false, onchange, onclose }: {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
onchange: (iso: string | null) => void;
|
has_time: boolean;
|
||||||
|
onchange: (iso: string | null, has_time: boolean) => void;
|
||||||
onclose: () => void;
|
onclose: () => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
let viewYear = $state(existing ? existing.getFullYear() : now.getFullYear());
|
let viewYear = $state(existing ? existing.getFullYear() : now.getFullYear());
|
||||||
let viewMonth = $state(existing ? existing.getMonth() : now.getMonth());
|
let viewMonth = $state(existing ? existing.getMonth() : now.getMonth());
|
||||||
let selectedDay = $state(existing ? existing.getDate() : now.getDate());
|
let selectedDay = $state(existing ? existing.getDate() : now.getDate());
|
||||||
let includeTime = $state(existing ? (existing.getHours() !== 0 || existing.getMinutes() !== 0) : false);
|
let includeTime = $state(has_time);
|
||||||
let selectedHour = $state(existing ? existing.getHours() : now.getHours());
|
let selectedHour = $state(existing ? existing.getHours() : now.getHours());
|
||||||
let selectedMinute = $state(existing ? existing.getMinutes() : 0);
|
let selectedMinute = $state(existing ? existing.getMinutes() : 0);
|
||||||
let visible = $state(false);
|
let visible = $state(false);
|
||||||
|
|
@ -66,12 +67,12 @@
|
||||||
const h = includeTime ? selectedHour : 0;
|
const h = includeTime ? selectedHour : 0;
|
||||||
const m = includeTime ? selectedMinute : 0;
|
const m = includeTime ? selectedMinute : 0;
|
||||||
const iso = new Date(viewYear, viewMonth, selectedDay, h, m).toISOString();
|
const iso = new Date(viewYear, viewMonth, selectedDay, h, m).toISOString();
|
||||||
onchange(iso);
|
onchange(iso, includeTime);
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
onchange(null);
|
onchange(null, false);
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
let title = $state("");
|
let title = $state("");
|
||||||
let description = $state("");
|
let description = $state("");
|
||||||
let dueDate = $state<string | null>(null);
|
let dueDate = $state<string | null>(null);
|
||||||
|
let dueDateHasTime = $state(false);
|
||||||
let inputEl = $state<HTMLInputElement | null>(null);
|
let inputEl = $state<HTMLInputElement | null>(null);
|
||||||
let showDatePicker = $state(false);
|
let showDatePicker = $state(false);
|
||||||
|
|
||||||
|
|
@ -17,11 +18,12 @@
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
const created = await app.createTask(title.trim(), description.trim() || undefined);
|
const created = await app.createTask(title.trim(), description.trim() || undefined);
|
||||||
if (dueDate && created) {
|
if (dueDate && created) {
|
||||||
await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() });
|
await app.updateTask({ ...created, due_date: dueDate, has_time: dueDateHasTime, updated_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
title = "";
|
title = "";
|
||||||
description = "";
|
description = "";
|
||||||
dueDate = null;
|
dueDate = null;
|
||||||
|
dueDateHasTime = false;
|
||||||
newTaskState.open = false;
|
newTaskState.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,11 +32,13 @@
|
||||||
title = "";
|
title = "";
|
||||||
description = "";
|
description = "";
|
||||||
dueDate = null;
|
dueDate = null;
|
||||||
|
dueDateHasTime = false;
|
||||||
showDatePicker = false;
|
showDatePicker = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateChange(iso: string | null) {
|
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
||||||
dueDate = iso;
|
dueDate = iso;
|
||||||
|
dueDateHasTime = hasTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateChip(iso: string): string {
|
function formatDateChip(iso: string): string {
|
||||||
|
|
@ -43,8 +47,7 @@
|
||||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
const day = dayNames[d.getDay()];
|
const day = dayNames[d.getDay()];
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
const hasTime = d.getHours() !== 0 || d.getMinutes() !== 0;
|
const timePart = dueDateHasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
||||||
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
|
||||||
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
||||||
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +140,7 @@
|
||||||
{#if showDatePicker}
|
{#if showDatePicker}
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={dueDate}
|
value={dueDate}
|
||||||
|
has_time={dueDateHasTime}
|
||||||
onchange={handleDateChange}
|
onchange={handleDateChange}
|
||||||
onclose={() => (showDatePicker = false)}
|
onclose={() => (showDatePicker = false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
debouncedSave({ description });
|
debouncedSave({ description });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateChange(iso: string | null) {
|
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
||||||
app.updateTask({ ...task, due_date: iso, updated_at: new Date().toISOString() });
|
app.updateTask({ ...task, due_date: iso, has_time: hasTime, updated_at: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggle() {
|
async function handleToggle() {
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
const day = dayNames[d.getDay()];
|
const day = dayNames[d.getDay()];
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
const hasTime = d.getHours() !== 0 || d.getMinutes() !== 0;
|
const hasTime = task.has_time;
|
||||||
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
||||||
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
||||||
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
||||||
|
|
@ -223,6 +223,7 @@
|
||||||
{#if showDatePicker}
|
{#if showDatePicker}
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={task.due_date}
|
value={task.due_date}
|
||||||
|
has_time={task.has_time}
|
||||||
onchange={handleDateChange}
|
onchange={handleDateChange}
|
||||||
onclose={() => (showDatePicker = false)}
|
onclose={() => (showDatePicker = false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -122,13 +122,35 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if app.config?.current_workspace}
|
{#if app.config?.current_workspace}
|
||||||
|
<div class="mt-3 flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
value={app.syncMode}
|
||||||
|
onchange={(e) => app.setSyncMode((e.target as HTMLSelectElement).value as "full" | "push" | "pull")}
|
||||||
|
class="appearance-none rounded-lg border border-border-light bg-surface-light px-3 py-2 text-sm text-text-light outline-none focus:border-primary dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
|
||||||
|
>
|
||||||
|
<option value="full">Sync both ways</option>
|
||||||
|
<option value="push">Push only</option>
|
||||||
|
<option value="pull">Pull only</option>
|
||||||
|
</select>
|
||||||
<button
|
<button
|
||||||
onclick={() => app.triggerSync()}
|
onclick={() => app.triggerSync()}
|
||||||
disabled={app.syncing}
|
disabled={app.syncing}
|
||||||
class="mt-3 w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
class="flex-1 rounded-lg bg-primary py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{app.syncing ? "Syncing…" : "Sync Now"}
|
{app.syncing ? "Syncing…" : "Sync Now"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if app.config.workspaces[app.config.current_workspace]?.last_sync}
|
||||||
|
{@const lastSync = new Date(app.config.workspaces[app.config.current_workspace].last_sync!)}
|
||||||
|
{@const secsAgo = Math.floor((Date.now() - lastSync.getTime()) / 1000)}
|
||||||
|
{@const relTime = secsAgo < 60 ? "just now" : secsAgo < 3600 ? `${Math.floor(secsAgo / 60)}m ago` : `${Math.floor(secsAgo / 3600)}h ago`}
|
||||||
|
<p class="mt-1.5 text-xs opacity-40">
|
||||||
|
Last sync: {relTime}
|
||||||
|
{#if app.lastSyncResult}
|
||||||
|
· ↑{app.lastSyncResult.uploaded} ↓{app.lastSyncResult.downloaded}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,9 +550,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sync spinner -->
|
<!-- Sync status indicator -->
|
||||||
{#if app.syncing}
|
{#if app.syncing}
|
||||||
<div class="absolute bottom-4 right-4 z-20 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
<div class="absolute bottom-4 right-4 z-20 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||||
|
{:else if app.lastSyncResult}
|
||||||
|
<div class="absolute bottom-4 right-4 z-20 flex items-center gap-1 rounded-full bg-black/10 px-2.5 py-1 text-xs opacity-60 dark:bg-white/10">
|
||||||
|
<span>↑{app.lastSyncResult.uploaded}</span>
|
||||||
|
<span>↓{app.lastSyncResult.downloaded}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ let darkMode = $state(
|
||||||
globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false,
|
globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false,
|
||||||
);
|
);
|
||||||
let syncing = $state(false);
|
let syncing = $state(false);
|
||||||
|
let syncMode = $state<"full" | "push" | "pull">("full");
|
||||||
|
let lastSyncResult = $state<SyncResult | null>(null);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
// ── Derived ──────────────────────────────────────────────────────────
|
// ── Derived ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -254,7 +256,8 @@ async function setGroupByDueDate(listId: string, enabled: boolean) {
|
||||||
|
|
||||||
async function triggerSync() {
|
async function triggerSync() {
|
||||||
if (!config?.current_workspace) return;
|
if (!config?.current_workspace) return;
|
||||||
const ws = config.workspaces[config.current_workspace];
|
const workspaceName = config.current_workspace;
|
||||||
|
const ws = config.workspaces[workspaceName];
|
||||||
if (!ws?.webdav_url) {
|
if (!ws?.webdav_url) {
|
||||||
error = "No WebDAV URL configured";
|
error = "No WebDAV URL configured";
|
||||||
return;
|
return;
|
||||||
|
|
@ -265,14 +268,19 @@ async function triggerSync() {
|
||||||
const domain = new URL(ws.webdav_url).hostname;
|
const domain = new URL(ws.webdav_url).hostname;
|
||||||
const [username, password] = await invoke<[string, string]>("load_credentials", { domain });
|
const [username, password] = await invoke<[string, string]>("load_credentials", { domain });
|
||||||
const result = await invoke<SyncResult>("sync_workspace", {
|
const result = await invoke<SyncResult>("sync_workspace", {
|
||||||
|
workspaceName,
|
||||||
workspacePath: ws.path,
|
workspacePath: ws.path,
|
||||||
webdavUrl: ws.webdav_url,
|
webdavUrl: ws.webdav_url,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
mode: syncMode,
|
||||||
});
|
});
|
||||||
|
lastSyncResult = result;
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
error = result.errors.join("; ");
|
error = result.errors.join("; ");
|
||||||
}
|
}
|
||||||
|
// Reload config to pick up updated last_sync timestamp
|
||||||
|
config = await invoke<AppConfig>("get_config");
|
||||||
await loadLists();
|
await loadLists();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e);
|
error = String(e);
|
||||||
|
|
@ -281,6 +289,10 @@ async function triggerSync() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSyncMode(mode: "full" | "push" | "pull") {
|
||||||
|
syncMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
darkMode = !darkMode;
|
darkMode = !darkMode;
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +338,12 @@ export const app = {
|
||||||
get syncing() {
|
get syncing() {
|
||||||
return syncing;
|
return syncing;
|
||||||
},
|
},
|
||||||
|
get syncMode() {
|
||||||
|
return syncMode;
|
||||||
|
},
|
||||||
|
get lastSyncResult() {
|
||||||
|
return lastSyncResult;
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return error;
|
return error;
|
||||||
},
|
},
|
||||||
|
|
@ -350,6 +368,7 @@ export const app = {
|
||||||
renameList,
|
renameList,
|
||||||
setGroupByDueDate,
|
setGroupByDueDate,
|
||||||
triggerSync,
|
triggerSync,
|
||||||
|
setSyncMode,
|
||||||
toggleDarkMode,
|
toggleDarkMode,
|
||||||
setScreen,
|
setScreen,
|
||||||
clearError,
|
clearError,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export interface Task {
|
||||||
description: string;
|
description: string;
|
||||||
status: "backlog" | "completed";
|
status: "backlog" | "completed";
|
||||||
due_date: string | null;
|
due_date: string | null;
|
||||||
|
has_time: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
parent_id: string | null;
|
parent_id: string | null;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ pub struct Task {
|
||||||
pub status: TaskStatus,
|
pub status: TaskStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub due_date: Option<DateTime<Utc>>,
|
pub due_date: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub has_time: bool,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -32,6 +34,7 @@ impl Task {
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
status: TaskStatus::Backlog,
|
status: TaskStatus::Backlog,
|
||||||
due_date: None,
|
due_date: None,
|
||||||
|
has_time: false,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ pub struct TaskFrontmatter {
|
||||||
pub status: TaskStatus,
|
pub status: TaskStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub due: Option<DateTime<Utc>>,
|
pub due: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub has_time: bool,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub updated: DateTime<Utc>,
|
pub updated: DateTime<Utc>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -68,6 +70,7 @@ impl From<&Task> for TaskFrontmatter {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
due: task.due_date,
|
due: task.due_date,
|
||||||
|
has_time: task.has_time,
|
||||||
created: task.created_at,
|
created: task.created_at,
|
||||||
updated: task.updated_at,
|
updated: task.updated_at,
|
||||||
parent: task.parent_id,
|
parent: task.parent_id,
|
||||||
|
|
@ -256,6 +259,7 @@ impl Storage for FileSystemStorage {
|
||||||
description,
|
description,
|
||||||
status: frontmatter.status,
|
status: frontmatter.status,
|
||||||
due_date: frontmatter.due,
|
due_date: frontmatter.due,
|
||||||
|
has_time: frontmatter.has_time,
|
||||||
created_at: frontmatter.created,
|
created_at: frontmatter.created,
|
||||||
updated_at: frontmatter.updated,
|
updated_at: frontmatter.updated,
|
||||||
parent_id: frontmatter.parent,
|
parent_id: frontmatter.parent,
|
||||||
|
|
@ -344,6 +348,7 @@ impl Storage for FileSystemStorage {
|
||||||
description,
|
description,
|
||||||
status: frontmatter.status,
|
status: frontmatter.status,
|
||||||
due_date: frontmatter.due,
|
due_date: frontmatter.due,
|
||||||
|
has_time: frontmatter.has_time,
|
||||||
created_at: frontmatter.created,
|
created_at: frontmatter.created,
|
||||||
updated_at: frontmatter.updated,
|
updated_at: frontmatter.updated,
|
||||||
parent_id: frontmatter.parent,
|
parent_id: frontmatter.parent,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue