This commit is contained in:
fengchuanhn@gmail.com
2026-05-25 01:08:30 +08:00
parent 8293ebf489
commit d4e017bd6d
7 changed files with 339 additions and 46 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -4178,6 +4178,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"dirs 5.0.1",
"dunce",
"env_logger",
"futures-util",
"hex",

View File

@@ -75,6 +75,7 @@ rusqlite = { version = "0.32", features = ["bundled"] }
# Misc
mime_guess = "2"
dirs = "5"
dunce = "1"
rand = "0.8"
tempfile = "3"
uuid = { version = "1", features = ["v4"] }

View File

@@ -74,3 +74,165 @@ pub async fn get_media_duration_seconds(path: String) -> Result<f64, String> {
.await
.map_err(|e| e.to_string())
}
/// 将受信任的本地媒体文件复制到用户指定路径(供「另存为」下载)。
#[tauri::command]
pub async fn copy_local_file(source_path: String, dest_path: String) -> Result<(), String> {
let source = normalize_path(source_path.trim());
if source.as_os_str().is_empty() {
return Err("源路径为空".into());
}
if !source.is_file() {
return Err(format!("源文件不存在: {}", source.display()));
}
if !is_allowed_local_media(&source) {
return Err("不允许复制该路径下的文件".into());
}
let dest = normalize_path(dest_path.trim());
if dest.as_os_str().is_empty() {
return Err("目标路径为空".into());
}
if let Some(parent) = dest.parent() {
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("创建目标目录失败: {e}"))?;
}
}
tokio::fs::copy(&source, &dest)
.await
.map_err(|e| format!("保存文件失败: {e}"))?;
Ok(())
}
/// 将文本写入用户指定路径(供 ASR 结果「另存为」)。
#[tauri::command]
pub async fn write_local_text_file(path: String, content: String) -> Result<(), String> {
let dest = normalize_path(path.trim());
if dest.as_os_str().is_empty() {
return Err("目标路径为空".into());
}
if let Some(parent) = dest.parent() {
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("创建目标目录失败: {e}"))?;
}
}
tokio::fs::write(&dest, content.as_bytes())
.await
.map_err(|e| format!("保存文件失败: {e}"))?;
Ok(())
}
#[cfg(target_os = "windows")]
mod win_shell {
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
pub fn select_file_in_explorer(path: &Path) -> Result<(), String> {
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let _ = ffi::CoInitialize(std::ptr::null());
let pidl = ffi::ILCreateFromPathW(wide.as_ptr());
if pidl.is_null() {
return Err(format!("无法定位文件: {}", path.display()));
}
let hr = ffi::SHOpenFolderAndSelectItems(pidl, 0, std::ptr::null(), 0);
ffi::ILFree(pidl);
if hr != 0 {
return Err(format!("打开文件位置失败 (错误码 {hr})"));
}
}
Ok(())
}
mod ffi {
#[repr(C, packed(1))]
pub struct ITEMIDLIST {
pub mkid: SHITEMID,
}
#[repr(C, packed(1))]
pub struct SHITEMID {
pub cb: u16,
pub ab_id: [u8; 1],
}
#[link(name = "shell32")]
extern "system" {
pub fn ILCreateFromPathW(pszpath: *const u16) -> *mut ITEMIDLIST;
pub fn SHOpenFolderAndSelectItems(
pidlfolder: *const ITEMIDLIST,
cidl: u32,
apidl: *const *const ITEMIDLIST,
dwflags: u32,
) -> i32;
pub fn ILFree(pidl: *const ITEMIDLIST) -> i32;
}
#[link(name = "ole32")]
extern "system" {
pub fn CoInitialize(pvreserved: *const core::ffi::c_void) -> i32;
}
}
}
fn reveal_in_folder_sync(path: &Path) -> Result<(), String> {
let absolute = std::path::absolute(path).map_err(|e| format!("无法解析路径: {e}"))?;
let absolute = dunce::simplified(&absolute);
#[cfg(target_os = "windows")]
{
return win_shell::select_file_in_explorer(&absolute);
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let path_str = absolute.display().to_string();
Command::new("open")
.args(["-R", &path_str])
.spawn()
.map_err(|e| format!("打开文件位置失败: {e}"))?;
return Ok(());
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
use std::process::Command;
let parent = absolute
.parent()
.ok_or_else(|| "无法获取文件所在目录".to_string())?;
Command::new("xdg-open")
.arg(parent)
.spawn()
.map_err(|e| format!("打开文件位置失败: {e}"))?;
Ok(())
}
}
/// 在系统文件管理器中打开并选中本地文件Windows: `explorer /select`)。
#[tauri::command]
pub async fn reveal_local_file_in_folder(path: String) -> Result<(), String> {
let normalized = normalize_path(path.trim());
if normalized.as_os_str().is_empty() {
return Err("路径为空".into());
}
if !normalized.is_file() {
return Err(format!("文件不存在: {}", normalized.display()));
}
if !is_allowed_local_media(&normalized) {
return Err("不允许打开该路径下的文件".into());
}
tokio::task::spawn_blocking(move || reveal_in_folder_sync(&normalized))
.await
.map_err(|e| e.to_string())?
}

View File

@@ -108,6 +108,9 @@ pub fn run() {
commands::nodejs::list_nodejs_scripts,
commands::fs_util::read_local_file_base64,
commands::fs_util::get_media_duration_seconds,
commands::fs_util::copy_local_file,
commands::fs_util::write_local_text_file,
commands::fs_util::reveal_local_file_in_folder,
commands::avatar::list_avatars,
commands::avatar::insert_avatar,
commands::avatar::update_avatar_name,