From b57283e42823609942ad501893a8d12eb575c5d1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 6 Apr 2025 11:17:57 -0500 Subject: [PATCH] Refine image zoom ref handling --- README.md | 2 +- src/components/image/ImageWithFallback.tsx | 24 ++-- src/components/image/ZoomControls.tsx | 26 ++-- src/components/image/useImageZoomControls.ts | 124 ++++++++++--------- src/photo/PhotoLarge.tsx | 13 +- 5 files changed, 102 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index af41b9ea..810bb866 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index a455a838..ab5a6fd3 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -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 & { )} >
{(BLUR_ENABLED && blurDataURL) ? children: ReactNode + selectImageElement?: + (container: HTMLElement | null) => HTMLImageElement | null isEnabled?: boolean shouldZoomOnFKeydown?: boolean }) { - const refContainer = useRef(null); + const refImageContainer = useRef(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 (
{children} - {viewerContainerRef.current - ? createPortal(button, viewerContainerRef.current) + {refViewerContainer.current + ? createPortal(button, refViewerContainer.current) : null}
); diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index 09479415..e6c5ca52 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -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, - isEnabled?: boolean, - shouldExpandOnFKeydown?: boolean, -) { +export default function useImageZoomControls({ + refImageContainer, + selectImageElement, + isEnabled, + shouldZoomOnFKeydown, +} : { + refImageContainer: RefObject +} & Omit, 'ref' | 'children'>) { const viewerRef = useRef(null); - const viewerContainerRef = useRef(null); + const refViewerContainer = useRef(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, }; } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 78996629..63c96a12 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -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(null); const refRecipeButton = useRef(null); @@ -200,6 +206,7 @@ export default function PhotoLarge({ )}>