From 2679214cb9c2c2d4201f60af2c4d2d491503d941 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Apr 2026 22:47:52 +0200 Subject: [PATCH] refactor: rename loupe to preview, bind to E and Space, fix empty viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loupe view is now called "preview" everywhere — file paths, type names, store actions, and the contextual hint pill. There's a single preview action bound to E and Space (Enter is gone); double-click on a thumbnail still works. Both shortcuts toggle: open from grid, close from preview. This commit also folds in the fix for the "preview shows nothing" bug the user just hit: - Extract usePhotosQuery into frontend/src/hooks/usePhotosQuery.ts so Timeline, PreviewView, and App.tsx all share one query — and one cache entry. Previously PreviewView and App.tsx looked the cache up by ['photos'], but the Timeline query key gained the filter params (['photos', filterParams]) when the filter bar shipped, so the lookup returned undefined and the preview rendered "No photo to display". App.tsx's getFirstPhotoId callback had the same bug. - Harden PreviewImage: render the immediately and overlay the spinner with absolute positioning, instead of toggling opacity-0 → opacity-100 on load. The previous opacity-toggle could leave the image stuck invisible if the load event raced with a key change. - Add { preventDefault: true } to every useHotkeys call so single letter shortcuts (1-5, P, X, U) no longer leak into Firefox quick- find, and Cmd/Ctrl+F no longer triggers the browser find toolbar. Files renamed: components/loupe/LoupeView.tsx -> components/preview/PreviewView.tsx components/loupe/LoupeImage.tsx -> components/preview/PreviewImage.tsx components/loupe/LoupeFilmstrip.tsx -> components/preview/PreviewFilmstrip.tsx components/loupe/loupeSrc.ts -> components/preview/previewSrc.ts Symbol renames: openLoupe→openPreview, closeLoupe→closePreview, the viewMode 'loupe' tag → 'preview', and all the LoupeXxx component and helper exports. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 22 +-- frontend/src/components/KeyboardHints.tsx | 8 +- .../PreviewFilmstrip.tsx} | 4 +- .../PreviewImage.tsx} | 53 ++++--- .../LoupeView.tsx => preview/PreviewView.tsx} | 60 ++++---- .../loupeSrc.ts => preview/previewSrc.ts} | 8 +- frontend/src/components/timeline/Timeline.tsx | 56 +------- frontend/src/hooks/useKeyboardShortcuts.ts | 133 ++++++++---------- frontend/src/hooks/usePhotosQuery.ts | 55 ++++++++ frontend/src/store/photoStore.ts | 10 +- 10 files changed, 196 insertions(+), 213 deletions(-) rename frontend/src/components/{loupe/LoupeFilmstrip.tsx => preview/PreviewFilmstrip.tsx} (92%) rename frontend/src/components/{loupe/LoupeImage.tsx => preview/PreviewImage.tsx} (81%) rename frontend/src/components/{loupe/LoupeView.tsx => preview/PreviewView.tsx} (75%) rename frontend/src/components/{loupe/loupeSrc.ts => preview/previewSrc.ts} (79%) create mode 100644 frontend/src/hooks/usePhotosQuery.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56593f5..6a2ceca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,4 @@ import { useState } from 'react' -import { useQueryClient } from '@tanstack/react-query' import { Timeline } from './components/timeline/Timeline' import { LeftSidebar } from './components/layout/LeftSidebar' import { RightSidebar } from './components/layout/RightSidebar' @@ -7,36 +6,37 @@ import { TopBar } from './components/layout/TopBar' import { ScanProgress } from './components/ScanProgress' import { ToastContainer } from './components/ToastContainer' import { KeyboardHints } from './components/KeyboardHints' -import { LoupeView } from './components/loupe/LoupeView' +import { PreviewView } from './components/preview/PreviewView' import { FilterBar } from './components/filter/FilterBar' import { ActiveFilterChips } from './components/filter/ActiveFilterChips' import { usePhotoStore } from './store/photoStore' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useFilterUrlSync } from './hooks/useFilterUrlSync' -import type { Photo } from './types/photo' +import { usePhotosQuery } from './hooks/usePhotosQuery' function App() { const [leftSidebarOpen, setLeftSidebarOpen] = useState(true) const [rightSidebarOpen, setRightSidebarOpen] = useState(false) const selectedPhotos = usePhotoStore((state) => state.selectedPhotos) const viewMode = usePhotoStore((state) => state.viewMode) - const queryClient = useQueryClient() // Bidirectional sync of filter store with URL query params. useFilterUrlSync() + // Subscribe to the same photos query the Timeline uses, so the keyboard + // "open preview on first photo" path can read from the live cache regardless + // of what filter key it's stored under. + const { data: allPhotos } = usePhotosQuery() + // Set up global keyboard shortcuts useKeyboardShortcuts({ onToggleLeftSidebar: () => setLeftSidebarOpen(!leftSidebarOpen), onToggleRightSidebar: () => setRightSidebarOpen(!rightSidebarOpen), - getFirstPhotoId: () => { - const photos = queryClient.getQueryData(['photos']) - return photos && photos.length > 0 ? photos[0].id : null - }, + getFirstPhotoId: () => allPhotos?.[0]?.id ?? null, }) // Auto-show right sidebar when photos are selected — but only in grid mode, - // so leaving loupe doesn't fight the user's prior sidebar state. + // so leaving the preview doesn't fight the user's prior sidebar state. if (viewMode === 'grid') { if (selectedPhotos.length > 0 && !rightSidebarOpen) { setRightSidebarOpen(true) @@ -85,8 +85,8 @@ function App() { {/* Toast Notifications */} - {/* Loupe overlay — covers TopBar when active */} - {viewMode === 'loupe' && } + {/* Preview overlay — covers TopBar when active */} + {viewMode === 'preview' && } ) } diff --git a/frontend/src/components/KeyboardHints.tsx b/frontend/src/components/KeyboardHints.tsx index a278318..6fa2a10 100644 --- a/frontend/src/components/KeyboardHints.tsx +++ b/frontend/src/components/KeyboardHints.tsx @@ -4,23 +4,23 @@ export function KeyboardHints() { const selectedCount = usePhotoStore((state) => state.selectedPhotos.length) const viewMode = usePhotoStore((state) => state.viewMode) - // In loupe mode the photo viewer has its own context, so the grid hints + // In preview mode the viewer has its own context, so the grid hints // would just be confusing. Hide them. - if (viewMode === 'loupe') return null + if (viewMode === 'preview') return null const hints = selectedCount > 0 ? [ { key: '1-5', action: 'Rate' }, { key: 'P', action: 'Pick' }, { key: 'X', action: 'Trash' }, - { key: 'E', action: 'Loupe' }, + { key: 'E / Space', action: 'Preview' }, { key: 'Esc', action: 'Deselect' }, ] : [ { key: '↑↓←→', action: 'Navigate' }, { key: 'Click', action: 'Select' }, { key: 'Shift+Click', action: 'Range' }, - { key: 'Space', action: 'Preview' }, + { key: 'E / Space', action: 'Preview' }, { key: '\\', action: 'Filters' }, { key: '/', action: 'Search' }, ] diff --git a/frontend/src/components/loupe/LoupeFilmstrip.tsx b/frontend/src/components/preview/PreviewFilmstrip.tsx similarity index 92% rename from frontend/src/components/loupe/LoupeFilmstrip.tsx rename to frontend/src/components/preview/PreviewFilmstrip.tsx index bc627d0..e044da8 100644 --- a/frontend/src/components/loupe/LoupeFilmstrip.tsx +++ b/frontend/src/components/preview/PreviewFilmstrip.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx' import type { Photo } from '../../types/photo' import { photos as photosApi } from '../../services/api' -interface LoupeFilmstripProps { +interface PreviewFilmstripProps { photos: Photo[] currentIndex: number onSelect: (id: string) => void @@ -11,7 +11,7 @@ interface LoupeFilmstripProps { const CELL_SIZE = 72 -export function LoupeFilmstrip({ photos, currentIndex, onSelect }: LoupeFilmstripProps) { +export function PreviewFilmstrip({ photos, currentIndex, onSelect }: PreviewFilmstripProps) { const activeRef = useRef(null) useEffect(() => { diff --git a/frontend/src/components/loupe/LoupeImage.tsx b/frontend/src/components/preview/PreviewImage.tsx similarity index 81% rename from frontend/src/components/loupe/LoupeImage.tsx rename to frontend/src/components/preview/PreviewImage.tsx index 897ec98..3f4af97 100644 --- a/frontend/src/components/loupe/LoupeImage.tsx +++ b/frontend/src/components/preview/PreviewImage.tsx @@ -1,15 +1,14 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import clsx from 'clsx' import type { Photo } from '../../types/photo' import { - getLoupeImageSrc, - getLoupeFallbackSrc, + getPreviewImageSrc, + getPreviewFallbackSrc, getVideoSrc, isVideo, -} from './loupeSrc' +} from './previewSrc' -interface LoupeImageProps { +interface PreviewImageProps { photo: Photo } @@ -17,14 +16,14 @@ const MIN_SCALE = 1 const MAX_SCALE = 8 const WHEEL_STEP = 1.15 -export function LoupeImage({ photo }: LoupeImageProps) { +export function PreviewImage({ photo }: PreviewImageProps) { if (isVideo(photo)) { - return + return } - return + return } -function LoupeVideo({ photo }: { photo: Photo }) { +function PreviewVideo({ photo }: { photo: Photo }) { return (