refactor: rename loupe to preview, bind to E and Space, fix empty viewer
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 <img> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Photo[]>(['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 */}
|
||||
<ToastContainer />
|
||||
|
||||
{/* Loupe overlay — covers TopBar when active */}
|
||||
{viewMode === 'loupe' && <LoupeView />}
|
||||
{/* Preview overlay — covers TopBar when active */}
|
||||
{viewMode === 'preview' && <PreviewView />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user