Add real file loading: native file/folder picker + OS drag-and-drop, backed by real Rust commands (pick_files/pick_folder/expand_dropped_paths)
This commit is contained in:
@@ -14,5 +14,7 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
renamr-core = { path = "../crates/renamr-core" }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
walkdir = "2"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for Renamr's main window.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "dialog:default"]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FileEntry {
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
fn is_hidden(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn to_file_entry(path: &Path) -> FileEntry {
|
||||
let name = path
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||
let ext = path
|
||||
.extension()
|
||||
.map(|s| s.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
FileEntry {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
name,
|
||||
ext,
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands a list of dropped/selected paths into the individual files they contain.
|
||||
/// Directories are walked recursively (this is how a user dropping a whole show/season
|
||||
/// folder ends up with every episode file listed); hidden files/dirs are skipped.
|
||||
fn expand(paths: &[String]) -> Vec<FileEntry> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
for raw in paths {
|
||||
let path = Path::new(raw);
|
||||
|
||||
if path.is_dir() {
|
||||
for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_entry(|e| !is_hidden(e.path()) || e.path() == path)
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
if entry.file_type().is_file() {
|
||||
let full = entry.path().to_string_lossy().to_string();
|
||||
if seen.insert(full) {
|
||||
out.push(to_file_entry(entry.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if path.is_file() {
|
||||
let full = path.to_string_lossy().to_string();
|
||||
if seen.insert(full) {
|
||||
out.push(to_file_entry(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
out
|
||||
}
|
||||
|
||||
/// Native "Select Files" dialog, mirroring the original's Load Files button.
|
||||
#[tauri::command]
|
||||
pub fn pick_files(app: AppHandle) -> Vec<FileEntry> {
|
||||
let selection = app.dialog().file().blocking_pick_files();
|
||||
let paths: Vec<String> = selection
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|f| f.into_path().ok())
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect();
|
||||
expand(&paths)
|
||||
}
|
||||
|
||||
/// Native "Select Folders" dialog; the folder's contents are expanded recursively.
|
||||
#[tauri::command]
|
||||
pub fn pick_folder(app: AppHandle) -> Vec<FileEntry> {
|
||||
let selection = app.dialog().file().blocking_pick_folder();
|
||||
let paths: Vec<String> = selection
|
||||
.into_iter()
|
||||
.filter_map(|f| f.into_path().ok())
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect();
|
||||
expand(&paths)
|
||||
}
|
||||
|
||||
/// Expands paths dropped onto the window (files and/or folders) into their files.
|
||||
#[tauri::command]
|
||||
pub fn expand_dropped_paths(paths: Vec<String>) -> Vec<FileEntry> {
|
||||
expand(&paths)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod files;
|
||||
|
||||
use renamr_core::format::ExpressionFormat;
|
||||
use renamr_core::string_bindings;
|
||||
use std::collections::HashMap;
|
||||
@@ -16,7 +18,13 @@ fn render_expression(expression: String, bindings: HashMap<String, String>) -> R
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![render_expression])
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
render_expression,
|
||||
files::pick_files,
|
||||
files::pick_folder,
|
||||
files::expand_dropped_paths,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Renamr");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user