feat: Library 批量注册 + PeopleView 修复 + 国际化

Library:
- 添加 checkbox 和鼠标框选功能
- Unregistered 卡片添加快速注册按钮
- 状态标签移至左下方,人脸图标移至左侧中间
- Deep Scan 改为 unregister -> register -> process
- 完整国际化(日语、繁体中文、英语)

PeopleView:
- 修复卡片不显示问题(使用 get_file_traces API)
- 移除对 identity_id 字段的依赖

其他:
- 添加日语语言包
- 添加 useDragSelect composable
- 新增文档:Face Bbox Null Bug Report
This commit is contained in:
2026-10-01 00:42:13 +08:00
parent 7b60ca3d2d
commit f3d75ebce8
35 changed files with 8054 additions and 961 deletions

View File

@@ -24,6 +24,16 @@ CREATE TABLE IF NOT EXISTS bookmarks (
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS video_marks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_uuid TEXT NOT NULL,
tag TEXT NOT NULL,
start_frame INTEGER NOT NULL,
end_frame INTEGER,
note TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS app_users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
@@ -36,6 +46,7 @@ CREATE TABLE IF NOT EXISTS app_users (
CREATE INDEX IF NOT EXISTS idx_search_history_sort ON search_history(pinned DESC, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_bookmarks_created ON bookmarks(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_video_marks_file ON video_marks(file_uuid);
";
static DB_CONN: Mutex<Option<Connection>> = Mutex::new(None);
@@ -281,6 +292,75 @@ pub fn delete_bookmark(id: i64) -> Result<(), String> {
Ok(())
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VideoMark {
pub id: i64,
pub file_uuid: String,
pub tag: String,
pub start_frame: i64,
pub end_frame: Option<i64>,
pub note: Option<String>,
pub created_at: Option<String>,
}
#[tauri::command(rename_all = "camelCase")]
pub fn get_video_marks(file_uuid: String) -> Result<Vec<VideoMark>, String> {
let conn = get_conn()?;
let mut stmt = conn
.prepare("SELECT id, file_uuid, tag, start_frame, end_frame, note, created_at FROM video_marks WHERE file_uuid = ?1 ORDER BY start_frame ASC")
.map_err(|e| format!("Prepare error: {}", e))?;
let items = stmt
.query_map([file_uuid], |row| {
Ok(VideoMark {
id: row.get(0)?,
file_uuid: row.get(1)?,
tag: row.get(2)?,
start_frame: row.get(3)?,
end_frame: row.get(4)?,
note: row.get(5)?,
created_at: row.get(6)?,
})
})
.map_err(|e| format!("Query error: {}", e))?
.filter_map(|r| r.ok())
.collect();
Ok(items)
}
#[tauri::command(rename_all = "camelCase")]
pub fn save_video_mark(
file_uuid: String,
tag: String,
start_frame: i64,
end_frame: Option<i64>,
note: Option<String>,
) -> Result<VideoMark, String> {
let conn = get_conn()?;
conn.execute(
"INSERT INTO video_marks (file_uuid, tag, start_frame, end_frame, note) VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![file_uuid, tag, start_frame, end_frame, note],
)
.map_err(|e| format!("Insert error: {}", e))?;
let id = conn.last_insert_rowid();
Ok(VideoMark {
id,
file_uuid,
tag,
start_frame,
end_frame,
note,
created_at: None,
})
}
#[tauri::command(rename_all = "camelCase")]
pub fn delete_video_mark(id: i64) -> Result<(), String> {
let conn = get_conn()?;
conn.execute("DELETE FROM video_marks WHERE id = ?1", [id])
.map_err(|e| format!("Delete error: {}", e))?;
Ok(())
}
pub fn verify_user(username: &str, password: &str) -> Result<Option<User>, String> {
let conn = get_conn()?;
let mut stmt = conn