Add per-item exclude to the rename confirmation modal

Each row in the "Confirm Rename" plan now has a small x button that drops just that
file from the batch without closing the modal or needing to cancel and re-select
beforehand (previously it was all-or-nothing - "Exclude Selected" on the file list was
the only way to leave a file out, and only before opening Rename at all). Excluding
every row disables the confirm button and shows an empty-plan hint instead of sending
an empty batch. No Rust changes needed - this is pure plan-state filtering in the
frontend, RenamePlanEntry's shape is unchanged.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Renamr Scaffold
2026-08-29 04:12:35 +00:00
co-authored by Claude Sonnet 5
parent 45a9f5d523
commit aab8e28246
3 changed files with 71 additions and 21 deletions
+6 -7
View File
@@ -11,7 +11,8 @@ TMDB ("Fetch Data") are working for both TV episodes and movies — including fu
title disambiguation, so a search that returns several similarly-named results doesn't
just take TMDB's top-ranked hit — and the actual on-disk rename operation is wired up
(skip-on-conflict, with a confirmation step showing the full old-path/new-path plan
before anything touches disk). Rename can also organize files into subfolders —
before anything touches disk, and a per-row ✕ to drop an individual file from that plan
at the last step). Rename can also organize files into subfolders —
including ones that don't exist yet — when the naming scheme includes a `/`. Every
completed rename is logged to a history panel that can undo it, moving files back to
their original paths. A single loaded batch can freely mix TV episodes and movies —
@@ -230,12 +231,10 @@ ported from, for anyone picking this up:
`--conflict` variants beyond skip (auto-number, override), and the CLI flag surface
itself.
7. **The rest of the UI** — the Rename panel (file loading, drag-drop, live preview,
local matching, confirmed rename, rename history/undo) is built; the other five
sidebar panels (Episodes/Subtitles/SFV/Filter/List) are still stub screens, plus
Align up/down reordering and Presets on the Rename panel itself are still no-ops.
There's also no per-item exclude inside the rename confirmation modal yet (it's
all-or-nothing — use "Exclude Selected" on the file list beforehand to leave a file
out entirely).
local TV/movie matching, confirmed rename with per-item exclude, rename history/undo)
is built; the other five sidebar panels (Episodes/Subtitles/SFV/Filter/List) are
still stub screens, plus Align up/down reordering and Presets on the Rename panel
itself are still no-ops.
8. **Undo/history for renames** — done, as a flat JSON-backed undo stack rather than a
full port of `net.filebot.HistoryDialog`'s session/element log
(`crates/renamr-core/src/history`, `src-tauri/src/history.rs`). Every successful
+25
View File
@@ -403,6 +403,31 @@
font-weight: 600;
}
.modal-plan-remove {
all: unset;
box-sizing: border-box;
flex: 0 0 auto;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: var(--muted-text);
cursor: pointer;
}
.modal-plan-remove:hover {
background: rgba(255, 255, 255, 0.06);
color: #ff8a8a;
}
.modal-plan-remove:disabled {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
.modal-note {
margin: 10px 0 0;
font-size: 11px;
+40 -14
View File
@@ -586,7 +586,7 @@ export default function RenamePanel() {
// on disk via the Rust `execute_rename` command, then reconcile local state (a
// renamed file's id, which is its path, changes) and report the outcome.
const confirmRename = async () => {
if (!renamePlan) return;
if (!renamePlan || renamePlan.length === 0) return;
setRenaming(true);
try {
@@ -649,6 +649,15 @@ export default function RenamePanel() {
setRenamePlan(null);
};
// Per-item exclude, inside the confirmation modal itself: drops one row from the
// plan without closing the modal or having to cancel and re-select beforehand. Every
// bit of the modal's copy (file counts, the confirm button's label) reads directly
// from `renamePlan.length`, so removing a row here updates all of it for free.
const excludeFromPlan = (id: string) => {
if (renaming) return;
setRenamePlan((prev) => (prev ? prev.filter((p) => p.id !== id) : prev));
};
// Rename history / undo - Renamr's only safety net beyond the confirm-before-execute
// step, since a filesystem rename has no OS-level undo. `execute_rename` records a
// batch of every successfully-renamed file after each Rename; this panel lists those
@@ -1073,24 +1082,37 @@ export default function RenamePanel() {
<h2 className="modal-title">Confirm Rename</h2>
<p className="modal-subtitle">
This will rename {renamePlan.length} file{renamePlan.length === 1 ? "" : "s"} on disk right now.
There's no undo yet — review the list below before continuing.
There's no undo yet — review the list below before continuing, and use the ✕ on any row to drop just
that file from this batch.
{renamePlanSkipped > 0 &&
` (${renamePlanSkipped} other file${renamePlanSkipped === 1 ? "" : "s"} excluded: no valid preview.)`}
{renamePlan.some((p) => p.newName.includes("/")) &&
" Some of these also move the file into a new subfolder — any missing folders are created automatically."}
</p>
<div className="modal-plan-list">
{renamePlan.map((p) => (
<div className="modal-plan-row" key={p.id}>
<span className="modal-plan-old" title={p.currentName}>
{p.currentName}
</span>
<span className="modal-plan-arrow">→</span>
<span className="modal-plan-new" title={p.newName}>
{p.newName}
</span>
</div>
))}
{renamePlan.length === 0 ? (
<div className="list-empty-hint">Every file was excluded — nothing left to rename.</div>
) : (
renamePlan.map((p) => (
<div className="modal-plan-row" key={p.id}>
<span className="modal-plan-old" title={p.currentName}>
{p.currentName}
</span>
<span className="modal-plan-arrow">→</span>
<span className="modal-plan-new" title={p.newName}>
{p.newName}
</span>
<button
className="modal-plan-remove"
title="Exclude this file from the rename"
onClick={() => excludeFromPlan(p.id)}
disabled={renaming}
>
<RemoveIcon />
</button>
</div>
))
)}
</div>
<p className="modal-note">
If a computed name already exists on disk, that file is skipped rather than overwritten.
@@ -1099,7 +1121,11 @@ export default function RenamePanel() {
<button className="action-button" onClick={cancelRename} disabled={renaming}>
Cancel
</button>
<button className="action-button primary" onClick={confirmRename} disabled={renaming}>
<button
className="action-button primary"
onClick={confirmRename}
disabled={renaming || renamePlan.length === 0}
>
{renaming ? "Renaming…" : `Rename ${renamePlan.length} File${renamePlan.length === 1 ? "" : "s"}`}
</button>
</div>