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:
@@ -98,6 +98,12 @@ interface RenameResult {
|
||||
// confirmRename below).
|
||||
type RenameAction = "move" | "copy" | "symlink";
|
||||
|
||||
// Mirrors renamr-core's `ConflictPolicy` - what happens when a computed target already
|
||||
// exists. "skip" (the default, and the only behavior Renamr had before) leaves the
|
||||
// conflicting file alone; "autoNumber" saves alongside it as "Name (1).ext" instead;
|
||||
// "override" replaces it outright (irreversible for whatever gets overwritten).
|
||||
type ConflictPolicy = "skip" | "autoNumber" | "override";
|
||||
|
||||
// Sent to `fetch_metadata`: one file's local guess (from Match) to resolve against
|
||||
// real TMDB data. Needs a series name + season/episode to search with - exactly what
|
||||
// Match already produces offline, which is why Fetch Data requires Match to have run
|
||||
@@ -287,6 +293,25 @@ const RENAME_ACTION_LABELS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
// Copy for each ConflictPolicy, used in the confirmation modal's second segmented
|
||||
// selector and the modal's conflict-behavior note.
|
||||
const CONFLICT_POLICY_LABELS: Record<ConflictPolicy, { button: string; description: string }> = {
|
||||
skip: {
|
||||
button: "Skip",
|
||||
description: "If a computed name already exists on disk, that file is left alone and skipped.",
|
||||
},
|
||||
autoNumber: {
|
||||
button: "Auto-number",
|
||||
description:
|
||||
'If a computed name already exists on disk, the new one is saved alongside it as "Name (1).ext" instead (counting up until a free name is found).',
|
||||
},
|
||||
override: {
|
||||
button: "Override",
|
||||
description:
|
||||
"If a computed name already exists on disk, it's replaced outright. This can't be undone — undoing this batch later can restore the source file, but not whatever got overwritten.",
|
||||
},
|
||||
};
|
||||
|
||||
function realFileToItem(entry: FileEntry): Item {
|
||||
return {
|
||||
id: entry.path,
|
||||
@@ -372,6 +397,10 @@ export default function RenamePanel() {
|
||||
// remembered across modal opens within the session (defaults to "move", matching
|
||||
// Renamr's original and only behavior before copy/symlink existed).
|
||||
const [renameAction, setRenameAction] = useState<RenameAction>("move");
|
||||
// What to do when a computed target already exists - same remembered-across-opens
|
||||
// pattern as renameAction, defaults to "skip" (Renamr's original and only behavior
|
||||
// before auto-number/override existed).
|
||||
const [conflictPolicy, setConflictPolicy] = useState<ConflictPolicy>("skip");
|
||||
// TMDB token state for "Fetch Data". `null` = not checked yet, `false` = checked and
|
||||
// nothing saved, `true` = a token is on file (the value itself never comes back to
|
||||
// the frontend after saving - see Settings modal below).
|
||||
@@ -707,6 +736,7 @@ export default function RenamePanel() {
|
||||
const results = await invoke<RenameResult[]>("execute_rename", {
|
||||
items: renamePlan.map((p) => ({ source: p.source, newName: p.newName })),
|
||||
action: renameAction,
|
||||
conflict: conflictPolicy,
|
||||
});
|
||||
const bySource = new Map(results.map((r) => [r.source, r] as const));
|
||||
// Compute counts from `results` directly, not inside the setItems updater below:
|
||||
@@ -1316,6 +1346,26 @@ export default function RenamePanel() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="modal-field-label">On conflict</span>
|
||||
<div
|
||||
className="action-segmented"
|
||||
role="radiogroup"
|
||||
aria-label="What to do when the computed name already exists"
|
||||
>
|
||||
{(["skip", "autoNumber", "override"] as const).map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className={`action-segmented-option${conflictPolicy === c ? " selected" : ""}${
|
||||
c === "override" ? " action-segmented-option-danger" : ""
|
||||
}`}
|
||||
onClick={() => setConflictPolicy(c)}
|
||||
disabled={renaming}
|
||||
>
|
||||
{CONFLICT_POLICY_LABELS[c].button}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="modal-subtitle">
|
||||
This will {RENAME_ACTION_LABELS[renameAction].verb} {renamePlan.length} file
|
||||
{renamePlan.length === 1 ? "" : "s"} on disk right now. Review the list below before continuing — use
|
||||
@@ -1355,8 +1405,8 @@ export default function RenamePanel() {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<p className="modal-note">
|
||||
If a computed name already exists on disk, that file is skipped rather than overwritten.
|
||||
<p className={`modal-note${conflictPolicy === "override" ? " modal-note-danger" : ""}`}>
|
||||
{CONFLICT_POLICY_LABELS[conflictPolicy].description}
|
||||
</p>
|
||||
<div className="modal-actions">
|
||||
<button className="action-button" onClick={cancelRename} disabled={renaming}>
|
||||
|
||||
Reference in New Issue
Block a user