diff --git a/backend/app/models/photos.py b/backend/app/models/photos.py index 730c6f5..4f28da4 100644 --- a/backend/app/models/photos.py +++ b/backend/app/models/photos.py @@ -55,7 +55,9 @@ class Photo(Base): rating = Column(Integer, default=0) # 0-5 stars color_label = Column(String) # 'red'|'orange'|'yellow'|'green'|'blue'|'purple'|NULL is_picked = Column(Boolean, default=False) - is_rejected = Column(Boolean, default=False) + # Note: is_rejected was merged into is_trashed (a single soft "trashed" + # concept). The DB column may still exist on legacy installs but is no + # longer read or written. # Duplicate detection is_duplicate = Column(Boolean, default=False) diff --git a/backend/app/routers/photos.py b/backend/app/routers/photos.py index 46f074a..4b052fb 100644 --- a/backend/app/routers/photos.py +++ b/backend/app/routers/photos.py @@ -33,7 +33,6 @@ async def list_photos( rating_max: Optional[int] = Query(None, ge=0, le=5), color_label: Optional[str] = None, is_picked: Optional[bool] = None, - is_rejected: Optional[bool] = None, is_trashed: Optional[bool] = False, heap_id: Optional[str] = None, sort: str = "taken_at", @@ -93,10 +92,8 @@ async def list_photos( # Flag filters if is_picked is not None: filters.append(Photo.is_picked == is_picked) - if is_rejected is not None: - filters.append(Photo.is_rejected == is_rejected) - - # Trash filter + + # Trash filter — defaults to hiding trashed photos filters.append(Photo.is_trashed == is_trashed) # Apply all filters @@ -414,33 +411,22 @@ async def trash_photo( photo_id: str, db: AsyncSession = Depends(get_db) ): - """Move photo to trash""" + """Soft-trash a photo: sets is_trashed=true. The file stays on disk so + restore is just a flag flip. Permanent deletion happens via DELETE + /trash/{id} or DELETE /trash/empty. + """ result = await db.execute( select(Photo).where(Photo.id == photo_id) ) photo = result.scalar_one_or_none() - + if not photo: raise HTTPException(status_code=404, detail="Photo not found") - - # Move file to trash directory - import shutil - trash_dir = f"{settings.trash.path}/{photo_id}" - os.makedirs(trash_dir, exist_ok=True) - - trash_path = f"{trash_dir}/original{Path(photo.filepath).suffix}" - - try: - shutil.move(photo.filepath, trash_path) - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to move file: {e}") - - # Update database + photo.is_trashed = True photo.trashed_at = datetime.utcnow() - await db.commit() - + return {"status": "success", "message": "Photo moved to trash"} @router.post("/bulk") @@ -476,11 +462,7 @@ async def bulk_action( elif action.action == 'pick': for photo in photos: photo.is_picked = True - photo.is_rejected = False - elif action.action == 'reject': - for photo in photos: - photo.is_rejected = True - photo.is_picked = False + photo.is_trashed = False else: raise HTTPException(status_code=400, detail="Invalid action") diff --git a/backend/app/schemas/photos.py b/backend/app/schemas/photos.py index 2aace2c..baaf710 100644 --- a/backend/app/schemas/photos.py +++ b/backend/app/schemas/photos.py @@ -20,7 +20,6 @@ class PhotoBase(BaseModel): rating: int = 0 color_label: Optional[str] = None is_picked: bool = False - is_rejected: bool = False class PhotoResponse(PhotoBase): """Photo response schema""" @@ -53,7 +52,7 @@ class PhotoUpdate(BaseModel): rating: Optional[int] = Field(None, ge=0, le=5) color_label: Optional[str] = None is_picked: Optional[bool] = None - is_rejected: Optional[bool] = None + is_trashed: Optional[bool] = None taken_at: Optional[datetime] = None class PhotoListResponse(BaseModel): diff --git a/frontend/src/components/filter/FilterBar.tsx b/frontend/src/components/filter/FilterBar.tsx index 0d1eb75..47b8ed3 100644 --- a/frontend/src/components/filter/FilterBar.tsx +++ b/frontend/src/components/filter/FilterBar.tsx @@ -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: 'rejected', label: 'Rejected' }, + { value: 'trashed', label: 'Trashed' }, { value: 'unflagged', label: 'Unflagged' }, ] diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 34fb6ae..7b72c6e 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -9,6 +9,7 @@ import { ChevronDown, ChevronRight, Check, + Trash2, } from 'lucide-react' import clsx from 'clsx' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' @@ -26,7 +27,7 @@ interface PhotoDetails { taken_at: string | null rating: number is_picked: boolean - is_rejected: boolean + is_trashed: boolean exif_json: string | null } @@ -107,7 +108,7 @@ export function RightSidebar() { mutationFn: (data: { rating?: number is_picked?: boolean - is_rejected?: boolean + is_trashed?: boolean }) => photosApi.update(activePhotoId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['photo', activePhotoId] }) @@ -131,7 +132,7 @@ export function RightSidebar() { const multipleSelected = selectedPhotos.length > 1 const rating = photo?.rating ?? 0 const isPicked = photo?.is_picked ?? false - const isRejected = photo?.is_rejected ?? false + const isTrashed = photo?.is_trashed ?? false return (