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>
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import { useEffect } from 'react'
|
|
import clsx from 'clsx'
|
|
|
|
interface ConfirmDialogProps {
|
|
isOpen: boolean
|
|
title: string
|
|
message: React.ReactNode
|
|
confirmLabel?: string
|
|
cancelLabel?: string
|
|
/** When true, the confirm button uses the destructive accent. */
|
|
destructive?: boolean
|
|
onConfirm: () => void
|
|
onClose: () => void
|
|
}
|
|
|
|
/**
|
|
* Tiny modal-confirmation dialog. Mirrors the AddSourceFolderDialog overlay
|
|
* pattern (custom fixed inset-0 backdrop, no shadcn Dialog dep). Esc closes.
|
|
*/
|
|
export function ConfirmDialog({
|
|
isOpen,
|
|
title,
|
|
message,
|
|
confirmLabel = 'Confirm',
|
|
cancelLabel = 'Cancel',
|
|
destructive = false,
|
|
onConfirm,
|
|
onClose,
|
|
}: ConfirmDialogProps) {
|
|
// Esc to close.
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose()
|
|
}
|
|
window.addEventListener('keydown', handler)
|
|
return () => window.removeEventListener('keydown', handler)
|
|
}, [isOpen, onClose])
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50">
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
<div className="relative z-10 w-96 rounded-lg border border-border bg-surface p-5 shadow-2xl">
|
|
<h2 className="mb-2 text-base font-semibold text-text">{title}</h2>
|
|
<div className="mb-4 text-sm text-text-muted">{message}</div>
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded border border-border px-3 py-1.5 text-sm text-text hover:bg-surface-2"
|
|
>
|
|
{cancelLabel}
|
|
</button>
|
|
<button
|
|
onClick={onConfirm}
|
|
className={clsx(
|
|
'rounded px-3 py-1.5 text-sm font-medium text-white',
|
|
destructive
|
|
? 'bg-reject hover:bg-reject/80'
|
|
: 'bg-primary hover:bg-primary/80'
|
|
)}
|
|
>
|
|
{confirmLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|