User-facing labels and code now use "discard" (verb) and "Discarded"
(state/view label) instead of "trash" / "Trashed". The DB column names
stay (is_trashed / trashed_at) so no migration is required — only the
SQLAlchemy attribute names are renamed via Column('old_name', ...).
Backend
- Photo model: is_discarded / discarded_at attributes (DB columns
unchanged).
- PhotoBase / PhotoResponse / PhotoUpdate schemas use the new field
names.
- Photos list endpoint: is_discarded query param, filter logic.
- DELETE /photos/{id} now sets is_discarded; success message updated.
- Bulk action 'trash' renamed to 'discard'.
- backend/app/routers/trash.py renamed to discard.py with renamed
functions and route prefix /api/v1/discard.
- main.py imports and mounts the discard router.
- tasks/scan.py marks missing files as is_discarded.
Frontend
- Photo TS type: is_discarded.
- PhotoThumbnail: shows the trash-can icon when is_discarded.
- RightSidebar: button label "Discard"; mutation field name; local
variable rename.
- TopBar: discardPhotosMutation and "Discard" button; toast text
"Discarded".
- LeftSidebar: virtual node id 'discarded' / label "Discarded".
- FilterBar / filterStore / useFilterUrlSync: FlagFilter enum value
'trashed' → 'discarded'; backend param key is_discarded.
- KeyboardHints: X label "Discard".
- useKeyboardShortcuts: PhotoUpdate field rename, X handler.
- api.ts: /trash routes → /discard, trash export → discard,
bulkUpdate trash field → discard.
Out of scope (intentional): the docker-compose trash_data volume,
backend/Dockerfile mkdir /data/trash, config.py TrashSettings, and
the spec doc — all unused since soft-discard, and renaming them is
churn for no benefit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { usePhotoStore } from '../store/photoStore'
|
|
|
|
export function KeyboardHints() {
|
|
const selectedCount = usePhotoStore((state) => state.selectedPhotos.length)
|
|
const viewMode = usePhotoStore((state) => state.viewMode)
|
|
|
|
// In preview mode the viewer has its own context, so the grid hints
|
|
// would just be confusing. Hide them.
|
|
if (viewMode === 'preview') return null
|
|
|
|
const hints = selectedCount > 0
|
|
? [
|
|
{ key: '1-5', action: 'Rate' },
|
|
{ key: 'P', action: 'Pick' },
|
|
{ key: 'X', action: 'Discard' },
|
|
{ key: 'E / Space', action: 'Preview' },
|
|
{ key: 'Esc', action: 'Deselect' },
|
|
]
|
|
: [
|
|
{ key: '↑↓←→', action: 'Navigate' },
|
|
{ key: 'Click', action: 'Select' },
|
|
{ key: 'Shift+Click', action: 'Range' },
|
|
{ key: 'E / Space', action: 'Preview' },
|
|
{ key: '\\', action: 'Filters' },
|
|
{ key: '/', action: 'Search' },
|
|
]
|
|
|
|
return (
|
|
<div className="flex justify-center border-b border-border bg-surface/60 px-4 py-1.5">
|
|
<div className="flex items-center gap-3">
|
|
{hints.map((hint, i) => (
|
|
<div key={i} className="flex items-center gap-1.5">
|
|
<kbd className="rounded bg-surface-offset px-2 py-0.5 text-[11px] font-medium text-text">
|
|
{hint.key}
|
|
</kbd>
|
|
<span className="text-xs text-text-muted">{hint.action}</span>
|
|
{i < hints.length - 1 && (
|
|
<span className="ml-2 text-text-faint">•</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
{selectedCount > 0 && (
|
|
<>
|
|
<span className="ml-2 text-text-faint">•</span>
|
|
<span className="text-xs font-medium text-primary">
|
|
{selectedCount} selected
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|