Skip to content

feat(files): multi-select files with bulk move/delete on the audiobook detail page#720

Draft
kevinheneveld wants to merge 2 commits into
Listenarrs:canaryfrom
kevinheneveld:feat/file-multi-select
Draft

feat(files): multi-select files with bulk move/delete on the audiobook detail page#720
kevinheneveld wants to merge 2 commits into
Listenarrs:canaryfrom
kevinheneveld:feat/file-multi-select

Conversation

@kevinheneveld

Copy link
Copy Markdown
Contributor

Stacked on #719 (transfer-files) — this branch contains that PR's commit because bulk move reuses its TransferFilesModal. Only the top commit (feat(files): multi-select…) is new here; the diff will collapse to just it once #719 merges.

What

Adds multi-select to the Files list on the audiobook detail page: a selection checkbox per file row (shift-click extends a range) and a toolbar with select-all, Move selected… and Delete selected.

  • Bulk move hands the selection to TransferFilesModal (from feat(library): move files to another audiobook record #719) via its initialFileIds preselection — pick a destination record and only the ticked files transfer.
  • Bulk delete loops a per-file delete behind a single confirm listing the files, with a shared "also delete from disk" option (unchecked: DB rows only; a rescan re-imports them) and a summary toast that reports partial failures.
  • New endpoint DELETE /library/{id}/files/{fileId}?deleteFromDisk= (LibraryFileDeleteWorkflow + IAudiobookFileService.DeleteAudiobookFileAsync): ownership-guarded (the file must belong to the addressed audiobook), disk-delete failures surface as warnings while the DB row is still removed, and a "File Removed" history entry records each deletion.
  • Selection clears on reload/navigation so a stale selection can't act on the wrong rows.

Why

Deleting duplicates one at a time doesn't scale — the motivating live case was a duplicated book: the same audio under two different filename schemes sitting on one record, which a split-by-content flow can't separate (it's the same book twice). Select the redundant copies → delete once.

Tests

6 new tests in LibraryController_FileDeleteTests (missing audiobook/file, wrong-owner guard, DB-only delete keeps the file on disk, disk delete, row-only ghost cleanup). Full suite: 1030/1030 passing. vue-tsc + eslint + prettier clean.

🤖 Generated with Claude Code

kevinheneveld and others added 2 commits July 1, 2026 17:39
When files actually belong to a different book the library already
tracks (two books' tracks imported onto one record, or a collection
being split into its real books), a "Move Files to Another Book" action
on the detail page opens a transfer dialog with per-file checkboxes and
a library search ranked by token overlap.

POST /library/{id}/files/transfer: DB ownership reassigns always; the
physical file moves into the destination folder best-effort (failures
leave it in place with a warning — Organize can relocate later).
Collisions with a row the target already owns at the same path are
detected BEFORE any disk move and skipped with a warning, and any
per-file DB failure becomes a warning rather than aborting the whole
transfer. A source left without audio gets its legacy single-file
columns reset; history entries land on both records.

Row reassignment uses a targeted IAudiobookFileRepository.ReassignAsync
(detached stub, only the reassigned columns marked modified) so bulk
transfers can't trip EF identity-map conflicts on overlapping
navigation graphs.

Warning/error toasts are now sticky by default (dismissed via the close
button) — transfer warnings carry information the user needs to read
and act on; info/success keep the 5s auto-dismiss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k detail page

Deleting duplicates or moving a subset of files one at a time doesn't
scale (e.g. a duplicated book: the same audio under two different
filename schemes on one record). Add a selection checkbox to each file
row (shift-click extends a range) and a toolbar with select-all, "Move
selected…" and "Delete selected". Bulk move hands the selection to
TransferFilesModal via its initialFileIds preselection; bulk delete
loops a per-file delete behind one confirm with a shared "also delete
from disk" option and a summary toast. Selection clears on reload.

The per-file delete this rides on is new here too:
DELETE /library/{id}/files/{fileId}?deleteFromDisk= — removes the
AudiobookFile row (ownership guarded: the file must belong to the
addressed audiobook), optionally deletes the file from disk
(disk-delete failures surface as warnings; the row is still removed),
and records a "File Removed" history entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant