Files
mule-image/frontend/src/store/photoStore.ts
dtoro 2679214cb9 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>
2026-04-07 22:47:52 +02:00

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' }),
}))