123 lines
3.5 KiB
Rust
123 lines
3.5 KiB
Rust
//! 画中画/字幕素材: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<PathBuf, String> {
|
||
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<Vec<crate::material_db::MaterialRecord>, 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<String>,
|
||
) -> Result<Vec<crate::material_db::MaterialRecord>, 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<String, String> {
|
||
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())
|
||
}
|
||
|