exchange tast to tactilea

This commit is contained in:
lennlouisgeek
2026-04-03 00:47:36 +08:00
parent a686d19e61
commit 7688986ad7
15 changed files with 1842 additions and 147 deletions

View File

@@ -0,0 +1,208 @@
use serde::Serialize;
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use tauri::{AppHandle, Manager};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileExplorerRoot {
pub label: String,
pub path: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileExplorerEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size_bytes: Option<u64>,
pub modified_ms: Option<u128>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileExplorerListResponse {
pub current_path: String,
pub parent_path: Option<String>,
pub roots: Vec<FileExplorerRoot>,
pub entries: Vec<FileExplorerEntry>,
}
#[tauri::command]
pub fn file_explorer_list(
app: AppHandle,
path: Option<String>,
extensions: Option<Vec<String>>,
) -> Result<FileExplorerListResponse, String> {
let current_path = resolve_start_path(&app, path)?;
let extension_filter = normalize_extensions(extensions);
let mut entries = fs::read_dir(&current_path)
.map_err(|err| format!("Failed to read '{}': {err}", current_path.display()))?
.filter_map(Result::ok)
.filter_map(|entry| {
let file_type = entry.file_type().ok()?;
let metadata = entry.metadata().ok();
let is_dir = file_type.is_dir();
let path = entry.path();
if !is_dir && !extension_filter.is_empty() {
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
if !extension_filter.contains(&extension) {
return None;
}
}
let name = entry.file_name().to_string_lossy().to_string();
let size_bytes = if is_dir {
None
} else {
metadata.as_ref().map(|value| value.len())
};
let modified_ms = metadata
.as_ref()
.and_then(|value| value.modified().ok())
.and_then(|value| value.duration_since(UNIX_EPOCH).ok())
.map(|value| value.as_millis());
Some(FileExplorerEntry {
name,
path: path.display().to_string(),
is_dir,
size_bytes,
modified_ms,
})
})
.collect::<Vec<_>>();
entries.sort_by(|left, right| {
if left.is_dir != right.is_dir {
return right.is_dir.cmp(&left.is_dir);
}
left.name
.to_ascii_lowercase()
.cmp(&right.name.to_ascii_lowercase())
});
Ok(FileExplorerListResponse {
current_path: current_path.display().to_string(),
parent_path: current_path.parent().map(|parent| parent.display().to_string()),
roots: collect_roots(&app),
entries,
})
}
fn normalize_extensions(extensions: Option<Vec<String>>) -> HashSet<String> {
extensions
.unwrap_or_default()
.into_iter()
.map(|value| value.trim().trim_start_matches('.').to_ascii_lowercase())
.filter(|value| !value.is_empty())
.collect()
}
fn resolve_start_path(app: &AppHandle, raw_path: Option<String>) -> Result<PathBuf, String> {
if let Some(value) = raw_path {
let trimmed = value.trim();
if trimmed.is_empty() {
return resolve_default_path(app);
}
let mut candidate = PathBuf::from(trimmed);
if candidate.is_relative() {
candidate = std::env::current_dir()
.map_err(|err| format!("Failed to read current dir: {err}"))?
.join(candidate);
}
if !candidate.exists() {
return Err(format!("Path does not exist: {}", candidate.display()));
}
if candidate.is_file() {
return candidate
.parent()
.map(|parent| parent.to_path_buf())
.ok_or_else(|| format!("No parent directory for {}", candidate.display()));
}
return Ok(candidate);
}
resolve_default_path(app)
}
fn resolve_default_path(app: &AppHandle) -> Result<PathBuf, String> {
if let Ok(path) = app.path().desktop_dir() {
return Ok(path);
}
if let Ok(path) = app.path().document_dir() {
return Ok(path);
}
if let Ok(path) = app.path().download_dir() {
return Ok(path);
}
if let Ok(path) = app.path().home_dir() {
return Ok(path);
}
std::env::current_dir().map_err(|err| format!("Failed to resolve default path: {err}"))
}
fn collect_roots(app: &AppHandle) -> Vec<FileExplorerRoot> {
let mut roots = Vec::new();
let mut seen = HashSet::new();
let mut push_root = |label: &str, path: PathBuf| {
let normalized = path.display().to_string();
if normalized.is_empty() || !Path::new(&normalized).exists() {
return;
}
if seen.insert(normalized.clone()) {
roots.push(FileExplorerRoot {
label: label.to_string(),
path: normalized,
});
}
};
if let Ok(path) = app.path().desktop_dir() {
push_root("Desktop", path);
}
if let Ok(path) = app.path().document_dir() {
push_root("Documents", path);
}
if let Ok(path) = app.path().download_dir() {
push_root("Downloads", path);
}
if let Ok(path) = app.path().home_dir() {
push_root("Home", path);
}
#[cfg(target_os = "windows")]
{
for letter in b'A'..=b'Z' {
let drive = format!("{}:\\", letter as char);
let drive_path = PathBuf::from(&drive);
if drive_path.exists() {
push_root(&format!("{}:", letter as char), drive_path);
}
}
}
#[cfg(not(target_os = "windows"))]
{
push_root("Root", PathBuf::from("/"));
}
roots
}