//! 画中画/字幕素材:SQLite 元数据 + 本地文件管理 use std::path::{Component, Path, PathBuf}; use chrono::Local; use serde_json::json; use tauri::{AppHandle, Manager, State}; use uuid::Uuid; use crate::commands::fs_util; use crate::material_db::MaterialDb; use crate::material_store::MaterialStore; const ALLOWED_EXTS: &[&str] = &[ "mp4", "mov", "avi", "mkv", "jpg", "jpeg", "png", "gif", "bmp", "webp", ]; fn normalize_path(path: &str) -> PathBuf { Path::new(path) .components() .filter(|c| !matches!(c, Component::ParentDir)) .collect() } fn materials_dir(app: &AppHandle) -> Result { let dir = app .path() .app_data_dir() .map_err(|e| format!("无法获取应用数据目录: {e}"))? .join("video-material"); Ok(dir) } #[tauri::command] pub async fn list_materials(store: State<'_, MaterialStore>) -> Result, String> { store.list().await } #[tauri::command] pub async fn update_material_name( store: State<'_, MaterialStore>, id: i64, name: String, ) -> Result<(), String> { store.update_name(id, name).await } #[tauri::command] pub async fn delete_material(store: State<'_, MaterialStore>, id: i64) -> Result<(), String> { let record = store .get(id) .await? .ok_or_else(|| "素材不存在".to_string())?; fs_util::remove_allowed_local_media_file(&record.path)?; if !store.delete(id).await? { return Err("素材不存在".into()); } Ok(()) } /// 将用户选择的媒体文件复制到应用数据目录并写入数据库 #[tauri::command] pub async fn import_material_files( app: AppHandle, store: State<'_, MaterialStore>, source_paths: Vec, ) -> Result, String> { let mut imported = Vec::new(); for raw in source_paths { let src = normalize_path(raw.trim()); if !src.is_file() { return Err(format!("文件不存在: {}", src.display())); } let ext = src .extension() .and_then(|e| e.to_str()) .unwrap_or("") .to_lowercase(); if !ALLOWED_EXTS.contains(&ext.as_str()) { return Err(format!("不支持的文件格式: .{ext}")); } let saved = copy_to_materials_dir(&app, &src, &ext).await?; let now = chrono::Utc::now().timestamp_millis(); let name = MaterialDb::material_name_from_path(raw.trim()); let material_type = MaterialDb::material_type_from_path(raw.trim()); let info = json!({ "sourcePath": raw.trim() }); let record = store .insert( name, saved, material_type, info, now, now, ) .await?; imported.push(record); } Ok(imported) } async fn copy_to_materials_dir( app: &AppHandle, src: &Path, ext: &str, ) -> Result { let root = materials_dir(app)?; let date = Local::now().format("%Y%m%d").to_string(); let dir = root.join(&date); tokio::fs::create_dir_all(&dir) .await .map_err(|e| format!("创建素材目录失败: {e}"))?; let file_name = format!("{}_{}.{}", Uuid::new_v4(), Local::now().format("%H%M%S"), ext); let dest = dir.join(file_name); tokio::fs::copy(src, &dest) .await .map_err(|e| format!("复制素材失败: {e}"))?; Ok(dest.to_string_lossy().to_string()) }