refactor: rename trash to discard end-to-end

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>
This commit is contained in:
2026-04-07 22:54:31 +02:00
parent 2679214cb9
commit 997e11db78
17 changed files with 82 additions and 81 deletions

View File

@@ -12,7 +12,7 @@ export function KeyboardHints() {
? [
{ key: '1-5', action: 'Rate' },
{ key: 'P', action: 'Pick' },
{ key: 'X', action: 'Trash' },
{ key: 'X', action: 'Discard' },
{ key: 'E / Space', action: 'Preview' },
{ key: 'Esc', action: 'Deselect' },
]

View File

@@ -26,7 +26,7 @@ const COLOR_LABELS: { value: ColorLabel; className: string }[] = [
const FLAG_OPTIONS: { value: FlagFilter; label: string }[] = [
{ value: 'any', label: 'Any' },
{ value: 'picked', label: 'Picked' },
{ value: 'trashed', label: 'Trashed' },
{ value: 'discarded', label: 'Discarded' },
{ value: 'unflagged', label: 'Unflagged' },
]

View File

@@ -111,7 +111,7 @@ export function LeftSidebar() {
{ id: 'by-date', label: 'By Date', icon: <Calendar className="h-4 w-4" /> },
{ id: 'rated', label: 'Rated', icon: <Star className="h-4 w-4" />, count: 0 },
{ id: 'flagged', label: 'Flagged', icon: <Flag className="h-4 w-4" />, count: 0 },
{ id: 'trash', label: 'Trash', icon: <Trash2 className="h-4 w-4" />, count: 0 },
{ id: 'discarded', label: 'Discarded', icon: <Trash2 className="h-4 w-4" />, count: 0 },
],
},
{

View File

@@ -27,7 +27,7 @@ interface PhotoDetails {
taken_at: string | null
rating: number
is_picked: boolean
is_trashed: boolean
is_discarded: boolean
exif_json: string | null
}
@@ -108,7 +108,7 @@ export function RightSidebar() {
mutationFn: (data: {
rating?: number
is_picked?: boolean
is_trashed?: boolean
is_discarded?: boolean
}) => photosApi.update(activePhotoId!, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['photo', activePhotoId] })
@@ -132,7 +132,7 @@ export function RightSidebar() {
const multipleSelected = selectedPhotos.length > 1
const rating = photo?.rating ?? 0
const isPicked = photo?.is_picked ?? false
const isTrashed = photo?.is_trashed ?? false
const isDiscarded = photo?.is_discarded ?? false
return (
<div className="flex h-full flex-col bg-surface">
@@ -187,7 +187,7 @@ export function RightSidebar() {
onClick={() =>
updateMutation.mutate({
is_picked: !isPicked,
is_trashed: false,
is_discarded: false,
})
}
className={clsx(
@@ -203,19 +203,19 @@ export function RightSidebar() {
<button
onClick={() =>
updateMutation.mutate({
is_trashed: !isTrashed,
is_discarded: !isDiscarded,
is_picked: false,
})
}
className={clsx(
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
isTrashed
isDiscarded
? 'bg-reject/20 text-reject'
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
)}
>
<Trash2 className="h-3 w-3" />
Trash
Discard
</button>
</div>
</div>

View File

@@ -58,18 +58,18 @@ export function TopBar() {
const selectedCount = selectedPhotos.length
const queryClient = useQueryClient()
// Mutation for moving photos to trash
const trashPhotosMutation = useMutation({
// Mutation for discarding selected photos
const discardPhotosMutation = useMutation({
mutationFn: async () => {
await photos.bulkUpdate(selectedPhotos, { trash: true })
await photos.bulkUpdate(selectedPhotos, { discard: true })
},
onSuccess: () => {
toast.success('Moved to Trash', `${selectedCount} photo${selectedCount > 1 ? 's' : ''} moved to trash`)
toast.success('Discarded', `${selectedCount} photo${selectedCount > 1 ? 's' : ''} discarded`)
clearSelection()
queryClient.invalidateQueries({ queryKey: ['photos'] })
},
onError: (error: any) => {
toast.error('Failed to Move to Trash', error.message || 'An error occurred')
toast.error('Failed to discard', error.message || 'An error occurred')
},
})
@@ -96,13 +96,13 @@ export function TopBar() {
{selectedCount} selected
</span>
<button
onClick={() => trashPhotosMutation.mutate()}
disabled={trashPhotosMutation.isPending}
onClick={() => discardPhotosMutation.mutate()}
disabled={discardPhotosMutation.isPending}
className="flex items-center gap-1 rounded bg-reject/20 px-2 py-0.5 text-sm text-reject hover:bg-reject/30 disabled:opacity-50"
title="Move to trash"
title="Discard"
>
<Trash2 className="h-3.5 w-3.5" />
Trash
Discard
</button>
</>
)}

View File

@@ -171,7 +171,7 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick, onDoubleClick
{photo.is_picked && (
<Check className="h-4 w-4 text-pick" />
)}
{photo.is_trashed && (
{photo.is_discarded && (
<Trash2 className="h-4 w-4 text-reject" />
)}
</div>