主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、Ctrl/Cmd+F搜索、Esc 关闭搜索、鼠标右滑返回上级。
- 移动端同步状态页、同步统计卡片、同步任务/累计统计事件、同步页布局更新。
- Android 相册同步默认路径改为 DCIM/Camera,下载路径改用 external_path。
- Rust sync-core 更新:Android 日志依赖、AlbumUpload/AlbumDownload、MirrorWcf 统计修复、Linux FUSE镜像挂载与读写同步支持。

- 依赖更新:external_path、fl_chart、pdfrx 升级,并修了 pdfrx 新版本的deprecated API。
- 新增文件包括:sort_options.dart、sync_page_android.dart、sync_stats_card.dart、platform/fuse.rs、sync_engine/fuse.rs。
This commit is contained in:
2026-06-04 07:11:43 +08:00
parent 5ee6ba1e28
commit b02daf1448
56 changed files with 7589 additions and 1200 deletions
+141 -60
View File
@@ -14,6 +14,7 @@ class SyncService {
SyncService._();
bool _initialized = false;
StreamSubscription<ffi_types.SyncEventFfi>? _rustEventSub;
/// 事件流,供 SyncProvider 订阅
final _eventController = StreamController<SyncEventModel>.broadcast();
@@ -22,17 +23,22 @@ class SyncService {
/// 初始化同步引擎(已初始化时更新配置)
Future<void> init(SyncConfigModel config) async {
if (_initialized) {
AppLogger.d('[FFI] → updateSyncConfig: mode=${config.syncMode}, conflict=${config.conflictStrategy}, concurrent=${config.maxConcurrentTransfers}, bandwidth=${config.bandwidthLimitKbps}kbps');
AppLogger.d(
'[FFI] → updateSyncConfig: mode=${config.syncMode}, conflict=${config.conflictStrategy}, concurrent=${config.maxConcurrentTransfers}, bandwidth=${config.bandwidthLimitKbps}kbps',
);
await ffi.updateSyncConfig(config: config.toFfi());
AppLogger.d('[FFI] ← updateSyncConfig: ok');
return;
}
AppLogger.d('[FFI] → initSyncEngine: localRoot=${config.localRoot}, mode=${config.syncMode}, conflict=${config.conflictStrategy}, logLevel=${config.logLevel}');
AppLogger.d(
'[FFI] → initSyncEngine: localRoot=${config.localRoot}, mode=${config.syncMode}, conflict=${config.conflictStrategy}, logLevel=${config.logLevel}',
);
await ffi.initSyncEngine(config: config.toFfi());
_initialized = true;
_subscribeRustEvents();
AppLogger.d('[FFI] ← initSyncEngine: ok');
}
@@ -40,7 +46,9 @@ class SyncService {
Future<SyncSummaryModel> startInitialSync() async {
AppLogger.d('[FFI] → startInitialSync');
final summary = await ffi.startInitialSync();
AppLogger.d('[FFI] ← startInitialSync: uploaded=${summary.uploaded}, downloaded=${summary.downloaded}, conflicts=${summary.conflicts}, failed=${summary.failed}');
AppLogger.d(
'[FFI] ← startInitialSync: uploaded=${summary.uploaded}, downloaded=${summary.downloaded}, conflicts=${summary.conflicts}, failed=${summary.failed}',
);
return SyncSummaryModel.fromFfi(summary);
}
@@ -76,7 +84,9 @@ class SyncService {
Future<SyncSummaryModel> forceSync() async {
AppLogger.d('[FFI] → forceSync');
final summary = await ffi.forceSync();
AppLogger.d('[FFI] ← forceSync: uploaded=${summary.uploaded}, downloaded=${summary.downloaded}, conflicts=${summary.conflicts}, failed=${summary.failed}');
AppLogger.d(
'[FFI] ← forceSync: uploaded=${summary.uploaded}, downloaded=${summary.downloaded}, conflicts=${summary.conflicts}, failed=${summary.failed}',
);
return SyncSummaryModel.fromFfi(summary);
}
@@ -90,7 +100,9 @@ class SyncService {
/// 更新同步配置(推送到 Rust 引擎,引擎未初始化时忽略)
Future<void> updateConfig(SyncConfigModel config) async {
if (!_initialized) return;
AppLogger.d('[FFI] → updateSyncConfig: mode=${config.syncMode}, conflict=${config.conflictStrategy}, concurrent=${config.maxConcurrentTransfers}, bandwidth=${config.bandwidthLimitKbps}kbps');
AppLogger.d(
'[FFI] → updateSyncConfig: mode=${config.syncMode}, conflict=${config.conflictStrategy}, concurrent=${config.maxConcurrentTransfers}, bandwidth=${config.bandwidthLimitKbps}kbps',
);
try {
await ffi.updateSyncConfig(config: config.toFfi());
AppLogger.d('[FFI] ← updateSyncConfig: ok');
@@ -104,7 +116,9 @@ class SyncService {
/// 获取同步状态快照(轮询高频调用,trace 级别)
Future<SyncStatusModel> getStatus() async {
final status = await ffi.getSyncStatus();
AppLogger.t('[FFI] ← getSyncStatus: state=${status.state}, synced=${status.syncedFiles}, total=${status.totalFiles}');
AppLogger.t(
'[FFI] ← getSyncStatus: state=${status.state}, synced=${status.syncedFiles}, total=${status.totalFiles}',
);
return SyncStatusModel.fromFfi(status);
}
@@ -119,17 +133,21 @@ class SyncService {
Future<List<SyncTaskModel>> getActiveTasksTyped() async {
final tasks = await ffi.getActiveTasks();
AppLogger.t('[FFI] ← getActiveTasks: count=${tasks.length}');
return tasks.map((t) => SyncTaskModel(
id: t.id,
trigger: t.trigger,
totalCount: t.totalCount,
completedCount: t.completedCount,
failedCount: t.failedCount,
status: t.status,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
finishedAt: t.finishedAt,
)).toList();
return tasks
.map(
(t) => SyncTaskModel(
id: t.id,
trigger: t.trigger,
totalCount: t.totalCount,
completedCount: t.completedCount,
failedCount: t.failedCount,
status: t.status,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
finishedAt: t.finishedAt,
),
)
.toList();
}
/// 获取最近同步任务列表(轮询高频调用,trace 级别)
@@ -137,17 +155,21 @@ class SyncService {
AppLogger.t('[FFI] → getRecentTasks: limit=$limit');
final tasks = await ffi.getRecentTasks(limit: limit);
AppLogger.t('[FFI] ← getRecentTasks: count=${tasks.length}');
return tasks.map((t) => SyncTaskModel(
id: t.id,
trigger: t.trigger,
totalCount: t.totalCount,
completedCount: t.completedCount,
failedCount: t.failedCount,
status: t.status,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
finishedAt: t.finishedAt,
)).toList();
return tasks
.map(
(t) => SyncTaskModel(
id: t.id,
trigger: t.trigger,
totalCount: t.totalCount,
completedCount: t.completedCount,
failedCount: t.failedCount,
status: t.status,
createdAt: t.createdAt,
updatedAt: t.updatedAt,
finishedAt: t.finishedAt,
),
)
.toList();
}
/// 获取任务详情(按需查询,trace 级别)
@@ -155,17 +177,21 @@ class SyncService {
AppLogger.t('[FFI] → getTaskDetail: taskId=$taskId');
final items = await ffi.getTaskDetail(taskId: taskId);
AppLogger.t('[FFI] ← getTaskDetail: count=${items.length}');
return items.map((i) => SyncTaskItemModel(
id: i.id.toInt(),
taskId: i.taskId,
relativePath: i.relativePath,
actionType: i.actionType,
status: i.status,
fileSize: i.fileSize.toInt(),
errorMessage: i.errorMessage,
createdAt: i.createdAt,
updatedAt: i.updatedAt,
)).toList();
return items
.map(
(i) => SyncTaskItemModel(
id: i.id.toInt(),
taskId: i.taskId,
relativePath: i.relativePath,
actionType: i.actionType,
status: i.status,
fileSize: i.fileSize.toInt(),
errorMessage: i.errorMessage,
createdAt: i.createdAt,
updatedAt: i.updatedAt,
),
)
.toList();
}
/// 分页查询任务详情(trace 级别)
@@ -174,24 +200,52 @@ class SyncService {
int limit = 20,
int offset = 0,
}) async {
AppLogger.t('[FFI] → queryTaskItems: taskId=$taskId, limit=$limit, offset=$offset');
final items = await ffi.queryTaskItems(filter: ffi_types.TaskItemFilterFfi(
taskId: taskId,
limit: limit,
offset: offset,
));
AppLogger.t(
'[FFI] → queryTaskItems: taskId=$taskId, limit=$limit, offset=$offset',
);
final items = await ffi.queryTaskItems(
filter: ffi_types.TaskItemFilterFfi(
taskId: taskId,
limit: limit,
offset: offset,
),
);
AppLogger.t('[FFI] ← queryTaskItems: count=${items.length}');
return items.map((i) => SyncTaskItemModel(
id: i.id.toInt(),
taskId: i.taskId,
relativePath: i.relativePath,
actionType: i.actionType,
status: i.status,
fileSize: i.fileSize.toInt(),
errorMessage: i.errorMessage,
createdAt: i.createdAt,
updatedAt: i.updatedAt,
)).toList();
return items
.map(
(i) => SyncTaskItemModel(
id: i.id.toInt(),
taskId: i.taskId,
relativePath: i.relativePath,
actionType: i.actionType,
status: i.status,
fileSize: i.fileSize.toInt(),
errorMessage: i.errorMessage,
createdAt: i.createdAt,
updatedAt: i.updatedAt,
),
)
.toList();
}
/// 从 DB 聚合累积统计(轮询高频调用,trace 级别)
Future<Map<String, int>> getCumStats() async {
AppLogger.t('[FFI] → getSyncCumStats');
final stats = await ffi.getSyncCumStats();
AppLogger.t(
'[FFI] ← getSyncCumStats: uploaded=${stats.uploaded}, downloaded=${stats.downloaded}, failed=${stats.failed}, conflicts=${stats.conflicts}, deletedLocal=${stats.deletedLocal}, deletedRemote=${stats.deletedRemote}, skipped=${stats.skipped}',
);
return {
'uploaded': stats.uploaded,
'downloaded': stats.downloaded,
'renamed': stats.renamed,
'moved': stats.moved,
'failed': stats.failed,
'conflicts': stats.conflicts,
'deleted_local': stats.deletedLocal,
'deleted_remote': stats.deletedRemote,
'skipped': stats.skipped,
};
}
// ========== 以下为低频操作,保持 debug 级别 ==========
@@ -207,12 +261,16 @@ class SyncService {
Future<Map<String, dynamic>> checkCloudAlbumDirs(String baseUri) async {
AppLogger.d('[FFI] → checkCloudAlbumDirs: uri=$baseUri');
final result = await ffi.checkCloudAlbumDirs(baseUri: baseUri);
AppLogger.d('[FFI] ← checkCloudAlbumDirs: dcim=${result.dcimExists}, pictures=${result.picturesExists}');
AppLogger.d(
'[FFI] ← checkCloudAlbumDirs: dcim=${result.dcimExists}, pictures=${result.picturesExists}, camera=${result.cameraExists}',
);
return {
'dcimExists': result.dcimExists,
'picturesExists': result.picturesExists,
'dcimUri': result.dcimUri,
'picturesUri': result.picturesUri,
'cameraExists': result.cameraExists,
'cameraUri': result.cameraUri,
};
}
@@ -226,12 +284,35 @@ class SyncService {
/// 销毁同步引擎
Future<void> dispose() async {
AppLogger.d('[FFI] → disposeSyncEngine');
_rustEventSub?.cancel();
_rustEventSub = null;
await _eventController.close();
await ffi.disposeSyncEngine();
_initialized = false;
AppLogger.d('[FFI] ← disposeSyncEngine: ok');
}
/// 订阅 Rust 事件流,转换后转发到 _eventController
void _subscribeRustEvents() {
_rustEventSub?.cancel();
try {
final stream = ffi.registerSyncEventSink();
_rustEventSub = stream.listen(
(event) {
final model = syncEventFromFfi(event);
if (model != null && !_eventController.isClosed) {
_eventController.add(model);
}
},
onError: (e) => AppLogger.e('[FFI] Rust event stream error: $e'),
onDone: () => AppLogger.d('[FFI] Rust event stream done'),
);
AppLogger.d('[FFI] Rust event stream subscribed');
} catch (e) {
AppLogger.e('[FFI] Failed to subscribe Rust event stream: $e');
}
}
/// 热修改日志级别(立即生效,无需重启)
Future<void> setLogLevel(String level) async {
AppLogger.d('[FFI] → setSyncLogLevel: level=$level');
@@ -240,9 +321,9 @@ class SyncService {
}
/// 重置同步:停止任务 → 清空 DB → 清空本地目录 → 回到初始状态
Future<void> resetSync() async {
AppLogger.d('[FFI] → resetSync');
await ffi.resetSync();
Future<void> resetSync({bool deleteLocalFiles = true}) async {
AppLogger.d('[FFI] → resetSync: deleteLocalFiles=$deleteLocalFiles');
await ffi.resetSync(deleteLocalFiles: deleteLocalFiles);
AppLogger.d('[FFI] ← resetSync: ok');
}
}