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:
parent
39239fadc3
commit
6ccc167239
|
|
@ -87,6 +87,53 @@ Future<void> setGroupByDueDate({
|
|||
Future<bool> getGroupByDueDate({required String 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}) =>
|
||||
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<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 {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
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<List<TaskDto>> crateApiListTasks({required String listId});
|
||||
|
||||
Future<List<String>> crateApiLoadCredentials({required String domain});
|
||||
|
||||
Future<void> crateApiMoveTask({
|
||||
required String fromListId,
|
||||
required String toListId,
|
||||
|
|
@ -133,6 +135,32 @@ abstract class RustLibApi extends BaseApi {
|
|||
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({
|
||||
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<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
|
||||
Future<void> 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<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
|
||||
Future<TaskDto> 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<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
|
||||
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<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
|
||||
TaskDto dco_decode_task_dto(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
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<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
|
||||
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<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
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> 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<RustLibWire> {
|
|||
@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<RustLibWire> {
|
|||
@protected
|
||||
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> 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<RustLibWire> {
|
|||
@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<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_prim_u_8_strict(
|
||||
Uint8List self,
|
||||
|
|
@ -175,6 +190,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@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);
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
TaskDto dco_decode_box_autoadd_task_dto(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<String> 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<RustLibWire> {
|
|||
@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<RustLibWire> {
|
|||
@protected
|
||||
TaskDto sse_decode_box_autoadd_task_dto(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<String> 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<RustLibWire> {
|
|||
@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<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_box_autoadd_task_dto(TaskDto self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_prim_u_8_strict(
|
||||
Uint8List self,
|
||||
|
|
@ -177,6 +192,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
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(
|
||||
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<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),
|
||||
// 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))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 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<TasksScreen> with SingleTickerProviderStat
|
|||
),
|
||||
),
|
||||
),
|
||||
// Sync status indicator
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: IgnorePointer(
|
||||
child: state.syncing
|
||||
? const SizedBox(
|
||||
width: 20, height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: AppTheme.primary))
|
||||
: state.lastSyncResult != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'↑${state.lastSyncResult!.uploaded} ↓${state.lastSyncResult!.downloaded}',
|
||||
style: TextStyle(fontSize: 11,
|
||||
color: (isDark ? AppTheme.textDark : AppTheme.textLight).withValues(alpha: 0.6)),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ class AppState extends ChangeNotifier {
|
|||
bool darkMode = true;
|
||||
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<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() {
|
||||
darkMode = !darkMode;
|
||||
notifyListeners();
|
||||
|
|
|
|||
|
|
@ -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<DateTimePicker> createState() => _DateTimePickerState();
|
||||
|
|
@ -27,7 +28,7 @@ class _DateTimePickerState extends State<DateTimePicker> {
|
|||
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<DateTimePicker> {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import '../theme.dart';
|
|||
import 'date_time_picker.dart';
|
||||
|
||||
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});
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
|||
final _descController = TextEditingController();
|
||||
final _titleFocus = FocusNode();
|
||||
DateTime? _selectedDate;
|
||||
bool _selectedHasTime = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -34,7 +35,7 @@ class _NewTaskInputState extends State<NewTaskInput> {
|
|||
Future<void> _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<NewTaskInput> {
|
|||
),
|
||||
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<NewTaskInput> {
|
|||
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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class _TaskDetailViewState extends State<TaskDetailView> 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<AppState>();
|
||||
|
|
@ -65,6 +65,7 @@ class _TaskDetailViewState extends State<TaskDetailView> 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<TaskDetailView> with SingleTickerProvid
|
|||
});
|
||||
}
|
||||
|
||||
void _updateDueDate(String? dueDate) {
|
||||
void _updateDueDate(String? dueDate, {bool hasTime = false}) {
|
||||
final state = context.read<AppState>();
|
||||
state.updateTask(api.TaskDto(
|
||||
id: widget.task.id,
|
||||
|
|
@ -80,6 +81,7 @@ class _TaskDetailViewState extends State<TaskDetailView> 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<TaskDetailView> 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<TaskDetailView> 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';
|
||||
}
|
||||
|
|
|
|||
1
apps/flutter/rust/Cargo.lock
generated
1
apps/flutter/rust/Cargo.lock
generated
|
|
@ -1194,6 +1194,7 @@ dependencies = [
|
|||
"notify-debouncer-mini",
|
||||
"once_cell",
|
||||
"onyx-core",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub has_time: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: 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 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<bool, 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 ───────────────────────────────────────────────────
|
||||
|
||||
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
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::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(
|
||||
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::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(
|
||||
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<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> {
|
||||
// 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<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 {
|
||||
// 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 = <String>::sse_decode(deserializer);
|
||||
let mut var_status = <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_updatedAt = <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,
|
||||
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<crate::api::AppConfigDto> 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<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 {
|
||||
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<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> {
|
||||
// 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<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 {
|
||||
// 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 {
|
|||
<String>::sse_encode(self.description, serializer);
|
||||
<String>::sse_encode(self.status, 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.updated_at, serializer);
|
||||
<Option<String>>::sse_encode(self.parent_id, serializer);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
screen_retriever_windows
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
Loading…
Reference in a new issue