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.
This commit is contained in:
Tristan Michael 2026-04-01 01:22:11 -07:00
parent 39239fadc3
commit 6ccc167239
17 changed files with 1286 additions and 155 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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 {
), ),
), ),
), ),
),
); );
} }
} }

View file

@ -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(),
),
),
], ],
); );
} }

View file

@ -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();

View file

@ -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();
} }

View file

@ -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';
} }

View file

@ -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';
} }

View file

@ -1194,6 +1194,7 @@ dependencies = [
"notify-debouncer-mini", "notify-debouncer-mini",
"once_cell", "once_cell",
"onyx-core", "onyx-core",
"tokio",
"uuid", "uuid",
] ]

View file

@ -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"] }

View file

@ -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>>> =

View file

@ -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);

View file

@ -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"));
}

View file

@ -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_

View file

@ -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)