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:
Renamr Scaffold
2026-08-29 02:06:14 +00:00
parent 87e150f31b
commit 07ce595f77
7 changed files with 550 additions and 65 deletions
+2
View File
@@ -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"
+7
View File
@@ -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"]
}
+102
View File
@@ -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)
}
+9 -1
View File
@@ -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");
}