优化调整

This commit is contained in:
zhongluofeng
2026-08-14 17:47:06 +08:00
parent 6c7897bf47
commit 7d49a7395f
50 changed files with 2805 additions and 259 deletions
+344
View File
@@ -0,0 +1,344 @@
//! 快速面板:基于 7-Zipbinaries/7z.exe)的批量文件操作。
//!
//! - 批量解压:逐文件调用 7z 控制台,支持一次传入统一密码(`-p`),
//! 每完成一个文件通过事件推送进度,前端可实时展示。
//! - 批量重命名:`regex` 匹配文件名生成预览,确认后执行 `fs::rename`。
//! - 目录列表:供前端展示当前目录的压缩包 / 文件列表。
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{AppHandle, Emitter, Manager};
use crate::constants::events::QUICKPANEL_EXTRACT_PROGRESS;
/// 7z 可执行文件名(与 7z.dll 同目录,位于 resources/binaries)。
const _7Z_EXE: &str = "binaries/7z.exe";
/// 支持的压缩包扩展名(解压面板中列出)。
const ARCHIVE_EXTS: &[&str] = &[
".zip", ".7z", ".rar", ".tar", ".gz", ".tgz", ".bz2", ".tbz", ".xz", ".txz", ".zst",
".tzst", ".cab", ".lzma",
];
/// 定位 7z 可执行文件(resources/binaries/7z.exe)。
fn locate_7z(app: &AppHandle) -> Option<PathBuf> {
let path = app
.path()
.resolve(_7Z_EXE, tauri::path::BaseDirectory::Resource)
.ok()?;
if path.exists() {
Some(path)
} else {
None
}
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveInfo {
pub name: String,
pub path: String,
pub size: u64,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FileEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ExtractResult {
pub name: String,
pub path: String,
pub ok: bool,
pub error: String,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ExtractProgress {
pub done: usize,
pub total: usize,
pub current: String,
pub ok: bool,
pub error: String,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RenamePreview {
pub path: String,
pub old_name: String,
pub new_name: String,
pub error: String,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RenameItem {
pub path: String,
pub old_name: String,
pub new_name: String,
}
#[derive(Serialize, Deserialize, Type, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RenameResult {
pub old_name: String,
pub new_name: String,
pub ok: bool,
pub error: String,
}
fn is_archive(name: &str) -> bool {
let lower = name.to_lowercase();
ARCHIVE_EXTS.iter().any(|ext| lower.ends_with(ext))
}
/// 列出目录下的压缩包文件(供批量解压面板使用)。
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_list_archives(dir: String) -> Result<Vec<ArchiveInfo>, String> {
tauri::async_runtime::spawn_blocking(move || {
let d = Path::new(&dir);
if !d.is_dir() {
return Err(format!("目录不存在: {dir}"));
}
let mut out = Vec::new();
for entry in std::fs::read_dir(d).map_err(|e| format!("读取目录失败: {e}"))? {
let Ok(entry) = entry else { continue };
let path = entry.path();
if !path.is_file() {
continue;
}
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if is_archive(&name) {
let size = path.metadata().map(|m| m.len()).unwrap_or(0);
out.push(ArchiveInfo {
name,
path: path.to_string_lossy().to_string(),
size,
});
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
})
.await
.map_err(|e| format!("读取压缩包失败: {e}"))?
}
/// 列出目录下的全部条目(供批量重命名/删除面板使用,不含子目录递归)。
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_list_dir(dir: String) -> Result<Vec<FileEntry>, String> {
tauri::async_runtime::spawn_blocking(move || {
let d = Path::new(&dir);
if !d.is_dir() {
return Err(format!("目录不存在: {dir}"));
}
let mut out = Vec::new();
for entry in std::fs::read_dir(d).map_err(|e| format!("读取目录失败: {e}"))? {
let Ok(entry) = entry else { continue };
let path = entry.path();
let is_dir = path.is_dir();
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let size = if is_dir {
0
} else {
path.metadata().map(|m| m.len()).unwrap_or(0)
};
out.push(FileEntry {
name,
path: path.to_string_lossy().to_string(),
is_dir,
size,
});
}
out.sort_by(|a, b| {
b.is_dir
.cmp(&a.is_dir)
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
Ok(out)
})
.await
.map_err(|e| format!("读取目录失败: {e}"))?
}
fn extract_one(
exe: &Path,
archive: &str,
dest: &str,
password: Option<&str>,
into_subfolder: bool,
) -> Result<(), String> {
let archive_path = Path::new(archive);
let mut cmd = Command::new(exe);
cmd.arg("x").arg(archive_path);
if into_subfolder {
// 解压到「压缩包同名子文件夹」,避免文件散落在当前目录。
let sub = archive_path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "extracted".into());
let sub_dir = Path::new(dest).join(&sub);
std::fs::create_dir_all(&sub_dir).map_err(|e| format!("创建目录 {sub} 失败: {e}"))?;
cmd.arg(format!("-o{}", sub_dir.display()));
} else {
cmd.arg(format!("-o{}", dest));
}
cmd.arg("-y"); // 全自动覆盖确认
if let Some(p) = password.filter(|p| !p.is_empty()) {
cmd.arg(format!("-p{p}"));
}
// 静默普通输出,仅错误进 stderr,逐文件粒度足够时无需 -bsp1 进度。
cmd.arg("-bso0").arg("-bse1").arg("-bsp0");
// 以 7z.exe 所在目录为工作目录,确保同目录的 7z.dll 可被加载。
if let Some(dir) = exe.parent() {
cmd.current_dir(dir);
}
let output = cmd.output().map_err(|e| format!("启动 7-Zip 失败: {e}"))?;
if output.status.success() {
Ok(())
} else {
let msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
let code = output.status.code().unwrap_or(-1);
Err(if msg.is_empty() {
format!("退出码 {code}")
} else {
msg
})
}
}
/// 批量解压。`files` 为压缩包路径列表,`dest_dir` 为目标目录,
/// `password` 为统一解压密码(可空),`into_subfolder` 是否解压到同名子文件夹。
/// 每完成一个文件通过 `quickpanel-extract-progress` 事件推送进度。
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_batch_extract(
app: AppHandle,
files: Vec<String>,
dest_dir: String,
password: Option<String>,
into_subfolder: bool,
) -> Result<Vec<ExtractResult>, String> {
let exe = locate_7z(&app).ok_or("未找到 7-Zip 组件(binaries/7z.exe),请重新安装")?;
let total = files.len();
let results = tauri::async_runtime::spawn_blocking(move || {
let mut results = Vec::with_capacity(total);
for (idx, file) in files.iter().enumerate() {
let err = extract_one(&exe, file, &dest_dir, password.as_deref(), into_subfolder);
let name = Path::new(file)
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| file.clone());
let (ok, error) = match err {
Ok(()) => (true, String::new()),
Err(e) => (false, e),
};
results.push(ExtractResult {
name,
path: file.clone(),
ok,
error: error.clone(),
});
let _ = app.emit(
QUICKPANEL_EXTRACT_PROGRESS,
ExtractProgress {
done: idx + 1,
total,
current: results[idx].name.clone(),
ok,
error,
},
);
}
results
})
.await
.map_err(|e| format!("解压任务异常终止: {e}"))?;
Ok(results)
}
/// 正则批量重命名预览:对每个文件名应用 `pattern → replacement`
/// 仅返回有匹配的文件,`newName` 为替换结果。
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_preview_rename(
files: Vec<String>,
pattern: String,
replacement: String,
) -> Result<Vec<RenamePreview>, String> {
let re = regex::Regex::new(&pattern).map_err(|e| format!("正则表达式无效: {e}"))?;
let mut out = Vec::new();
for file in files {
let name = Path::new(&file)
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if !re.is_match(&name) {
continue;
}
let new_name = re.replace_all(&name, replacement.as_str()).to_string();
let error = if new_name.is_empty() || new_name == name {
"名称未变化".to_string()
} else {
String::new()
};
out.push(RenamePreview {
path: file,
old_name: name,
new_name,
error,
});
}
Ok(out)
}
/// 执行重命名。同一目录下若目标已存在则跳过该项。
#[tauri::command]
#[specta::specta]
pub async fn quickpanel_apply_rename(items: Vec<RenameItem>) -> Result<Vec<RenameResult>, String> {
let mut out = Vec::new();
for item in items {
let result = (|| -> Result<(), String> {
if item.new_name.is_empty() {
return Err("新文件名为空".into());
}
if item.new_name.contains(['/', '\\', ':', '*', '?', '"', '<', '>', '|']) {
return Err("文件名包含非法字符".into());
}
let old = Path::new(&item.path);
let parent = old.parent().unwrap_or(Path::new("."));
let new_path = parent.join(&item.new_name);
if new_path.exists() {
return Err("目标已存在".into());
}
std::fs::rename(old, &new_path).map_err(|e| format!("重命名失败: {e}"))?;
Ok(())
})();
out.push(RenameResult {
old_name: item.old_name,
new_name: item.new_name,
ok: result.is_ok(),
error: result.err().unwrap_or_default(),
});
}
Ok(out)
}