Add Align Up/Down list reordering and naming-scheme Presets

Align Up/Down now swaps the selected file with its neighbor in the shared
item list (Original Files and New Names stay positionally paired, matching
the original FileBot's "align" buttons for manually lining up mismatched
lists).

Naming-scheme Presets: up to 9 saved expressions, one per digit slot,
persisted as presets.json (src-tauri/src/presets.rs, same load/save
pattern as settings.rs/history.rs). A new Presets panel lists all 9 slots
with Save Current / Apply / Clear, and a global 1-9 keydown handler applies
a slot instantly from anywhere in the app (ignored while a text field is
focused or a modal is open).
This commit is contained in:
Renamr Scaffold
2026-08-29 09:39:52 +00:00
parent 94eb26f778
commit 21d78b8f3a
5 changed files with 376 additions and 13 deletions
+4
View File
@@ -1,6 +1,7 @@
mod files;
mod history;
mod metadata;
mod presets;
mod settings;
use renamr_core::format::ExpressionFormat;
@@ -87,6 +88,9 @@ pub fn run() {
settings::save_tmdb_token,
history::list_rename_history,
history::undo_rename_batch,
presets::list_presets,
presets::save_preset,
presets::delete_preset,
files::pick_files,
files::pick_folder,
files::expand_dropped_paths,
+105
View File
@@ -0,0 +1,105 @@
//! Naming-scheme presets: up to 9 saved expressions, one per digit slot ("1".."9"),
//! recalled with a single keypress in the UI (mirrors the original FileBot's
//! `PresetEditor`/1-9 hotkey behavior in `RenamePanel.java`). Stored as plain JSON in
//! Tauri's per-OS app config directory, same load/save pattern as `settings.rs` and
//! `history.rs` - `presets.json` alongside `settings.json`/`history.json`.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager};
#[derive(Debug, Default, Serialize, Deserialize)]
struct StoredPresets {
#[serde(default)]
slots: HashMap<String, String>,
}
/// A slot key is exactly one ASCII digit `1`-`9` ("0" and multi-character keys are
/// rejected - there are only 9 digit keys to bind hotkeys to, matching the original's
/// 1-9 preset shortcuts).
fn is_valid_slot(slot: &str) -> bool {
matches!(slot.as_bytes(), [b] if (b'1'..=b'9').contains(b))
}
fn presets_path(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_config_dir()
.map_err(|e| format!("couldn't resolve the app config directory: {e}"))?;
fs::create_dir_all(&dir).map_err(|e| format!("couldn't create {}: {e}", dir.display()))?;
Ok(dir.join("presets.json"))
}
fn load(app: &AppHandle) -> StoredPresets {
let Ok(path) = presets_path(app) else {
return StoredPresets::default();
};
fs::read_to_string(path)
.ok()
.and_then(|contents| serde_json::from_str(&contents).ok())
.unwrap_or_default()
}
fn save(app: &AppHandle, presets: &StoredPresets) -> Result<(), String> {
let path = presets_path(app)?;
let json = serde_json::to_string_pretty(presets).map_err(|e| e.to_string())?;
fs::write(&path, json).map_err(|e| format!("couldn't write {}: {e}", path.display()))
}
/// Lists every saved preset slot -> naming-scheme expression. Slots with nothing saved
/// simply aren't present in the map - the frontend renders those as empty.
#[tauri::command]
pub fn list_presets(app: AppHandle) -> HashMap<String, String> {
load(&app).slots
}
/// Saves (or overwrites) the naming-scheme expression at one digit slot.
#[tauri::command]
pub fn save_preset(app: AppHandle, slot: String, expression: String) -> Result<(), String> {
if !is_valid_slot(&slot) {
return Err("Preset slots are the digits 1-9.".to_string());
}
let trimmed = expression.trim();
if trimmed.is_empty() {
return Err("Can't save an empty naming scheme as a preset.".to_string());
}
let mut presets = load(&app);
presets.slots.insert(slot, trimmed.to_string());
save(&app, &presets)
}
/// Clears one digit slot, if anything was saved there.
#[tauri::command]
pub fn delete_preset(app: AppHandle, slot: String) -> Result<(), String> {
if !is_valid_slot(&slot) {
return Err("Preset slots are the digits 1-9.".to_string());
}
let mut presets = load(&app);
presets.slots.remove(&slot);
save(&app, &presets)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_every_digit_one_through_nine() {
for d in '1'..='9' {
assert!(is_valid_slot(&d.to_string()), "slot {d} should be valid");
}
}
#[test]
fn rejects_zero_multi_digit_and_non_digit_slots() {
assert!(!is_valid_slot("0"));
assert!(!is_valid_slot("10"));
assert!(!is_valid_slot(""));
assert!(!is_valid_slot("a"));
assert!(!is_valid_slot("99"));
}
}