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>
79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
import { create } from 'zustand'
|
|
import type { Photo } from '../types/photo'
|
|
|
|
type ViewMode = 'grid' | 'preview'
|
|
|
|
interface PhotoStore {
|
|
photos: Photo[]
|
|
selectedPhotos: string[]
|
|
activePhotoId: string | null
|
|
lastSelectedIndex: number | null
|
|
rangeStartIndex: number | null
|
|
viewMode: ViewMode
|
|
|
|
setPhotos: (photos: Photo[]) => void
|
|
selectPhoto: (id: string, index: number) => void
|
|
togglePhotoSelection: (id: string, index: number) => void
|
|
selectRange: (endIndex: number) => void
|
|
deselectPhoto: (id: string) => void
|
|
clearSelection: () => void
|
|
setActivePhoto: (id: string | null) => void
|
|
setViewMode: (mode: ViewMode) => void
|
|
openPreview: (id: string) => void
|
|
closePreview: () => void
|
|
}
|
|
|
|
export const usePhotoStore = create<PhotoStore>((set) => ({
|
|
photos: [],
|
|
selectedPhotos: [],
|
|
activePhotoId: null,
|
|
lastSelectedIndex: null,
|
|
rangeStartIndex: null,
|
|
viewMode: 'grid',
|
|
|
|
setPhotos: (photos) => set({ photos }),
|
|
|
|
selectPhoto: (id, index) => set({
|
|
selectedPhotos: [id],
|
|
activePhotoId: id,
|
|
lastSelectedIndex: index,
|
|
rangeStartIndex: index,
|
|
}),
|
|
|
|
togglePhotoSelection: (id, index) => set((state) => {
|
|
const isSelected = state.selectedPhotos.includes(id)
|
|
return {
|
|
selectedPhotos: isSelected
|
|
? state.selectedPhotos.filter(photoId => photoId !== id)
|
|
: [...state.selectedPhotos, id],
|
|
lastSelectedIndex: index,
|
|
rangeStartIndex: isSelected ? state.rangeStartIndex : index,
|
|
}
|
|
}),
|
|
|
|
selectRange: (endIndex) => {
|
|
// Note: The actual range selection logic should be handled in the Timeline component
|
|
// which has access to the photos array
|
|
set({
|
|
lastSelectedIndex: endIndex,
|
|
})
|
|
},
|
|
|
|
deselectPhoto: (id) => set((state) => ({
|
|
selectedPhotos: state.selectedPhotos.filter(photoId => photoId !== id)
|
|
})),
|
|
|
|
clearSelection: () => set({
|
|
selectedPhotos: [],
|
|
lastSelectedIndex: null,
|
|
rangeStartIndex: null,
|
|
}),
|
|
|
|
setActivePhoto: (id) => set({ activePhotoId: id }),
|
|
|
|
setViewMode: (mode) => set({ viewMode: mode }),
|
|
|
|
openPreview: (id) => set({ viewMode: 'preview', activePhotoId: id }),
|
|
|
|
closePreview: () => set({ viewMode: 'grid' }),
|
|
})) |