Refine image zoom ref handling

This commit is contained in:
Sam Becker 2025-04-06 11:17:57 -05:00
parent 0ee0e120ca
commit b57283e428
5 changed files with 102 additions and 87 deletions

View File

@ -300,7 +300,7 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider
> If you don't see a recipe, first try syncing your photo from the ••• menu, or from `/admin/photos`. If the data looks incorrect, open an issue with the file in question attached in order for it to be investigated. Fujifilm file specifications have evolved over time and recipe parsing may need to be adjusted based on camera model/vintage.
#### How do I hide Fujifilm content such as a recipes and film simulations?
> This can be accomplished by setting `NEXT_PUBLIC_CATEGORY_VISIBILITY` (which has a default value of `tags, cameras, recipes, simulations`) to simply `tags, cameras`.
> This can be accomplished by setting `NEXT_PUBLIC_CATEGORY_VISIBILITY` (which has a default value of `tags,cameras,lenses,recipes,films`) to `tags,cameras,lenses`.
#### Why do my images appear flipped/rotated incorrectly?
> For a number of reasons, only EXIF orientations: 1, 3, 6, and 8 are supported. Orientations 2, 4, 5, and 7—which make use of mirroring—are not supported.

View File

@ -7,19 +7,17 @@ import { clsx} from 'clsx/lite';
import Image, { ImageProps } from 'next/image';
import { useCallback, useEffect, useRef, useState } from 'react';
export default function ImageWithFallback(props: ImageProps & {
export default function ImageWithFallback({
className,
classNameImage = 'object-cover h-full',
priority,
blurDataURL,
blurCompatibilityLevel = 'low',
...props
}: ImageProps & {
blurCompatibilityLevel?: 'none' | 'low' | 'high'
classNameImage?: string
}) {
const {
className,
classNameImage = 'object-cover h-full',
priority,
blurDataURL,
blurCompatibilityLevel = 'low',
...rest
} = props;
const { shouldDebugImageFallbacks } = useAppState();
const [wasCached, setWasCached] = useState(true);
@ -72,7 +70,7 @@ export default function ImageWithFallback(props: ImageProps & {
)}
>
<Image {...{
...rest,
...props,
ref: imgRef,
priority,
className: classNameImage,
@ -81,7 +79,7 @@ export default function ImageWithFallback(props: ImageProps & {
}} />
<div className={clsx(
'@container',
'absolute inset-0',
'absolute inset-0 pointer-events-none',
'overflow-hidden',
(showFallback || shouldDebugImageFallbacks) &&
'transition-opacity duration-300 ease-in',
@ -92,7 +90,7 @@ export default function ImageWithFallback(props: ImageProps & {
)}>
{(BLUR_ENABLED && blurDataURL)
? <img {...{
...rest,
...props,
src: blurDataURL,
className: clsx(
getBlurClass(),

View File

@ -12,27 +12,27 @@ export type ZoomControlsRef = {
export default function ZoomControls({
ref,
children,
isEnabled,
shouldZoomOnFKeydown,
...props
}: {
ref?: RefObject<ZoomControlsRef | null>
children: ReactNode
selectImageElement?:
(container: HTMLElement | null) => HTMLImageElement | null
isEnabled?: boolean
shouldZoomOnFKeydown?: boolean
}) {
const refContainer = useRef<HTMLDivElement>(null);
const refImageContainer = useRef<HTMLDivElement>(null);
const {
open,
reset,
zoomTo,
zoomLevel,
viewerContainerRef,
} = useImageZoomControls(
refContainer,
isEnabled,
shouldZoomOnFKeydown,
);
refViewerContainer,
} = useImageZoomControls({
refImageContainer,
...props,
});
useEffect(() => {
if (ref) { ref.current = { open, zoomTo }; }
@ -57,12 +57,12 @@ export default function ZoomControls({
return (
<div
ref={refContainer}
className={clsx('h-full', isEnabled && 'cursor-zoom-in')}
ref={refImageContainer}
className={clsx('h-full', props.isEnabled && 'cursor-zoom-in')}
>
{children}
{viewerContainerRef.current
? createPortal(button, viewerContainerRef.current)
{refViewerContainer.current
? createPortal(button, refViewerContainer.current)
: null}
</div>
);

View File

@ -1,17 +1,28 @@
import useMetaThemeColor from '@/utility/useMetaThemeColor';
import { useAppState } from '@/state/AppState';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
import {
ComponentProps,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import Viewer from 'viewerjs';
import ZoomControls from './ZoomControls';
export default function useImageZoomControls(
imageRef: RefObject<HTMLDivElement | null>,
isEnabled?: boolean,
shouldExpandOnFKeydown?: boolean,
) {
export default function useImageZoomControls({
refImageContainer,
selectImageElement,
isEnabled,
shouldZoomOnFKeydown,
} : {
refImageContainer: RefObject<HTMLElement | null>
} & Omit<ComponentProps<typeof ZoomControls>, 'ref' | 'children'>) {
const viewerRef = useRef<Viewer | null>(null);
const viewerContainerRef = useRef<HTMLDivElement>(null);
const refViewerContainer = useRef<HTMLDivElement>(null);
const { setShouldRespondToKeyboardCommands } = useAppState();
@ -37,69 +48,68 @@ export default function useImageZoomControls(
// On 'F' keydown, toggle fullscreen
const handleKeyDown = useCallback(() => {
if (shouldExpandOnFKeydown) { open(); }
}, [shouldExpandOnFKeydown, open]);
if (shouldZoomOnFKeydown) { open(); }
}, [shouldZoomOnFKeydown, open]);
useKeydownHandler(handleKeyDown, ['F']);
const initialize = useCallback(() => {
if (imageRef.current && isEnabled) {
viewerRef.current = new Viewer(imageRef.current, {
navbar: false,
title: false,
toolbar: {
zoomIn: 1,
reset: 2,
zoomOut: 3,
},
ready: ({ target }) => {
viewerContainerRef.current =
(target as any).viewer.viewer as HTMLDivElement;
},
url: (image: HTMLImageElement) => {
// Addresses Safari bug where images don't load
image.loading = 'eager';
return image.src;
},
show: () => {
setShouldRespondToKeyboardCommands?.(false);
setColorLight('#000');
},
hide: () => {
// Optimizes Safari status bar animation
setTimeout(() => setColorLight(undefined), 300);
},
hidden: () => {
setShouldRespondToKeyboardCommands?.(true);
},
zoom: ({ detail: { ratio } }) => {
setZoomLevel(ratio);
},
});
useEffect(() => {
if (isEnabled) {
const imageRef = (
selectImageElement?.(refImageContainer.current) ??
refImageContainer.current
);
if (imageRef) {
viewerRef.current = new Viewer(imageRef, {
navbar: false,
title: false,
toolbar: {
zoomIn: 1,
reset: 2,
zoomOut: 3,
},
ready: ({ target }) => {
refViewerContainer.current =
(target as any).viewer.viewer as HTMLDivElement;
},
url: (image: HTMLImageElement) => {
// Addresses Safari bug where images don't load
image.loading = 'eager';
return image.src;
},
show: () => {
setShouldRespondToKeyboardCommands?.(false);
setColorLight('#000');
},
hide: () => {
// Optimizes Safari status bar animation
setTimeout(() => setColorLight(undefined), 300);
},
hidden: () => {
setShouldRespondToKeyboardCommands?.(true);
},
zoom: ({ detail: { ratio } }) => {
setZoomLevel(ratio);
},
});
return () => {
viewerRef.current?.destroy();
viewerRef.current = null;
};
}
}
}, [
imageRef,
isEnabled,
refImageContainer,
selectImageElement,
setShouldRespondToKeyboardCommands,
]);
const cleanUp = useCallback(() => {
viewerRef.current?.destroy();
viewerRef.current = null;
}, []);
useEffect(() => {
initialize();
return cleanUp;
}, [initialize, cleanUp]);
return {
initialize,
cleanUp,
open,
close,
reset,
zoomTo,
zoomLevel,
viewerContainerRef,
refViewerContainer,
};
}

View File

@ -34,7 +34,7 @@ import {
} from '@/app/config';
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
import { RevalidatePhoto } from './InfinitePhotoScroll';
import { useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import useVisible from '@/utility/useVisible';
import PhotoDate from './PhotoDate';
import { useAppState } from '@/state/AppState';
@ -67,7 +67,7 @@ export default function PhotoLarge({
showLens = true,
showFilm = true,
showRecipe = true,
showZoomControls: showZoomControlsProp = true,
showZoomControls: _showZoomControls = true,
shouldZoomOnFKeydown = true,
shouldShare = true,
shouldShareCamera,
@ -123,7 +123,13 @@ export default function PhotoLarge({
filmCount,
} = useCategoryCountsForPhoto(photo);
const showZoomControls = showZoomControlsProp && areZoomControlsShown;
const showZoomControls = _showZoomControls && areZoomControlsShown;
const selectZoomImageElement = useCallback(
(container: HTMLElement | null) => Array
.from(container?.getElementsByTagName('img') ?? [])
// Ignore fallback blur images
.filter((img) => !img.src.startsWith('data:image'))[0]
, []);
const refRecipe = useRef<HTMLDivElement>(null);
const refRecipeButton = useRef<HTMLButtonElement>(null);
@ -200,6 +206,7 @@ export default function PhotoLarge({
)}>
<ZoomControls
ref={zoomControlsRef}
selectImageElement={selectZoomImageElement}
{...{ isEnabled: showZoomControls, shouldZoomOnFKeydown }}
>
<ImageLarge