diff --git a/backend/database.py b/backend/database.py index f535292..d2a9d9d 100644 --- a/backend/database.py +++ b/backend/database.py @@ -129,6 +129,7 @@ def get_photos( camera: str | None = None, date_from: str | None = None, date_to: str | None = None, + filename_search: str | None = None, cursor_date: str | None = None, cursor_id: int | None = None, limit: int = 100, @@ -144,6 +145,7 @@ def get_photos( camera: Filter by camera model (exact match). date_from: Filter photos taken on or after this date (YYYY-MM-DD). date_to: Filter photos taken on or before this date (YYYY-MM-DD). + filename_search: Case-insensitive substring search on filename. cursor_date: Keyset pagination cursor date_taken. cursor_id: Keyset pagination cursor photo id. limit: Max number of results. @@ -169,6 +171,9 @@ def get_photos( if camera is not None: conditions.append("camera = ?") params.append(camera) + if filename_search is not None: + conditions.append("filename LIKE ? ESCAPE '\\'") + params.append(f"%{filename_search.replace('%', r'\%').replace('_', r'\_')}%") if date_from is not None: conditions.append("REPLACE(date_taken, ':', '-') >= ?") params.append(date_from) @@ -223,6 +228,7 @@ def get_photo_count( camera: str | None = None, date_from: str | None = None, date_to: str | None = None, + filename_search: str | None = None, ) -> int: """Return total count of photos matching filters.""" conn = get_connection(db_path) @@ -242,6 +248,9 @@ def get_photo_count( if camera is not None: conditions.append("camera = ?") params.append(camera) + if filename_search is not None: + conditions.append("filename LIKE ? ESCAPE '\\'") + params.append(f"%{filename_search.replace('%', r'\%').replace('_', r'\_')}%") if date_from is not None: conditions.append("REPLACE(date_taken, ':', '-') >= ?") params.append(date_from) diff --git a/backend/main.py b/backend/main.py index c92f6cc..ede569f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -269,6 +269,7 @@ def list_photos( camera: Optional[str] = Query(None), date_from: Optional[str] = Query(None, pattern=r"^\d{4}-\d{2}-\d{2}$"), date_to: Optional[str] = Query(None, pattern=r"^\d{4}-\d{2}-\d{2}$"), + filename_search: Optional[str] = Query(None, max_length=255), cursor_date: Optional[str] = Query(None), cursor_id: Optional[int] = Query(None), limit: int = Query(100, ge=1, le=500), @@ -283,6 +284,7 @@ def list_photos( camera=camera, date_from=date_from, date_to=date_to, + filename_search=filename_search or None, cursor_date=cursor_date, cursor_id=cursor_id, limit=limit, @@ -305,6 +307,7 @@ def count_photos( camera: Optional[str] = Query(None), date_from: Optional[str] = Query(None, pattern=r"^\d{4}-\d{2}-\d{2}$"), date_to: Optional[str] = Query(None, pattern=r"^\d{4}-\d{2}-\d{2}$"), + filename_search: Optional[str] = Query(None, max_length=255), ): """Return total count of photos matching filters.""" total = get_photo_count( @@ -315,6 +318,7 @@ def count_photos( camera=camera, date_from=date_from, date_to=date_to, + filename_search=filename_search or None, ) return {"count": total} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4d2cd35..9f5e630 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,7 +8,9 @@ import { usePhotos } from './hooks/usePhotos' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' function App() { - const [filters, setFilters] = useState({ min_rating: null, status: null, dev_status: null, camera: null, date_from: null, date_to: null }) + const [filters, setFilters] = useState({ min_rating: null, status: null, dev_status: null, camera: null, date_from: null, date_to: null, filename_search: null }) + const [searchInput, setSearchInput] = useState('') + const searchTimerRef = useRef(null) const [selectedIndex, setSelectedIndex] = useState(null) const [showSettings, setShowSettings] = useState(false) const [selectedIds, setSelectedIds] = useState(new Set()) @@ -39,6 +41,17 @@ function App() { // Clean up timer on unmount useEffect(() => () => { if (jumpTimerRef.current) clearTimeout(jumpTimerRef.current) }, []) + const handleSearchChange = useCallback((e) => { + const val = e.target.value + setSearchInput(val) + if (searchTimerRef.current) clearTimeout(searchTimerRef.current) + searchTimerRef.current = setTimeout(() => { + setFilters(prev => ({ ...prev, filename_search: val.trim() || null })) + searchTimerRef.current = null + }, 300) + }, []) + useEffect(() => () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current) }, []) + const handleScrollToMonth = useCallback((monthKey) => { const el = document.querySelector(`[data-month="${monthKey}"]`) if (el) { @@ -213,6 +226,34 @@ function App() {
+ {/* Filename search */} +
+ + + + + {searchInput && ( + + )} +