From 1054d64a640b125421446b34ec8666e67f12c6e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 16:44:19 +0000 Subject: [PATCH] Add filename search input to header next to refresh button Adds a debounced filename search box in the app header (next to the Refresh button) that filters RAW files by name substring match. - backend/database.py: add `filename_search` LIKE param to get_photos() and get_photo_count() - backend/main.py: expose `filename_search` query param on /api/photos and /api/photos/count endpoints - frontend/src/hooks/usePhotos.js: include filename_search in buildParams - frontend/src/App.jsx: add debounced search input (300ms) with clear button; filename_search wired into filters state - frontend/src/components/Toolbar.jsx: include filename_search in hasFilters check; support onClearAll prop to also reset search input https://claude.ai/code/session_01Rz1mnmLosnStLBTjSv5Wx4 --- backend/database.py | 9 +++++ backend/main.py | 4 +++ frontend/src/App.jsx | 53 +++++++++++++++++++++++++++-- frontend/src/components/Toolbar.jsx | 9 +++-- frontend/src/hooks/usePhotos.js | 3 +- 5 files changed, 73 insertions(+), 5 deletions(-) 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 && ( + + )} +