Add a global last-action stack with toast-based "Undo" buttons and a
Cmd/Ctrl+Z hotkey for the destructive photo operations.
Reversible:
- X (discard) → bulkRestore
- U (restore) → bulkDiscard
- Drag-onto-Discarded → bulkRestore
- Drag-onto-folder (move) → move back to per-photo source folders. The
source folder ids are snapshotted from the photos cache before the
move runs, then grouped so multi-source moves restore correctly.
- Restore button in the discard action bar → bulkDiscard
Toast gains an optional action button (label + onClick); toasts with an
action stay visible longer so the user has time to click. The undo
store caps at 20 entries; failed undo re-pushes the entry so the user
can try again.
Not reversible (call out, document later): rating, color label, copy,
permanent delete from trash, tag changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Discarded photos now look discarded in the grid (50% opacity + grayscale)
with a red trash badge in the corner instead of a bare icon. The discard
action bar gains a "Delete N" button that permanently deletes only the
current selection, complementing the existing "Empty discard pile".
Backend: new DELETE /discard endpoint accepting {photo_ids: [...]} that
permanently removes only listed photos. Skips ids that aren't in the
discard pile so it can never bypass the soft-delete safety net.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the destructive-action loop the discard concept needed:
- Click "Discarded" in the left sidebar → activates the discarded
filter; the timeline reloads showing discarded photos.
- DiscardActionBar appears at the top of the timeline only when the
discarded filter is active. Shows the count, a Restore button (when
photos are selected), and an Empty discard pile button.
- Empty action goes through a ConfirmDialog (new tiny reusable modal,
same overlay pattern as AddSourceFolderDialog).
- Restore goes through POST /api/v1/discard/restore.
- DELETE /api/v1/discard/empty now actually os.unlink()s the files
from disk in addition to removing the DB rows. Per-file failures
are logged and reported in the response so a single permission
error doesn't abort the batch.
Other library nodes wired in passing:
- "All Photos" → clearAll()
- "Rated" → setRatingMin(1)
- "Flagged" → setFlag('picked')
- "Discarded" → setFlag('discarded')
- "By Date" left unwired (needs a date-grouping UI)
Single-photo restore via the U keyboard shortcut already worked from
an earlier round, no change needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>