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>
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
import { photos as photosApi } from '../../services/api'
|
|
import type { Photo } from '../../types/photo'
|
|
|
|
const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.webm', '.mkv', '.m4v']
|
|
|
|
export function isVideo(photo: Photo): boolean {
|
|
if (photo.media_type === 'video') return true
|
|
const lower = photo.filepath.toLowerCase()
|
|
return VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext))
|
|
}
|
|
|
|
/**
|
|
* Pick the best display URL for a still photo in the preview view.
|
|
*
|
|
* Always uses the /proxy endpoint, which the backend resolves to:
|
|
* - the original file for web-safe formats (JPEG/PNG/WebP/GIF)
|
|
* - a transcoded full-res WebP for RAW/HEIC/TIFF (cached on first hit)
|
|
*
|
|
* Videos go through `getVideoSrc` instead and use /original directly.
|
|
*/
|
|
export function getPreviewImageSrc(photo: Photo): string {
|
|
return photosApi.getProxyUrl(photo.id)
|
|
}
|
|
|
|
/** Fallback used when the proxy endpoint fails or 404s — shows the 1280px
|
|
* large thumbnail so the user still sees something. */
|
|
export function getPreviewFallbackSrc(photo: Photo): string {
|
|
return photosApi.getThumbnailUrl(photo.id, 'large')
|
|
}
|
|
|
|
export function getVideoSrc(photo: Photo): string {
|
|
return photosApi.getOriginalUrl(photo.id)
|
|
}
|