The Photo model previously had two near-identical "negative culling"
states: is_rejected (a flag) and is_trashed (a flag plus a file move).
Lightroom users typically use one or the other, never both, and the
file-move semantics of the old trash made it harder to undo. Merging
into a single soft is_trashed flag — file stays on disk, restore is a
flag flip, permanent deletion still happens via DELETE /trash/empty.
Backend
- Drop is_rejected from PhotoBase, PhotoResponse, PhotoUpdate, the
list endpoint filter, and the bulk-action 'reject' branch.
- Add is_trashed to PhotoUpdate so the PATCH path can set it.
- Drop is_rejected Column declaration from the SQLAlchemy model. The
legacy DB column may persist on existing installs but is no longer
read or written; SQLAlchemy ignores extra columns.
- Rewrite DELETE /photos/{id} as a soft trash: just sets is_trashed=
true and trashed_at=now, no shutil.move. Permanent deletion still
goes through the trash router.
Frontend
- Photo TS type drops is_rejected, gains is_trashed.
- X keyboard shortcut now sets is_trashed=true (was is_rejected); U
clears both is_picked and is_trashed.
- RightSidebar Reject button → Trash button (Trash2 icon).
- PhotoThumbnail flag overlay shows Trash2 icon for trashed photos
instead of an X for rejected.
- KeyboardHints relabels X from "Reject" to "Trash".
- filterStore FlagFilter renames 'rejected' → 'trashed'; the params
builder now sends is_trashed=true for the trashed filter (the list
endpoint defaults to hiding trashed photos otherwise).
- FilterBar dropdown / URL sync allow-list updated accordingly.
No data migration: existing rejected photos remain as-is (flag stale)
and effectively become unflagged in the new model. Re-trash from the
UI to bring them into the new state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
"""
|
|
Pydantic schemas for photos
|
|
"""
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
|
|
class PhotoBase(BaseModel):
|
|
"""Base photo schema"""
|
|
filename: str
|
|
media_type: str
|
|
original_format: Optional[str] = None
|
|
width: Optional[int] = None
|
|
height: Optional[int] = None
|
|
file_size: Optional[int] = None
|
|
taken_at: Optional[datetime] = None
|
|
taken_at_source: Optional[str] = None
|
|
user_title: Optional[str] = None
|
|
user_notes: Optional[str] = None
|
|
rating: int = 0
|
|
color_label: Optional[str] = None
|
|
is_picked: bool = False
|
|
|
|
class PhotoResponse(PhotoBase):
|
|
"""Photo response schema"""
|
|
id: str
|
|
filepath: str
|
|
folder_id: Optional[str] = None
|
|
file_hash: Optional[str] = None
|
|
added_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
is_trashed: bool = False
|
|
trashed_at: Optional[datetime] = None
|
|
thumb_small: Optional[str] = None
|
|
thumb_medium: Optional[str] = None
|
|
thumb_large: Optional[str] = None
|
|
processing_status: str = 'pending'
|
|
processing_error: Optional[str] = None
|
|
exif_json: Optional[str] = None
|
|
is_duplicate: bool = False
|
|
live_photo_video_id: Optional[str] = None
|
|
# tags: List[Dict[str, Any]] = [] # TODO: Enable when using eager loading
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
from_attributes = True
|
|
|
|
class PhotoUpdate(BaseModel):
|
|
"""Photo update schema"""
|
|
user_title: Optional[str] = None
|
|
user_notes: Optional[str] = None
|
|
rating: Optional[int] = Field(None, ge=0, le=5)
|
|
color_label: Optional[str] = None
|
|
is_picked: Optional[bool] = None
|
|
is_trashed: Optional[bool] = None
|
|
taken_at: Optional[datetime] = None
|
|
|
|
class PhotoListResponse(BaseModel):
|
|
"""Photo list response with pagination"""
|
|
photos: List[PhotoResponse]
|
|
total: int
|
|
page: int
|
|
per_page: int
|
|
pages: int
|
|
|
|
class BulkAction(BaseModel):
|
|
"""Bulk action on photos"""
|
|
ids: List[str]
|
|
action: str # 'trash', 'restore', 'delete_permanent', 'move', 'copy', 'add_tag', 'remove_tag', 'set_rating', 'set_color', 'pick', 'reject'
|
|
value: Optional[Any] = None # For actions that need a value (rating, color, tag_id, folder_id) |