主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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
+156 -24
View File
@@ -175,7 +175,9 @@ impl SyncDb {
// 添加 task_item 唯一索引(去重:保留 id 最小的记录)
// 先清理重复数据,再建唯一索引
let existing: Vec<String> = conn
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_task_item_unique'")?
.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_task_item_unique'",
)?
.query_map([], |r| r.get(0))?
.filter_map(|r| r.ok())
.collect();
@@ -200,7 +202,8 @@ impl SyncDb {
SyncMode::Full => "full",
SyncMode::UploadOnly => "upload_only",
SyncMode::DownloadOnly => "download_only",
SyncMode::Album => "album",
SyncMode::AlbumUpload => "album_upload",
SyncMode::AlbumDownload => "album_download",
SyncMode::MirrorWcf => "mirror_wcf",
};
@@ -327,12 +330,38 @@ impl SyncDb {
Ok(result)
}
/// 查询所有占位符文件映射(用于 WCF 退出时清理)
#[cfg(feature = "windows-cfapi")]
pub async fn list_placeholder_mappings(
/// 删除指定 remote_uri 的文件映射
pub async fn delete_mapping_by_remote_uri(
&self,
sync_root_id: &str,
) -> Result<Vec<FileMapping>> {
remote_uri: &str,
) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute(
"DELETE FROM file_mapping WHERE sync_root_id = ?1 AND remote_uri = ?2",
rusqlite::params![sync_root_id, remote_uri],
)?;
Ok(())
}
/// 更新文件映射的 remote_uri(重命名/移动时使用)
pub async fn update_mapping_remote_uri(
&self,
sync_root_id: &str,
old_remote_uri: &str,
new_remote_uri: &str,
) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute(
"UPDATE file_mapping SET remote_uri = ?3 WHERE sync_root_id = ?1 AND remote_uri = ?2",
rusqlite::params![sync_root_id, old_remote_uri, new_remote_uri],
)?;
Ok(())
}
/// 查询所有占位符文件映射(用于 WCF 退出时清理)
#[cfg(feature = "windows-cfapi")]
pub async fn list_placeholder_mappings(&self, sync_root_id: &str) -> Result<Vec<FileMapping>> {
let pool = self.read_pool.clone();
let sync_root_id = sync_root_id.to_string();
@@ -345,26 +374,30 @@ impl SyncDb {
FROM file_mapping WHERE sync_root_id = ?1 AND is_placeholder = 1",
)?;
let mappings = stmt.query_map(rusqlite::params![sync_root_id], |row| {
Ok(FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(row.get::<_, String>(2)?),
remote_uri: row.get(3)?,
remote_file_id: row.get(4)?,
local_hash: row.get(5)?,
remote_hash: row.get(6)?,
local_mtime: row.get(7)?,
remote_mtime: row.get(8)?,
local_size: row.get(9)?,
remote_size: row.get(10)?,
sync_status: parse_sync_status(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
})
})?.filter_map(|m| m.ok()).collect();
let mappings = stmt
.query_map(rusqlite::params![sync_root_id], |row| {
Ok(FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(row.get::<_, String>(2)?),
remote_uri: row.get(3)?,
remote_file_id: row.get(4)?,
local_hash: row.get(5)?,
remote_hash: row.get(6)?,
local_mtime: row.get(7)?,
remote_mtime: row.get(8)?,
local_size: row.get(9)?,
remote_size: row.get(10)?,
sync_status: parse_sync_status(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
})
})?
.filter_map(|m| m.ok())
.collect();
Ok(mappings)
}).await??;
})
.await??;
Ok(result)
}
@@ -727,6 +760,46 @@ impl SyncDb {
Ok(conn.last_insert_rowid())
}
/// 创建独立的 task + task_item 记录(用于 WCF 等绕过 WorkerPool 的操作统计)
pub async fn record_standalone_task_item(
&self,
trigger: &WorkerTrigger,
item: &SyncTaskItem,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let tx = conn.unchecked_transaction()?;
tx.execute(
"INSERT INTO sync_task (id, trigger, total_count, completed_count, failed_count, status, created_at, updated_at, finished_at)
VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
trigger.as_str(),
if item.status == TaskItemStatus::Failed { 0 } else { 1 },
if item.status == TaskItemStatus::Failed { 1 } else { 0 },
if item.status == TaskItemStatus::Failed { WorkerStatus::Failed.as_str() } else { WorkerStatus::Completed.as_str() },
item.created_at,
item.updated_at,
item.updated_at,
],
)?;
tx.execute(
"INSERT INTO sync_task_item (task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
item.relative_path,
item.action_type.as_str(),
item.status.as_str(),
item.file_size,
item.error_message,
item.created_at,
item.updated_at,
],
)?;
tx.commit()?;
Ok(())
}
/// 批量插入 task_item(单次持锁,用事务包裹)
pub async fn create_sync_task_items_batch(&self, items: &[SyncTaskItem]) -> Result<()> {
let conn = self.write_conn.lock().await;
@@ -848,6 +921,63 @@ impl SyncDb {
Ok(result)
}
/// 从 DB 聚合累积统计(跨所有同步任务)
/// downloaded 包含 download + hydrationcreate_placeholder 不计入,仅重建映射非实际下载)
pub async fn get_cum_stats(&self) -> Result<SyncCumStats> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(move || -> Result<SyncCumStats> {
let conn = pool.get()?;
let uploaded: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'upload' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let downloaded: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type IN ('download', 'hydration') AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let renamed: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'rename' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let moved: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'move' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let failed: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE status = 'failed'",
[], |r| r.get(0),
).unwrap_or(0);
let conflicts: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'conflict_resolve' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let deleted_local: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'delete_local' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let deleted_remote: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'delete_remote' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let skipped: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE status = 'skipped'",
[], |r| r.get(0),
).unwrap_or(0);
Ok(SyncCumStats { uploaded, downloaded, renamed, moved, failed, conflicts, deleted_local, deleted_remote, skipped })
}).await??;
Ok(result)
}
/// 清空所有同步数据(保留 sync_root 和 album_sync_record
pub async fn reset_sync_data(&self) -> Result<()> {
let conn = self.write_conn.lock().await;
@@ -933,6 +1063,8 @@ fn sync_task_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SyncTask> {
"initial_sync" => WorkerTrigger::InitialSync,
"continuous" => WorkerTrigger::Continuous,
"manual" => WorkerTrigger::Manual,
"hydration" => WorkerTrigger::Hydration,
"wcf_event" => WorkerTrigger::WcfEvent,
_ => WorkerTrigger::Manual,
};
let status_str: String = row.get(5)?;