Add conflict policy options: Skip / Auto-number / Override

New ConflictPolicy enum in rename/ (Skip = original/default behavior,
AutoNumber = save as "Name (1).ext" counting up to the first free slot,
Override = replace the conflicting file outright), threaded through
execute_renames/rename_one alongside RenameAction. Override never deletes
a conflicting directory (falls back to Skip for that case); AutoNumber
sidesteps directories the same way by just trying the next number.
execute_rename now takes a conflict argument; undo/history are
deliberately NOT conflict-aware and always use their own fixed
skip-on-conflict policy regardless of what the original rename used.

Frontend: a second segmented selector in the rename confirmation modal
("On conflict: Skip / Auto-number / Override"), with Override styled in
red and its warning copy calling out that it can't be undone.
This commit is contained in:
Renamr Scaffold
2026-08-29 09:50:36 +00:00
parent 21d78b8f3a
commit 7dd9ad2078
5 changed files with 449 additions and 71 deletions
+16 -9
View File
@@ -6,7 +6,7 @@ mod settings;
use renamr_core::format::ExpressionFormat;
use renamr_core::matching::{self, EpisodeGuess, MovieGuess, ReleaseInfo};
use renamr_core::rename::{self, RenameAction, RenamePlanItem, RenameResult};
use renamr_core::rename::{self, ConflictPolicy, RenameAction, RenamePlanItem, RenameResult};
use renamr_core::string_bindings;
use std::collections::HashMap;
use tauri::AppHandle;
@@ -53,19 +53,26 @@ fn guess_release_info(filename: String) -> Option<ReleaseInfo> {
}
/// Executes a batch of planned on-disk rename/copy/symlink operations (optionally
/// moving/copying files into subfolders - see `renamr_core::rename`; conflicting
/// targets are always skipped, never overwritten). `action` picks which of the three
/// happens - the UI is expected to have already shown the user the full old-path/
/// new-path plan, the chosen action, and gotten explicit confirmation before calling
/// this. Runs off the main thread since real filesystem I/O shouldn't block the UI.
/// moving/copying files into subfolders - see `renamr_core::rename`). `action` picks
/// which of the three happens; `conflict` picks what happens when a computed target
/// already exists (skip it, save alongside it with an auto-numbered name, or replace
/// it) - the UI is expected to have already shown the user the full old-path/new-path
/// plan and both choices, and gotten explicit confirmation before calling this. Runs
/// off the main thread since real filesystem I/O shouldn't block the UI.
///
/// Every successfully-produced item is recorded to the rename history log afterward
/// (see `history::record_batch`), tagged with `action`, so it can be undone later from
/// the History panel - a Move undo moves the file back, a Copy/Symlink undo just
/// removes what was created.
/// removes what was created. `conflict` is NOT recorded - undoing always uses its own
/// fixed skip-on-conflict policy regardless of what the original rename used.
#[tauri::command]
async fn execute_rename(app: AppHandle, items: Vec<RenamePlanItem>, action: RenameAction) -> Vec<RenameResult> {
let results = tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items, action))
async fn execute_rename(
app: AppHandle,
items: Vec<RenamePlanItem>,
action: RenameAction,
conflict: ConflictPolicy,
) -> Vec<RenameResult> {
let results = tauri::async_runtime::spawn_blocking(move || rename::execute_renames(&items, action, conflict))
.await
.unwrap_or_default();
history::record_batch(&app, &results, action);