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 type { Photo } from '../../types/photo'
|
||||||
import {
|
import {
|
||||||
getPreviewImageSrc,
|
getPreviewImageSrc,
|
||||||
getPreviewFallbackSrc,
|
getPreviewFullResSrc,
|
||||||
getVideoSrc,
|
getVideoSrc,
|
||||||
isVideo,
|
isVideo,
|
||||||
} from './previewSrc'
|
} from './previewSrc'
|
||||||
@@ -44,7 +44,9 @@ function PreviewVideo({ photo }: { photo: Photo }) {
|
|||||||
|
|
||||||
function PreviewStillImage({ photo }: { photo: Photo }) {
|
function PreviewStillImage({ photo }: { photo: Photo }) {
|
||||||
const [loaded, setLoaded] = useState(false)
|
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
|
// scale=1 means "fit to viewport". Anything >1 zooms in; we don't allow <1
|
||||||
// because the fit size already fills the viewport.
|
// 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 dragStateRef = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null)
|
||||||
const imgRef = useRef<HTMLImageElement>(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(() => {
|
useEffect(() => {
|
||||||
setLoaded(false)
|
setLoaded(false)
|
||||||
setUsingFallback(false)
|
setUsingFullRes(false)
|
||||||
setScale(1)
|
setScale(1)
|
||||||
setOffset({ x: 0, y: 0 })
|
setOffset({ x: 0, y: 0 })
|
||||||
}, [photo.id])
|
|
||||||
|
|
||||||
const primarySrc = getPreviewImageSrc(photo)
|
const preloader = new Image()
|
||||||
const fallbackSrc = getPreviewFallbackSrc(photo)
|
preloader.onload = () => setUsingFullRes(true)
|
||||||
const src = usingFallback ? fallbackSrc : primarySrc
|
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 = () => {
|
const handleError = () => {
|
||||||
if (!usingFallback && primarySrc !== fallbackSrc) {
|
if (!usingFullRes) {
|
||||||
setUsingFallback(true)
|
setUsingFullRes(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +166,7 @@ function PreviewStillImage({ photo }: { photo: Photo }) {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
key={`${photo.id}-${usingFallback}`}
|
key={photo.id}
|
||||||
src={src}
|
src={src}
|
||||||
alt={photo.filename}
|
alt={photo.filename}
|
||||||
loading="eager"
|
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:
|
* Pair with `getPreviewFullResSrc` for progressive enhancement: load this
|
||||||
* - the original file for web-safe formats (JPEG/PNG/WebP/GIF)
|
* first, then preload the full-res in the background and swap it in.
|
||||||
* - a transcoded full-res WebP for RAW/HEIC/TIFF (cached on first hit)
|
|
||||||
*
|
*
|
||||||
* Videos go through `getVideoSrc` instead and use /original directly.
|
* Videos go through `getVideoSrc` instead and use /original directly.
|
||||||
*/
|
*/
|
||||||
export function getPreviewImageSrc(photo: Photo): string {
|
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. */
|
* Full-resolution URL fetched in the background after the thumbnail is
|
||||||
export function getPreviewFallbackSrc(photo: Photo): string {
|
* already visible. The /proxy endpoint serves the original file for
|
||||||
return photosApi.getThumbnailUrl(photo.id, 'large')
|
* 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 {
|
export function getVideoSrc(photo: Photo): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user