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:
55
frontend/src/hooks/usePhotosQuery.ts
Normal file
55
frontend/src/hooks/usePhotosQuery.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import axios from 'axios'
|
||||
import { useFilterStore, filtersToParams } from '../store/filterStore'
|
||||
import type { Photo } from '../types/photo'
|
||||
|
||||
/**
|
||||
* Single source of truth for the timeline photos query. Both Timeline and
|
||||
* PreviewView call this so they share one cache entry — previously
|
||||
* PreviewView looked the cache up by key directly, which broke the moment
|
||||
* Timeline's key gained the filter params.
|
||||
*/
|
||||
export function usePhotosQuery() {
|
||||
const q = useFilterStore((s) => s.q)
|
||||
const dateFrom = useFilterStore((s) => s.dateFrom)
|
||||
const dateTo = useFilterStore((s) => s.dateTo)
|
||||
const mediaTypes = useFilterStore((s) => s.mediaTypes)
|
||||
const ratingMin = useFilterStore((s) => s.ratingMin)
|
||||
const colorLabel = useFilterStore((s) => s.colorLabel)
|
||||
const flag = useFilterStore((s) => s.flag)
|
||||
|
||||
const filterParams = useMemo(
|
||||
() =>
|
||||
filtersToParams({
|
||||
q,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
mediaTypes,
|
||||
ratingMin,
|
||||
colorLabel,
|
||||
flag,
|
||||
}),
|
||||
[q, dateFrom, dateTo, mediaTypes, ratingMin, colorLabel, flag]
|
||||
)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['photos', filterParams],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<{ photos: Photo[]; total: number }>(
|
||||
'http://localhost:8001/api/v1/photos',
|
||||
{
|
||||
params: {
|
||||
page: 1,
|
||||
per_page: 500,
|
||||
sort: 'taken_at',
|
||||
order: 'desc',
|
||||
...filterParams,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data.photos || []
|
||||
},
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user