ui(preview): load 1280px thumb first, upgrade to full-res in bg
The /proxy endpoint is slow on first hit, especially for RAW/HEIC where it transcodes synchronously. Preview now renders the pre-generated large thumb immediately, then preloads /proxy via Image() and swaps src when ready, so zoom (Z key / wheel) still reaches the original pixels.
This commit is contained in:
@@ -3,7 +3,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import type { Photo } from '../../types/photo'
|
||||
import {
|
||||
getPreviewImageSrc,
|
||||
getPreviewFallbackSrc,
|
||||
getPreviewFullResSrc,
|
||||
getVideoSrc,
|
||||
isVideo,
|
||||
} from './previewSrc'
|
||||
@@ -44,7 +44,9 @@ function PreviewVideo({ photo }: { photo: Photo }) {
|
||||
|
||||
function PreviewStillImage({ photo }: { photo: Photo }) {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [usingFallback, setUsingFallback] = useState(false)
|
||||
// Progressive enhancement: render the 1280px thumb first, then preload the
|
||||
// full-res /proxy in the background and flip this to true once decoded.
|
||||
const [usingFullRes, setUsingFullRes] = useState(false)
|
||||
|
||||
// scale=1 means "fit to viewport". Anything >1 zooms in; we don't allow <1
|
||||
// because the fit size already fills the viewport.
|
||||
@@ -53,21 +55,35 @@ function PreviewStillImage({ photo }: { photo: Photo }) {
|
||||
const dragStateRef = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
|
||||
// Reset everything when the photo changes.
|
||||
const thumbSrc = getPreviewImageSrc(photo)
|
||||
const fullResSrc = getPreviewFullResSrc(photo)
|
||||
const src = usingFullRes ? fullResSrc : thumbSrc
|
||||
|
||||
// Reset everything when the photo changes, then start the background
|
||||
// full-res preload. Cancel the preloader's callback on unmount/change so a
|
||||
// late-arriving onload from the previous photo can't flip state for the
|
||||
// current one.
|
||||
useEffect(() => {
|
||||
setLoaded(false)
|
||||
setUsingFallback(false)
|
||||
setUsingFullRes(false)
|
||||
setScale(1)
|
||||
setOffset({ x: 0, y: 0 })
|
||||
}, [photo.id])
|
||||
|
||||
const primarySrc = getPreviewImageSrc(photo)
|
||||
const fallbackSrc = getPreviewFallbackSrc(photo)
|
||||
const src = usingFallback ? fallbackSrc : primarySrc
|
||||
const preloader = new Image()
|
||||
preloader.onload = () => setUsingFullRes(true)
|
||||
preloader.src = fullResSrc
|
||||
return () => {
|
||||
preloader.onload = null
|
||||
}
|
||||
}, [photo.id, fullResSrc])
|
||||
|
||||
// If the thumb 404s (e.g. derivative not yet generated for a fresh import),
|
||||
// jump straight to the full-res — the same <img> element will retry against
|
||||
// /proxy. If /proxy also fails, the browser shows its broken-image icon and
|
||||
// we surface no further fallback.
|
||||
const handleError = () => {
|
||||
if (!usingFallback && primarySrc !== fallbackSrc) {
|
||||
setUsingFallback(true)
|
||||
if (!usingFullRes) {
|
||||
setUsingFullRes(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +166,7 @@ function PreviewStillImage({ photo }: { photo: Photo }) {
|
||||
>
|
||||
<img
|
||||
ref={imgRef}
|
||||
key={`${photo.id}-${usingFallback}`}
|
||||
key={photo.id}
|
||||
src={src}
|
||||
alt={photo.filename}
|
||||
loading="eager"
|
||||
|
||||
@@ -10,22 +10,29 @@ export function isVideo(photo: Photo): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the best display URL for a still photo in the preview view.
|
||||
* Initial display URL for a still photo in the preview view: the 1280px
|
||||
* `large` WebP thumbnail. Cheap and always pre-generated, so the preview
|
||||
* appears instantly even for RAW/HEIC photos whose full-res /proxy would
|
||||
* otherwise transcode synchronously on first hit.
|
||||
*
|
||||
* 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)
|
||||
* Pair with `getPreviewFullResSrc` for progressive enhancement: load this
|
||||
* first, then preload the full-res in the background and swap it in.
|
||||
*
|
||||
* Videos go through `getVideoSrc` instead and use /original directly.
|
||||
*/
|
||||
export function getPreviewImageSrc(photo: Photo): string {
|
||||
return photosApi.getProxyUrl(photo.id)
|
||||
return photosApi.getThumbnailUrl(photo.id, 'large')
|
||||
}
|
||||
|
||||
/** 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')
|
||||
/**
|
||||
* Full-resolution URL fetched in the background after the thumbnail is
|
||||
* already visible. The /proxy endpoint serves the original file for
|
||||
* web-safe formats (JPEG/PNG/WebP/GIF) or a transcoded full-res WebP for
|
||||
* RAW/HEIC/TIFF (cached on first hit). Used so Z-key / wheel zoom stays
|
||||
* pixel-sharp once the upgrade lands.
|
||||
*/
|
||||
export function getPreviewFullResSrc(photo: Photo): string {
|
||||
return photosApi.getProxyUrl(photo.id)
|
||||
}
|
||||
|
||||
export function getVideoSrc(photo: Photo): string {
|
||||
|
||||
Reference in New Issue
Block a user