From 6ccc1672390f8c4c2c2d9370b5860d56e3deaebd Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Wed, 1 Apr 2026 01:22:11 -0700 Subject: [PATCH] Settings: add WebDAV sync UI and syncing backend Add WebDAV configuration, credential storage/testing, and sync controls across Flutter UI and Rust API. This implements a stateful Settings screen with fields to enter server URL, username, and password, plus Test and Save actions; persist/load credentials and workspace WebDAV URL via the Rust API; add sync mode selection, a Sync Now action, and a sync status indicator in Tasks screen; thread has_time through date/time pickers, new task creation, task detail updates, and task DTOs; implement async Rust functions for testing connections, storing/loading credentials, setting workspace WebDAV config, and triggering workspace sync with a SyncResult mapped back to Flutter; add tokio runtime dependency. These changes were needed to enable full WebDAV-based synchronization and provide users controls and feedback for configuring and running syncs from the Flutter app. --- apps/flutter/lib/src/rust/api.dart | 90 ++++ apps/flutter/lib/src/rust/frb_generated.dart | 329 ++++++++++++- .../lib/src/rust/frb_generated.io.dart | 18 + .../lib/src/rust/frb_generated.web.dart | 18 + .../lib/src/screens/settings_screen.dart | 444 +++++++++++++----- .../flutter/lib/src/screens/tasks_screen.dart | 30 +- apps/flutter/lib/src/state/app_state.dart | 41 ++ .../lib/src/widgets/date_time_picker.dart | 9 +- .../lib/src/widgets/new_task_input.dart | 13 +- .../lib/src/widgets/task_detail_view.dart | 12 +- apps/flutter/rust/Cargo.lock | 1 + apps/flutter/rust/Cargo.toml | 1 + apps/flutter/rust/src/api.rs | 82 ++++ apps/flutter/rust/src/frb_generated.rs | 296 +++++++++++- .../flutter/generated_plugin_registrant.cc | 17 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 25 + 17 files changed, 1286 insertions(+), 155 deletions(-) diff --git a/apps/flutter/lib/src/rust/api.dart b/apps/flutter/lib/src/rust/api.dart index 0982289..2668c80 100644 --- a/apps/flutter/lib/src/rust/api.dart +++ b/apps/flutter/lib/src/rust/api.dart @@ -87,6 +87,53 @@ Future setGroupByDueDate({ Future getGroupByDueDate({required String listId}) => RustLib.instance.api.crateApiGetGroupByDueDate(listId: listId); +Future storeCredentials({ + required String domain, + required String username, + required String password, +}) => RustLib.instance.api.crateApiStoreCredentials( + domain: domain, + username: username, + password: password, +); + +Future> loadCredentials({required String domain}) => + RustLib.instance.api.crateApiLoadCredentials(domain: domain); + +Future setWebdavConfig({ + required String workspaceName, + required String webdavUrl, +}) => RustLib.instance.api.crateApiSetWebdavConfig( + workspaceName: workspaceName, + webdavUrl: webdavUrl, +); + +Future testWebdavConnection({ + required String url, + required String username, + required String password, +}) => RustLib.instance.api.crateApiTestWebdavConnection( + url: url, + username: username, + password: password, +); + +Future 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> watchWorkspaceChanges({required String path}) => RustLib.instance.api.crateApiWatchWorkspaceChanges(path: path); @@ -111,12 +158,52 @@ class AppConfigDto { currentWorkspace == other.currentWorkspace; } +class SyncResultDto { + final int uploaded; + final int downloaded; + final int deletedLocal; + final int deletedRemote; + final int conflicts; + final List 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 { final String id; final String title; final String description; final String status; final String? dueDate; + final bool hasTime; final String createdAt; final String updatedAt; final String? parentId; @@ -127,6 +214,7 @@ class TaskDto { required this.description, required this.status, this.dueDate, + required this.hasTime, required this.createdAt, required this.updatedAt, this.parentId, @@ -139,6 +227,7 @@ class TaskDto { description.hashCode ^ status.hashCode ^ dueDate.hashCode ^ + hasTime.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode ^ parentId.hashCode; @@ -153,6 +242,7 @@ class TaskDto { description == other.description && status == other.status && dueDate == other.dueDate && + hasTime == other.hasTime && createdAt == other.createdAt && updatedAt == other.updatedAt && parentId == other.parentId; diff --git a/apps/flutter/lib/src/rust/frb_generated.dart b/apps/flutter/lib/src/rust/frb_generated.dart index 831031d..6aab38f 100644 --- a/apps/flutter/lib/src/rust/frb_generated.dart +++ b/apps/flutter/lib/src/rust/frb_generated.dart @@ -64,7 +64,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -75020133; + int get rustContentHash => -1094746925; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -107,6 +107,8 @@ abstract class RustLibApi extends BaseApi { Future> crateApiListTasks({required String listId}); + Future> crateApiLoadCredentials({required String domain}); + Future crateApiMoveTask({ required String fromListId, required String toListId, @@ -133,6 +135,32 @@ abstract class RustLibApi extends BaseApi { required bool enabled, }); + Future crateApiSetWebdavConfig({ + required String workspaceName, + required String webdavUrl, + }); + + Future crateApiStoreCredentials({ + required String domain, + required String username, + required String password, + }); + + Future crateApiSyncWorkspaceCmd({ + required String workspaceName, + required String workspacePath, + required String webdavUrl, + required String username, + required String password, + required String mode, + }); + + Future crateApiTestWebdavConnection({ + required String url, + required String username, + required String password, + }); + Future crateApiToggleTask({ required String listId, required String taskId, @@ -482,6 +510,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiListTasksConstMeta => const TaskConstMeta(debugName: "list_tasks", argNames: ["listId"]); + @override + Future> 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 Future crateApiMoveTask({ required String fromListId, @@ -498,7 +554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 13, port: port_, ); }, @@ -528,7 +584,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -560,7 +616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 15, port: port_, ); }, @@ -596,7 +652,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 16, port: port_, ); }, @@ -626,7 +682,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 17, port: port_, ); }, @@ -661,7 +717,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 18, port: port_, ); }, @@ -681,6 +737,169 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["listId", "enabled"], ); + @override + Future 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 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 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 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 Future crateApiToggleTask({ required String listId, @@ -695,7 +914,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 23, port: port_, ); }, @@ -729,7 +948,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 24, port: port_, ); }, @@ -763,7 +982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 25, port: port_, ); }, @@ -827,6 +1046,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_task_dto(raw); } + @protected + List dco_decode_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_String).toList(); + } + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { // 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); } + @protected + SyncResultDto dco_decode_sync_result_dto(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + 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 TaskDto dco_decode_task_dto(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 8) - throw Exception('unexpected arr length: expect 8 but see ${arr.length}'); + if (arr.length != 9) + throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); return TaskDto( id: dco_decode_String(arr[0]), title: dco_decode_String(arr[1]), description: dco_decode_String(arr[2]), status: dco_decode_String(arr[3]), dueDate: dco_decode_opt_String(arr[4]), - createdAt: dco_decode_String(arr[5]), - updatedAt: dco_decode_String(arr[6]), - parentId: dco_decode_opt_String(arr[7]), + hasTime: dco_decode_bool(arr[5]), + createdAt: dco_decode_String(arr[6]), + 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)); } + @protected + List 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_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_String(deserializer)); + } + return ans_; + } + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // 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 TaskDto sse_decode_task_dto(SseDeserializer deserializer) { // 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_status = sse_decode_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_updatedAt = sse_decode_String(deserializer); var var_parentId = sse_decode_opt_String(deserializer); @@ -1042,6 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { description: var_description, status: var_status, dueDate: var_dueDate, + hasTime: var_hasTime, createdAt: var_createdAt, updatedAt: var_updatedAt, parentId: var_parentId, @@ -1154,6 +1429,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_task_dto(self, serializer); } + @protected + void sse_encode_list_String(List 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 void sse_encode_list_prim_u_8_strict( 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 void sse_encode_task_dto(TaskDto self, SseSerializer serializer) { // 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.status, serializer); sse_encode_opt_String(self.dueDate, serializer); + sse_encode_bool(self.hasTime, serializer); sse_encode_String(self.createdAt, serializer); sse_encode_String(self.updatedAt, serializer); sse_encode_opt_String(self.parentId, serializer); diff --git a/apps/flutter/lib/src/rust/frb_generated.io.dart b/apps/flutter/lib/src/rust/frb_generated.io.dart index 98ae350..fdbee3b 100644 --- a/apps/flutter/lib/src/rust/frb_generated.io.dart +++ b/apps/flutter/lib/src/rust/frb_generated.io.dart @@ -36,6 +36,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TaskDto dco_decode_box_autoadd_task_dto(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -51,6 +54,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + SyncResultDto dco_decode_sync_result_dto(dynamic raw); + @protected TaskDto dco_decode_task_dto(dynamic raw); @@ -89,6 +95,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -106,6 +115,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer); + @protected TaskDto sse_decode_task_dto(SseDeserializer deserializer); @@ -151,6 +163,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -175,6 +190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer); + @protected void sse_encode_task_dto(TaskDto self, SseSerializer serializer); diff --git a/apps/flutter/lib/src/rust/frb_generated.web.dart b/apps/flutter/lib/src/rust/frb_generated.web.dart index 3d0c0b4..5a30b0a 100644 --- a/apps/flutter/lib/src/rust/frb_generated.web.dart +++ b/apps/flutter/lib/src/rust/frb_generated.web.dart @@ -38,6 +38,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TaskDto dco_decode_box_autoadd_task_dto(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -53,6 +56,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? dco_decode_opt_String(dynamic raw); + @protected + SyncResultDto dco_decode_sync_result_dto(dynamic raw); + @protected TaskDto dco_decode_task_dto(dynamic raw); @@ -91,6 +97,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -108,6 +117,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + SyncResultDto sse_decode_sync_result_dto(SseDeserializer deserializer); + @protected TaskDto sse_decode_task_dto(SseDeserializer deserializer); @@ -153,6 +165,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -177,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_sync_result_dto(SyncResultDto self, SseSerializer serializer); + @protected void sse_encode_task_dto(TaskDto self, SseSerializer serializer); diff --git a/apps/flutter/lib/src/screens/settings_screen.dart b/apps/flutter/lib/src/screens/settings_screen.dart index 59ec686..1bd4dc9 100644 --- a/apps/flutter/lib/src/screens/settings_screen.dart +++ b/apps/flutter/lib/src/screens/settings_screen.dart @@ -1,15 +1,131 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../rust/api.dart' as api; import '../state/app_state.dart'; import '../theme.dart'; -class SettingsScreen extends StatelessWidget { +class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + 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 _loadCredentials() async { + final state = context.read(); + final wsName = state.config?.currentWorkspace; + if (wsName == null) return; + final ws = state.config!.workspaces.cast().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 _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 _save() async { + final state = context.read(); + 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 Widget build(BuildContext context) { final state = context.watch(); 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() + .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( onTap: () => state.setScreen('tasks'), child: Container( @@ -20,131 +136,233 @@ class SettingsScreen extends StatelessWidget { ), child: GestureDetector( onTap: () {}, - child: AnimatedScale( - scale: 1.0, - duration: const Duration(milliseconds: 150), - curve: Curves.easeOut, - child: Container( - decoration: BoxDecoration( - color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withValues(alpha: 0.1)), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.7), blurRadius: 60, offset: const Offset(0, 25)), - BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10)), - ], - ), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - // Header (matching Tauri: text-lg font-bold, border-b, px-4 py-3) - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: isDark ? AppTheme.borderDark : AppTheme.borderLight, width: 0.5)), - ), - child: Row( - children: [ - const Text('Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), - const Spacer(), - GestureDetector( - onTap: () => state.setScreen('tasks'), - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: Icon(Icons.close, size: 20, - color: isDark ? AppTheme.textDark : AppTheme.textLight), - ), - ), - ], - ), + child: Container( + decoration: BoxDecoration( + color: isDark ? AppTheme.surfaceDark : AppTheme.surfaceLight, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.7), blurRadius: 60, offset: const Offset(0, 25)), + BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 20, offset: const Offset(0, 10)), + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: borderColor, width: 0.5)), ), - // Scrollable content - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // WebDAV Sync section (matching Tauri order: sync first) - Text('WEBDAV SYNC', - style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, - letterSpacing: 0.5, - color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5), - )), - const SizedBox(height: 12), - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), - ), - child: Text( - 'WebDAV sync not yet available in Flutter build', - style: TextStyle(fontSize: 13, color: isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight), - ), + child: Row( + children: [ + const Text('Settings', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), + const Spacer(), + GestureDetector( + onTap: () => state.setScreen('tasks'), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: Icon(Icons.close, size: 20, + color: isDark ? AppTheme.textDark : AppTheme.textLight), + ), + ), + ], + ), + ), + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // WebDAV section header + Text('WEBDAV SYNC', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))), + const SizedBox(height: 12), + // Credentials card + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor), ), - const SizedBox(height: 24), - // Appearance section - Text('APPEARANCE', - style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w600, - letterSpacing: 0.5, - color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5), - )), - const SizedBox(height: 12), - // Dark mode toggle in bordered card (matching Tauri) - GestureDetector( - onTap: () => state.toggleDarkMode(), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: isDark ? AppTheme.borderDark : AppTheme.borderLight), - ), - child: Row( + 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: [ - const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), - const Spacer(), - // Toggle switch (matching Tauri: h-6 w-11) - AnimatedContainer( - duration: const Duration(milliseconds: 150), - width: 44, - height: 24, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)), - ), - child: AnimatedAlign( - duration: const Duration(milliseconds: 150), - alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft, - child: Container( - width: 20, - height: 20, - margin: const EdgeInsets.symmetric(horizontal: 2), - decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white), + 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( + _testStatus == 'testing' ? 'Testing…' + : _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( + 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), + // Appearance + Text('APPEARANCE', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: (isDark ? AppTheme.textSecondaryDark : AppTheme.textSecondaryLight).withValues(alpha: 0.5))), + const SizedBox(height: 12), + GestureDetector( + onTap: () => state.toggleDarkMode(), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + const Text('Dark mode', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + const Spacer(), + AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: 44, height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: state.darkMode ? AppTheme.primary : (isDark ? const Color(0xFF4B5563) : const Color(0xFFD1D5DB)), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 150), + alignment: state.darkMode ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 20, height: 20, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white), + ), + ), + ), + ], ), ), - const SizedBox(height: 32), - Center( - child: Text('Flutter + Rust', style: TextStyle(fontSize: 12, + ), + const SizedBox(height: 32), + Center( + child: Text('Flutter + Rust', + style: TextStyle(fontSize: 12, color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.3))), - ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ), ), ), diff --git a/apps/flutter/lib/src/screens/tasks_screen.dart b/apps/flutter/lib/src/screens/tasks_screen.dart index f26fbc5..c7bd4e4 100644 --- a/apps/flutter/lib/src/screens/tasks_screen.dart +++ b/apps/flutter/lib/src/screens/tasks_screen.dart @@ -69,13 +69,13 @@ class _TasksScreenState extends State with SingleTickerProviderStat }); } - Future _handleCreateTask(String title, String desc, {String? dueDate}) async { + Future _handleCreateTask(String title, String desc, {String? dueDate, bool hasTime = false}) async { final state = context.read(); final task = await state.createTask(title, desc); if (task != null && dueDate != null) { await state.updateTask(api.TaskDto( 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, )); } @@ -257,6 +257,32 @@ class _TasksScreenState extends State 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(), + ), + ), ], ); } diff --git a/apps/flutter/lib/src/state/app_state.dart b/apps/flutter/lib/src/state/app_state.dart index 41c213f..7b131c8 100644 --- a/apps/flutter/lib/src/state/app_state.dart +++ b/apps/flutter/lib/src/state/app_state.dart @@ -11,6 +11,8 @@ class AppState extends ChangeNotifier { bool darkMode = true; StreamSubscription? _watcherSub; bool syncing = false; + String syncMode = 'full'; + api.SyncResultDto? lastSyncResult; String? error; // Selected task for detail view @@ -264,6 +266,45 @@ class AppState extends ChangeNotifier { notifyListeners(); } + void setSyncMode(String mode) { + syncMode = mode; + notifyListeners(); + } + + Future 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() { darkMode = !darkMode; notifyListeners(); diff --git a/apps/flutter/lib/src/widgets/date_time_picker.dart b/apps/flutter/lib/src/widgets/date_time_picker.dart index 3443487..ea5e28b 100644 --- a/apps/flutter/lib/src/widgets/date_time_picker.dart +++ b/apps/flutter/lib/src/widgets/date_time_picker.dart @@ -3,10 +3,11 @@ import '../theme.dart'; class DateTimePicker extends StatefulWidget { final DateTime? initialDate; - final void Function(DateTime date) onDone; + final bool initialHasTime; + final void Function(DateTime date, bool hasTime) onDone; 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 State createState() => _DateTimePickerState(); @@ -27,7 +28,7 @@ class _DateTimePickerState extends State { if (widget.initialDate != null) { _hour = widget.initialDate!.hour; _minute = widget.initialDate!.minute; - _showTime = _hour != 0 || _minute != 0; + _showTime = widget.initialHasTime; } } @@ -39,7 +40,7 @@ class _DateTimePickerState extends State { final result = _showTime ? DateTime(_selected!.year, _selected!.month, _selected!.day, _hour, _minute) : DateTime(_selected!.year, _selected!.month, _selected!.day); - widget.onDone(result); + widget.onDone(result, _showTime); Navigator.of(context).pop(); } diff --git a/apps/flutter/lib/src/widgets/new_task_input.dart b/apps/flutter/lib/src/widgets/new_task_input.dart index 96e1daa..c88688c 100644 --- a/apps/flutter/lib/src/widgets/new_task_input.dart +++ b/apps/flutter/lib/src/widgets/new_task_input.dart @@ -3,7 +3,7 @@ import '../theme.dart'; import 'date_time_picker.dart'; class NewTaskInput extends StatefulWidget { - final Future Function(String title, String description, {String? dueDate}) onCreate; + final Future Function(String title, String description, {String? dueDate, bool hasTime}) onCreate; const NewTaskInput({super.key, required this.onCreate}); @@ -16,6 +16,7 @@ class _NewTaskInputState extends State { final _descController = TextEditingController(); final _titleFocus = FocusNode(); DateTime? _selectedDate; + bool _selectedHasTime = false; @override void initState() { @@ -34,7 +35,7 @@ class _NewTaskInputState extends State { Future _submit() async { final title = _titleController.text.trim(); 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() { @@ -47,8 +48,9 @@ class _NewTaskInputState extends State { ), builder: (_) => DateTimePicker( initialDate: _selectedDate, - onDone: (date) => setState(() => _selectedDate = date), - onClear: () => setState(() => _selectedDate = null), + initialHasTime: _selectedHasTime, + onDone: (date, hasTime) => setState(() { _selectedDate = date; _selectedHasTime = hasTime; }), + onClear: () => setState(() { _selectedDate = null; _selectedHasTime = false; }), ), ); } @@ -60,8 +62,7 @@ class _NewTaskInputState extends State { final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; final day = dayNames[d.weekday % 7]; final pad = (int n) => n.toString().padLeft(2, '0'); - final hasTime = d.hour != 0 || d.minute != 0; - final timePart = hasTime ? ', ${pad(d.hour)}:${pad(d.minute)}' : ''; + final timePart = _selectedHasTime ? ', ${pad(d.hour)}:${pad(d.minute)}' : ''; if (taskDate == today) return 'Today$timePart'; return '$day, ${pad(d.day)}/${pad(d.month)}$timePart'; } diff --git a/apps/flutter/lib/src/widgets/task_detail_view.dart b/apps/flutter/lib/src/widgets/task_detail_view.dart index aacc06e..0e0311a 100644 --- a/apps/flutter/lib/src/widgets/task_detail_view.dart +++ b/apps/flutter/lib/src/widgets/task_detail_view.dart @@ -55,7 +55,7 @@ class _TaskDetailViewState extends State with SingleTickerProvid super.dispose(); } - void _scheduleUpdate({String? dueDate}) { + void _scheduleUpdate({String? dueDate, bool? hasTime}) { _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 400), () { final state = context.read(); @@ -65,6 +65,7 @@ class _TaskDetailViewState extends State with SingleTickerProvid description: _descController.text, status: widget.task.status, dueDate: dueDate ?? widget.task.dueDate, + hasTime: hasTime ?? widget.task.hasTime, createdAt: widget.task.createdAt, updatedAt: widget.task.updatedAt, parentId: widget.task.parentId, @@ -72,7 +73,7 @@ class _TaskDetailViewState extends State with SingleTickerProvid }); } - void _updateDueDate(String? dueDate) { + void _updateDueDate(String? dueDate, {bool hasTime = false}) { final state = context.read(); state.updateTask(api.TaskDto( id: widget.task.id, @@ -80,6 +81,7 @@ class _TaskDetailViewState extends State with SingleTickerProvid description: _descController.text, status: widget.task.status, dueDate: dueDate, + hasTime: hasTime, createdAt: widget.task.createdAt, updatedAt: widget.task.updatedAt, parentId: widget.task.parentId, @@ -96,7 +98,8 @@ class _TaskDetailViewState extends State with SingleTickerProvid ), builder: (_) => DateTimePicker( 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), ), ); @@ -112,8 +115,7 @@ class _TaskDetailViewState extends State with SingleTickerProvid final dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; final day = dayNames[local.weekday % 7]; final pad = (int n) => n.toString().padLeft(2, '0'); - final hasTime = local.hour != 0 || local.minute != 0; - final timePart = hasTime ? ', ${pad(local.hour)}:${pad(local.minute)}' : ''; + final timePart = widget.task.hasTime ? ', ${pad(local.hour)}:${pad(local.minute)}' : ''; if (taskDate == today) return 'Today$timePart'; return '$day, ${pad(local.day)}/${pad(local.month)}$timePart'; } diff --git a/apps/flutter/rust/Cargo.lock b/apps/flutter/rust/Cargo.lock index 6b6df3f..2ec4983 100644 --- a/apps/flutter/rust/Cargo.lock +++ b/apps/flutter/rust/Cargo.lock @@ -1194,6 +1194,7 @@ dependencies = [ "notify-debouncer-mini", "once_cell", "onyx-core", + "tokio", "uuid", ] diff --git a/apps/flutter/rust/Cargo.toml b/apps/flutter/rust/Cargo.toml index f0630d9..14d1a65 100644 --- a/apps/flutter/rust/Cargo.toml +++ b/apps/flutter/rust/Cargo.toml @@ -14,3 +14,4 @@ chrono = { version = "0.4", features = ["serde"] } once_cell = "1" notify = "7" notify-debouncer-mini = "0.5" +tokio = { version = "1", features = ["full"] } diff --git a/apps/flutter/rust/src/api.rs b/apps/flutter/rust/src/api.rs index 389cb3e..00baa04 100644 --- a/apps/flutter/rust/src/api.rs +++ b/apps/flutter/rust/src/api.rs @@ -11,6 +11,8 @@ use onyx_core::{ config::{AppConfig, WorkspaceConfig}, models::{Task, TaskStatus}, repository::TaskRepository, + sync::{self, SyncMode}, + webdav, }; // ── State ─────────────────────────────────────────────────────────── @@ -44,11 +46,21 @@ pub struct TaskDto { pub description: String, pub status: String, pub due_date: Option, + pub has_time: bool, pub created_at: String, pub updated_at: String, pub parent_id: Option, } +pub struct SyncResultDto { + pub uploaded: u32, + pub downloaded: u32, + pub deleted_local: u32, + pub deleted_remote: u32, + pub conflicts: u32, + pub errors: Vec, +} + pub struct TaskListDto { pub id: String, pub title: String, @@ -79,6 +91,7 @@ fn task_to_dto(t: &Task) -> TaskDto { TaskStatus::Completed => "completed".into(), }, due_date: t.due_date.map(|d| d.to_rfc3339()), + has_time: t.has_time, created_at: t.created_at.to_rfc3339(), updated_at: t.updated_at.to_rfc3339(), 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() .and_then(|d| chrono::DateTime::parse_from_rfc3339(d).ok()) .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()) } @@ -295,6 +309,74 @@ pub fn get_group_by_due_date(list_id: String) -> Result { 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, 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 { + 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 ─────────────────────────────────────────────────── static WATCHER: Mutex>> = diff --git a/apps/flutter/rust/src/frb_generated.rs b/apps/flutter/rust/src/frb_generated.rs index b780f8b..766401e 100644 --- a/apps/flutter/rust/src/frb_generated.rs +++ b/apps/flutter/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); 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 @@ -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::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 = ::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( port_: flutter_rust_bridge::for_generated::MessagePort, 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::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 = ::sse_decode(&mut deserializer); + let api_webdav_url = ::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::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 = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_password = ::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::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 = ::sse_decode(&mut deserializer); + let api_workspace_path = ::sse_decode(&mut deserializer); + let api_webdav_url = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_password = ::sse_decode(&mut deserializer); + let api_mode = ::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::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 = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_password = ::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( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -770,6 +963,18 @@ impl SseDecode for bool { } } +impl SseDecode for Vec { + // 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_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -829,6 +1034,26 @@ impl SseDecode for Option { } } +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 = ::sse_decode(deserializer); + let mut var_downloaded = ::sse_decode(deserializer); + let mut var_deletedLocal = ::sse_decode(deserializer); + let mut var_deletedRemote = ::sse_decode(deserializer); + let mut var_conflicts = ::sse_decode(deserializer); + let mut var_errors = >::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 { // Codec=Sse (Serialization based), see doc to use other codecs 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 = ::sse_decode(deserializer); let mut var_status = ::sse_decode(deserializer); let mut var_dueDate = >::sse_decode(deserializer); + let mut var_hasTime = ::sse_decode(deserializer); let mut var_createdAt = ::sse_decode(deserializer); let mut var_updatedAt = ::sse_decode(deserializer); let mut var_parentId = >::sse_decode(deserializer); @@ -846,6 +1072,7 @@ impl SseDecode for crate::api::TaskDto { description: var_description, status: var_status, due_date: var_dueDate, + has_time: var_hasTime, created_at: var_createdAt, updated_at: var_updatedAt, 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), 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), - 12 => wire__crate__api__move_task_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__api__remove_workspace_impl(port, ptr, rust_vec_len, data_len), - 14 => wire__crate__api__rename_list_impl(port, ptr, rust_vec_len, data_len), - 15 => wire__crate__api__reorder_task_impl(port, ptr, rust_vec_len, data_len), - 16 => wire__crate__api__set_current_workspace_impl(port, ptr, rust_vec_len, data_len), - 17 => wire__crate__api__set_group_by_due_date_impl(port, ptr, rust_vec_len, data_len), - 18 => wire__crate__api__toggle_task_impl(port, ptr, rust_vec_len, data_len), - 19 => wire__crate__api__update_task_impl(port, ptr, rust_vec_len, data_len), - 20 => wire__crate__api__watch_workspace_changes_impl(port, ptr, rust_vec_len, data_len), + 12 => wire__crate__api__load_credentials_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__remove_workspace_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__reorder_task_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__set_group_by_due_date_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__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!(), } } @@ -977,6 +1209,26 @@ impl flutter_rust_bridge::IntoIntoDart for crate::api: } } // 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 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 { 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.status.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.updated_at.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 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1122,6 +1385,18 @@ impl SseEncode for Option { } } +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) { + ::sse_encode(self.uploaded, serializer); + ::sse_encode(self.downloaded, serializer); + ::sse_encode(self.deleted_local, serializer); + ::sse_encode(self.deleted_remote, serializer); + ::sse_encode(self.conflicts, serializer); + >::sse_encode(self.errors, serializer); + } +} + impl SseEncode for crate::api::TaskDto { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1130,6 +1405,7 @@ impl SseEncode for crate::api::TaskDto { ::sse_encode(self.description, serializer); ::sse_encode(self.status, serializer); >::sse_encode(self.due_date, serializer); + ::sse_encode(self.has_time, serializer); ::sse_encode(self.created_at, serializer); ::sse_encode(self.updated_at, serializer); >::sse_encode(self.parent_id, serializer); diff --git a/apps/flutter/windows/flutter/generated_plugin_registrant.cc b/apps/flutter/windows/flutter/generated_plugin_registrant.cc index e69de29..c6fe39a 100644 --- a/apps/flutter/windows/flutter/generated_plugin_registrant.cc +++ b/apps/flutter/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/apps/flutter/windows/flutter/generated_plugin_registrant.h b/apps/flutter/windows/flutter/generated_plugin_registrant.h index e69de29..dc139d8 100644 --- a/apps/flutter/windows/flutter/generated_plugin_registrant.h +++ b/apps/flutter/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/flutter/windows/flutter/generated_plugins.cmake b/apps/flutter/windows/flutter/generated_plugins.cmake index e69de29..5e3bc3d 100644 --- a/apps/flutter/windows/flutter/generated_plugins.cmake +++ b/apps/flutter/windows/flutter/generated_plugins.cmake @@ -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 $) + 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)