Add rename history with undo

- New crates/renamr-core/src/history module: HistoryBatch/HistoryEntry
  records, revert_batch() moves files back to their original paths
  (skip-on-conflict, same policy as rename::execute_renames; recreates a
  deleted destination folder defensively), push_batch() maintains a
  most-recent-first log capped at 50 batches. A partial port of the idea
  behind net.filebot.HistoryDialog, as a flat undo stack rather than a full
  session/element log. 6 new tests.
- New src-tauri/src/history.rs: persists the log as history.json in the OS
  app-config directory (same location as settings.json). execute_rename now
  takes an AppHandle and calls history::record_batch after every rename,
  recording only the items that actually got renamed. Two new commands:
  list_rename_history and undo_rename_batch.
- Frontend: the History button (previously a no-op) now opens a panel
  listing past rename batches with a timestamp, file count, and the
  old-name/new-name pairs, each with an Undo button. Undoing reconciles any
  currently-loaded item back to its pre-rename path/name, same pattern as
  post-rename reconciliation.
- Verified via cargo test (45 total)/check + npm build + a full Playwright
  pass: loaded mocked files, renamed them, opened History, undid the batch,
  and confirmed both the UI and the (mocked) backend state matched at every
  step.
This commit is contained in:
Renamr Scaffold
2026-08-29 03:44:38 +00:00
parent 658a235e05
commit 93b5ac5e56
7 changed files with 643 additions and 28 deletions
+16 -7
View File
@@ -1,4 +1,5 @@
mod files;
mod history;
mod metadata;
mod settings;
@@ -7,6 +8,7 @@ use renamr_core::matching::{self, EpisodeGuess};
use renamr_core::rename::{self, RenamePlanItem, RenameResult};
use renamr_core::string_bindings;
use std::collections::HashMap;
use tauri::AppHandle;
/// Renders a naming-scheme expression against a set of binding values.
/// This is the Tauri-facing entry point into the core format engine, used by the
@@ -28,16 +30,21 @@ fn guess_episode(filename: String) -> Option<EpisodeGuess> {
matching::guess_episode(&filename)
}
/// Executes a batch of planned on-disk renames (same-directory rename only; conflicting
/// targets are skipped, never overwritten - see `renamr_core::rename` for the policy).
/// The UI is expected to have already shown the user the full old-name/new-name plan
/// and gotten explicit confirmation before calling this - there is no undo yet.
/// Executes a batch of planned on-disk renames (optionally moving files into
/// subfolders - see `renamr_core::rename`; conflicting targets are always skipped,
/// never overwritten). The UI is expected to have already shown the user the full
/// old-path/new-path plan and gotten explicit confirmation before calling this.
/// Runs off the main thread since real filesystem I/O shouldn't block the UI.
///
/// Every successfully-renamed item is recorded to the rename history log afterward
/// (see `history::record_batch`) so it can be undone later from the History panel.
#[tauri::command]
async fn execute_rename(items: Vec<RenamePlanItem>) -> Vec<RenameResult> {
tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items))
async fn execute_rename(app: AppHandle, items: Vec<RenamePlanItem>) -> Vec<RenameResult> {
let results = tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items))
.await
.unwrap_or_default()
.unwrap_or_default();
history::record_batch(&app, &results);
results
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -51,6 +58,8 @@ pub fn run() {
metadata::fetch_metadata,
settings::get_tmdb_token,
settings::save_tmdb_token,
history::list_rename_history,
history::undo_rename_batch,
files::pick_files,
files::pick_folder,
files::expand_dropped_paths,