From b74f83694b9186d968a726e923bae903ad74c649 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 13 Jan 2025 18:10:07 +0100 Subject: [PATCH 01/11] Add fullscreen state management to AppState --- src/state/AppState.ts | 3 +++ src/state/AppStateProvider.tsx | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 69975e9e..9dcde3ca 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -39,6 +39,9 @@ export interface AppStateContext { setShouldDebugImageFallbacks?: Dispatch> shouldShowBaselineGrid?: boolean setShouldShowBaselineGrid?: Dispatch> + // FULLSCREEN + isFullscreen?: boolean + setIsFullscreen?: Dispatch> } export const AppStateContext = createContext({}); diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 8a8f1da8..5e527c74 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -52,6 +52,9 @@ export default function AppStateProvider({ useState(false); const [shouldShowBaselineGrid, setShouldShowBaselineGrid] = useState(false); + // FULLSCREEN + const [isFullscreen, setIsFullscreen] = + useState(false); const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); @@ -120,6 +123,9 @@ export default function AppStateProvider({ setShouldDebugImageFallbacks, shouldShowBaselineGrid, setShouldShowBaselineGrid, + // FULLSCREEN + isFullscreen, + setIsFullscreen, }} > {children} From 416a45bd2f8a1d55fd1b4413a138470ba6ac21e2 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 13 Jan 2025 18:10:44 +0100 Subject: [PATCH 02/11] Add fullscreen toggle to large images --- src/components/FullscreenButton.tsx | 54 ++++++++++++++++++++++ src/components/image/ImageLarge.tsx | 1 + src/components/image/ImageWithFallback.tsx | 6 ++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/components/FullscreenButton.tsx diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx new file mode 100644 index 00000000..fcbab1b6 --- /dev/null +++ b/src/components/FullscreenButton.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { useEffect, RefObject } from 'react'; +import { MdFullscreen, MdFullscreenExit } from 'react-icons/md'; +import { clsx } from 'clsx/lite'; +import { useAppState } from '@/state/AppState'; +import LoaderButton from './primitives/LoaderButton'; + +export default function FullscreenButton({ + className, + imageRef, +}: { + className?: string; + imageRef: RefObject; +}) { + const { isFullscreen, setIsFullscreen } = useAppState(); + + const toggleFullscreen = async () => { + if (!document.fullscreenElement) { + await imageRef.current?.requestFullscreen(); + setIsFullscreen && setIsFullscreen(true); + } else { + await document.exitFullscreen(); + setIsFullscreen && setIsFullscreen(false); + } + }; + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'f' || event.key === 'F') { + toggleFullscreen(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [toggleFullscreen]); + + return ( + : } + spinnerColor='light-gray' + styleAs='link' + onClick={toggleFullscreen} + /> + ); +} diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index 18569d92..d7d178d0 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -13,6 +13,7 @@ export default function ImageLarge(props: ImageProps) { blurCompatibilityLevel: blurCompatibilityMode ? 'high' : 'none', width: IMAGE_WIDTH_LARGE, height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio), + allowFullscreen: true, }} /> ); }; diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index 23457add..5d18738b 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -6,10 +6,12 @@ import { useAppState } from '@/state/AppState'; import { clsx} from 'clsx/lite'; import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; +import FullscreenButton from '../FullscreenButton'; export default function ImageWithFallback(props: ImageProps & { blurCompatibilityLevel?: 'none' | 'low' | 'high' imgClassName?: string + allowFullscreen?: boolean }) { const { className, @@ -17,6 +19,7 @@ export default function ImageWithFallback(props: ImageProps & { blurDataURL, blurCompatibilityLevel = 'low', imgClassName = 'object-cover h-full', + allowFullscreen, ...rest } = props; @@ -104,6 +107,7 @@ export default function ImageWithFallback(props: ImageProps & { onLoad, onError, }} /> - + {allowFullscreen && } + ); } From cc30c2ea49ce76c06f8bd361f17abef916c1487b Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 13 Jan 2025 18:34:11 +0100 Subject: [PATCH 03/11] Update FullscreenButton.tsx and fix linting --- src/components/FullscreenButton.tsx | 45 ++++++++++++++-------- src/components/image/ImageWithFallback.tsx | 2 +- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx index fcbab1b6..e5794184 100644 --- a/src/components/FullscreenButton.tsx +++ b/src/components/FullscreenButton.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, RefObject } from 'react'; +import { useEffect, useCallback, RefObject } from 'react'; import { MdFullscreen, MdFullscreenExit } from 'react-icons/md'; import { clsx } from 'clsx/lite'; import { useAppState } from '@/state/AppState'; @@ -15,39 +15,52 @@ export default function FullscreenButton({ }) { const { isFullscreen, setIsFullscreen } = useAppState(); - const toggleFullscreen = async () => { + // Toggle fullscreen mode + const toggleFullscreen = useCallback(async () => { if (!document.fullscreenElement) { await imageRef.current?.requestFullscreen(); - setIsFullscreen && setIsFullscreen(true); + setIsFullscreen?.(true); } else { await document.exitFullscreen(); - setIsFullscreen && setIsFullscreen(false); + setIsFullscreen?.(false); } - }; + }, [imageRef, setIsFullscreen]); + + // Toggle fullscreen on 'f' key press + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key === 'f' || event.key === 'F') { + toggleFullscreen(); + } + }, [toggleFullscreen]); + + // Handle fullscreen change (e.g, switching tabs in fullscreen mode) + const handleFullscreenChange = useCallback(() => { + if (!document.fullscreenElement) { + setIsFullscreen?.(false); + } + }, [setIsFullscreen]); useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'f' || event.key === 'F') { - toggleFullscreen(); - } - }; - document.addEventListener('keydown', handleKeyDown); + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => { document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('fullscreenchange', handleFullscreenChange); }; - }, [toggleFullscreen]); + }, [handleKeyDown, handleFullscreenChange]); return ( : } - spinnerColor='light-gray' - styleAs='link' + icon={isFullscreen ? + : } + spinnerColor="light-gray" + styleAs="link" onClick={toggleFullscreen} /> ); diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index 5d18738b..0143c3f8 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -108,6 +108,6 @@ export default function ImageWithFallback(props: ImageProps & { onError, }} /> {allowFullscreen && } - + ); } From 4a7c988f542d31d1891d9320fa7cf61d6c4c0945 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Wed, 15 Jan 2025 19:23:55 +0100 Subject: [PATCH 04/11] Add viewerjs support --- package.json | 3 +- pnpm-lock.yaml | 8 +++ src/app/p/[photoId]/page.tsx | 9 ++- src/components/image/ImageLarge.tsx | 1 - src/components/image/ImageWithFallback.tsx | 65 ++++++++++++++++++---- src/components/image/index.ts | 1 + src/photo/PhotoDetailPage.tsx | 3 + src/photo/PhotoLarge.tsx | 3 + 8 files changed, 78 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 9816e848..ec73a2d2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "sonner": "^1.7.1", "swr": "^2.3.0", "ts-exif-parser": "^0.2.2", - "use-debounce": "^10.0.4" + "use-debounce": "^10.0.4", + "viewerjs": "^1.11.7" }, "devDependencies": { "@next/bundle-analyzer": "15.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3042825e..47842bd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: use-debounce: specifier: ^10.0.4 version: 10.0.4(react@19.0.0) + viewerjs: + specifier: ^1.11.7 + version: 1.11.7 devDependencies: '@next/bundle-analyzer': specifier: 15.1.4 @@ -4255,6 +4258,9 @@ packages: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} + viewerjs@1.11.7: + resolution: {integrity: sha512-0JuVqOmL5v1jmEAlG5EBDR3XquxY8DWFQbFMprOXgaBB0F7Q/X9xWdEaQc59D8xzwkdUgXEMSSknTpriq95igg==} + vue@3.4.27: resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==} peerDependencies: @@ -9467,6 +9473,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + viewerjs@1.11.7: {} + vue@3.4.27(typescript@5.7.3): dependencies: '@vue/compiler-dom': 3.4.27 diff --git a/src/app/p/[photoId]/page.tsx b/src/app/p/[photoId]/page.tsx index decfb273..26faa932 100644 --- a/src/app/p/[photoId]/page.tsx +++ b/src/app/p/[photoId]/page.tsx @@ -77,6 +77,13 @@ export default async function PhotoPage({ if (!photo) { redirect(PATH_ROOT); } return ( - + ); } diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index d7d178d0..18569d92 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -13,7 +13,6 @@ export default function ImageLarge(props: ImageProps) { blurCompatibilityLevel: blurCompatibilityMode ? 'high' : 'none', width: IMAGE_WIDTH_LARGE, height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio), - allowFullscreen: true, }} /> ); }; diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index 0143c3f8..07279f87 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -3,15 +3,18 @@ /* eslint-disable jsx-a11y/alt-text */ import { BLUR_ENABLED } from '@/site/config'; import { useAppState } from '@/state/AppState'; -import { clsx} from 'clsx/lite'; +import { clsx } from 'clsx/lite'; import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; import FullscreenButton from '../FullscreenButton'; +import Viewer from 'viewerjs'; +import 'viewerjs/dist/viewer.css'; export default function ImageWithFallback(props: ImageProps & { blurCompatibilityLevel?: 'none' | 'low' | 'high' imgClassName?: string allowFullscreen?: boolean + enableImageActions?: boolean }) { const { className, @@ -19,7 +22,7 @@ export default function ImageWithFallback(props: ImageProps & { blurDataURL, blurCompatibilityLevel = 'low', imgClassName = 'object-cover h-full', - allowFullscreen, + enableImageActions = false, ...rest } = props; @@ -34,7 +37,10 @@ export default function ImageWithFallback(props: ImageProps & { const [hideFallback, setHideFallback] = useState(false); - const imgRef = useRef(null); + const containerRef = useRef(null); + const viewerRef = useRef(null); + const imgRef = useRef(null); + const { isFullscreen } = useAppState(); useEffect(() => { const timeout = setTimeout( @@ -53,6 +59,37 @@ export default function ImageWithFallback(props: ImageProps & { } }, [isLoading, didError]); + useEffect(() => { + if (containerRef.current && enableImageActions) { + viewerRef.current = new Viewer(containerRef.current, { + inline: false, + button: true, + navbar: false, + title: false, + toolbar: { + zoomIn: 1, + zoomOut: 1, + oneToOne: 1, + reset: 1, + prev: 0, + play: { + show: 0, + size: 'large', + }, + next: 0, + rotateLeft: 1, + rotateRight: 1, + flipHorizontal: 1, + flipVertical: 1, + tooltip: 1, + }, + }); + return () => { + viewerRef.current?.destroy(); + }; + } + }, [enableImageActions]); + const showFallback = !wasCached && !hideFallback; @@ -73,6 +110,7 @@ export default function ImageWithFallback(props: ImageProps & { className, 'flex relative', )} + ref={containerRef} > {(showFallback || shouldDebugImageFallbacks) &&
}
} - - {allowFullscreen && } + + {enableImageActions && } ); } diff --git a/src/components/image/index.ts b/src/components/image/index.ts index eb7d6582..8a46d1ac 100644 --- a/src/components/image/index.ts +++ b/src/components/image/index.ts @@ -14,4 +14,5 @@ export interface ImageProps { alt: string blurDataURL?: string priority?: boolean + enableImageActions?: boolean } diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index beb67cf4..da945e7e 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -25,6 +25,7 @@ export default function PhotoDetailPage({ dateRange, shouldShare, includeFavoriteInAdminMenu, + enableImageActions, }: { photo: Photo photos: Photo[] @@ -34,6 +35,7 @@ export default function PhotoDetailPage({ dateRange?: PhotoDateRange shouldShare?: boolean includeFavoriteInAdminMenu?: boolean + enableImageActions?: boolean } & PhotoSetCategory) { let customHeader: JSX.Element | undefined; @@ -112,6 +114,7 @@ export default function PhotoDetailPage({ shouldShareSimulation={simulation !== undefined} shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} + enableImageActions={enableImageActions} />, ]} /> diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 32befc1b..f4608afb 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -55,6 +55,7 @@ export default function PhotoLarge({ shouldShareFocalLength, includeFavoriteInAdminMenu, onVisible, + enableImageActions = false, }: { photo: Photo className?: string @@ -75,6 +76,7 @@ export default function PhotoLarge({ shouldScrollOnShare?: boolean includeFavoriteInAdminMenu?: boolean onVisible?: () => void + enableImageActions?: boolean }) { const ref = useRef(null); @@ -143,6 +145,7 @@ export default function PhotoLarge({ blurDataURL={photo.blurData} blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)} priority={priority} + enableImageActions={enableImageActions} /> } From 550d17f4904b60df3a8ffc41d94b58668f90211f Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Wed, 22 Jan 2025 04:05:28 +0100 Subject: [PATCH 05/11] Update viewerjs styling --- src/components/image/ImageWithFallback.tsx | 98 +++++++++++----------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index 07279f87..c2dd4801 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -69,18 +69,11 @@ export default function ImageWithFallback(props: ImageProps & { toolbar: { zoomIn: 1, zoomOut: 1, - oneToOne: 1, reset: 1, - prev: 0, play: { show: 0, size: 'large', }, - next: 0, - rotateLeft: 1, - rotateRight: 1, - flipHorizontal: 1, - flipVertical: 1, tooltip: 1, }, }); @@ -105,50 +98,55 @@ export default function ImageWithFallback(props: ImageProps & { }; return ( -
- {(showFallback || shouldDebugImageFallbacks) && -
- {(BLUR_ENABLED && blurDataURL) - ? - :
} -
} - + +
- {enableImageActions && } -
+ ref={containerRef} + > + {(showFallback || shouldDebugImageFallbacks) && +
+ {(BLUR_ENABLED && blurDataURL) + ? + :
} +
} + + {enableImageActions && } +
+ ); } From 805ac69fa9d54f41b4a5d1704ce97658a8864b15 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sat, 25 Jan 2025 17:30:58 +0100 Subject: [PATCH 06/11] Enable fullscreen and zoom actions for images via env variable --- README.md | 1 + src/app/p/[photoId]/page.tsx | 1 - src/photo/PhotoDetailPage.tsx | 5 ++--- src/site/SiteChecklistClient.tsx | 10 ++++++++++ src/site/config.ts | 3 +++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d1bb34be..e6b2d455 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint) - `NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS = 1` ensures large thumbnails on photo grid views - `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top) +- `NEXT_PUBLIC_IMAGE_ACTIONS = 1` enables fullscreen and zoom actions when clicking on an image ## Alternate storage providers diff --git a/src/app/p/[photoId]/page.tsx b/src/app/p/[photoId]/page.tsx index 26faa932..42e53c78 100644 --- a/src/app/p/[photoId]/page.tsx +++ b/src/app/p/[photoId]/page.tsx @@ -82,7 +82,6 @@ export default async function PhotoPage({ photo, photos, photosGrid, - enableImageActions: true, }} /> ); diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index da945e7e..aab19b82 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -11,6 +11,7 @@ import HiddenHeader from '@/tag/HiddenHeader'; import FocalLengthHeader from '@/focal/FocalLengthHeader'; import PhotoHeader from './PhotoHeader'; import { JSX } from 'react'; +import { IMAGE_ACTIONS_ENABLED } from '@/site/config'; export default function PhotoDetailPage({ photo, @@ -25,7 +26,6 @@ export default function PhotoDetailPage({ dateRange, shouldShare, includeFavoriteInAdminMenu, - enableImageActions, }: { photo: Photo photos: Photo[] @@ -35,7 +35,6 @@ export default function PhotoDetailPage({ dateRange?: PhotoDateRange shouldShare?: boolean includeFavoriteInAdminMenu?: boolean - enableImageActions?: boolean } & PhotoSetCategory) { let customHeader: JSX.Element | undefined; @@ -114,7 +113,7 @@ export default function PhotoDetailPage({ shouldShareSimulation={simulation !== undefined} shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} - enableImageActions={enableImageActions} + enableImageActions={IMAGE_ACTIONS_ENABLED} />, ]} /> diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 7cadb46a..e291732c 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -66,6 +66,7 @@ export default function SiteChecklistClient({ isPublicApiEnabled, arePublicDownloadsEnabled, isOgTextBottomAligned, + isImageActionsEnabled, gridAspectRatio, hasGridAspectRatio, gridDensity, @@ -599,6 +600,15 @@ export default function SiteChecklistClient({ keep OG image text bottom aligned (default is {'"top"'}): {renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])} + + Set environment variable to {'"1"'} to enable fullscreen and zoom + actions when clicking on an image: + {renderEnvVars(['NEXT_PUBLIC_IMAGE_ACTIONS'])} + }
diff --git a/src/site/config.ts b/src/site/config.ts index 7a632de7..eba577a0 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -168,6 +168,8 @@ export const GRID_ASPECT_RATIO = : 1; export const OG_TEXT_BOTTOM_ALIGNMENT = (process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM'; +export const IMAGE_ACTIONS_ENABLED = + process.env.NEXT_PUBLIC_IMAGE_ACTIONS === '1'; export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1'; export const PREFERS_LOW_DENSITY_GRID = @@ -232,6 +234,7 @@ export const CONFIG_CHECKLIST_STATUS = { isPublicApiEnabled: PUBLIC_API_ENABLED, arePublicDownloadsEnabled: ALLOW_PUBLIC_DOWNLOADS, isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT, + isImageActionsEnabled: IMAGE_ACTIONS_ENABLED, gridAspectRatio: GRID_ASPECT_RATIO, hasGridAspectRatio: Boolean(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO), gridDensity: HIGH_DENSITY_GRID, From a709bf03f52b320bbceae08519084e99bc76174a Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sat, 25 Jan 2025 17:33:50 +0100 Subject: [PATCH 07/11] Update viewerjs toolbar styles --- src/components/image/ImageWithFallback.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index c2dd4801..7cd60e4a 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -70,10 +70,6 @@ export default function ImageWithFallback(props: ImageProps & { zoomIn: 1, zoomOut: 1, reset: 1, - play: { - show: 0, - size: 'large', - }, tooltip: 1, }, }); @@ -99,9 +95,19 @@ export default function ImageWithFallback(props: ImageProps & { return ( <> - +
Date: Sat, 25 Jan 2025 21:29:51 +0100 Subject: [PATCH 08/11] Refactor viewerjs code into separate ImageActions.tsx component and revert ImageWithFallback.tsx --- src/components/FullscreenButton.tsx | 4 +- src/components/image/ImageActions.tsx | 55 +++++++++ src/components/image/ImageLarge.tsx | 16 ++- src/components/image/ImageWithFallback.tsx | 131 +++++++-------------- 4 files changed, 108 insertions(+), 98 deletions(-) create mode 100644 src/components/image/ImageActions.tsx diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx index e5794184..4b6124d7 100644 --- a/src/components/FullscreenButton.tsx +++ b/src/components/FullscreenButton.tsx @@ -11,7 +11,7 @@ export default function FullscreenButton({ imageRef, }: { className?: string; - imageRef: RefObject; + imageRef: RefObject; }) { const { isFullscreen, setIsFullscreen } = useAppState(); @@ -55,7 +55,7 @@ export default function FullscreenButton({ title="Toggle Fullscreen" className={clsx( className, - 'text-medium absolute bottom-2 right-2 bg-white p-2 rounded', + 'text-medium absolute bottom-2 right-2 bg-white p-2 rounded hidden md:block', )} icon={isFullscreen ? : } diff --git a/src/components/image/ImageActions.tsx b/src/components/image/ImageActions.tsx new file mode 100644 index 00000000..1bfe6326 --- /dev/null +++ b/src/components/image/ImageActions.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef } from 'react'; +import Viewer from 'viewerjs'; +import 'viewerjs/dist/viewer.css'; +import { clsx } from 'clsx/lite'; +import FullscreenButton from '../FullscreenButton'; + +export default function ImageActions({ children, enableImageActions = false, className }: { children: React.ReactNode, enableImageActions?: boolean, className?: string }) { + const containerRef = useRef(null); + const viewerRef = useRef(null); + + useEffect(() => { + if (containerRef.current && enableImageActions) { + viewerRef.current = new Viewer(containerRef.current, { + inline: false, + button: true, + navbar: false, + title: false, + toolbar: { + zoomIn: 1, + zoomOut: 1, + reset: 1, + tooltip: 1, + }, + }); + return () => { + viewerRef.current?.destroy(); + }; + } + }, [enableImageActions]); + + return ( + <> + +
+ {children} + {enableImageActions && } +
+ + ); +} diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index 18569d92..e9bcf92d 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -1,18 +1,22 @@ import { IMAGE_WIDTH_LARGE, ImageProps } from '.'; import ImageWithFallback from './ImageWithFallback'; +import ImageActions from './ImageActions'; export default function ImageLarge(props: ImageProps) { const { aspectRatio, blurCompatibilityMode, + enableImageActions = false, ...rest } = props; return ( - + + + ); }; diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index 7cd60e4a..dbf76af6 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -3,18 +3,13 @@ /* eslint-disable jsx-a11y/alt-text */ import { BLUR_ENABLED } from '@/site/config'; import { useAppState } from '@/state/AppState'; -import { clsx } from 'clsx/lite'; +import { clsx} from 'clsx/lite'; import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; -import FullscreenButton from '../FullscreenButton'; -import Viewer from 'viewerjs'; -import 'viewerjs/dist/viewer.css'; export default function ImageWithFallback(props: ImageProps & { blurCompatibilityLevel?: 'none' | 'low' | 'high' imgClassName?: string - allowFullscreen?: boolean - enableImageActions?: boolean }) { const { className, @@ -22,7 +17,6 @@ export default function ImageWithFallback(props: ImageProps & { blurDataURL, blurCompatibilityLevel = 'low', imgClassName = 'object-cover h-full', - enableImageActions = false, ...rest } = props; @@ -37,10 +31,7 @@ export default function ImageWithFallback(props: ImageProps & { const [hideFallback, setHideFallback] = useState(false); - const containerRef = useRef(null); - const viewerRef = useRef(null); - const imgRef = useRef(null); - const { isFullscreen } = useAppState(); + const imgRef = useRef(null); useEffect(() => { const timeout = setTimeout( @@ -59,26 +50,6 @@ export default function ImageWithFallback(props: ImageProps & { } }, [isLoading, didError]); - useEffect(() => { - if (containerRef.current && enableImageActions) { - viewerRef.current = new Viewer(containerRef.current, { - inline: false, - button: true, - navbar: false, - title: false, - toolbar: { - zoomIn: 1, - zoomOut: 1, - reset: 1, - tooltip: 1, - }, - }); - return () => { - viewerRef.current?.destroy(); - }; - } - }, [enableImageActions]); - const showFallback = !wasCached && !hideFallback; @@ -94,65 +65,45 @@ export default function ImageWithFallback(props: ImageProps & { }; return ( - <> - -
- {(showFallback || shouldDebugImageFallbacks) && -
- {(BLUR_ENABLED && blurDataURL) - ? - :
} -
} - - {enableImageActions && } +
+ {(showFallback || shouldDebugImageFallbacks) && +
+ {(BLUR_ENABLED && blurDataURL) + ? + :
} +
} +
- ); } From d287871e48e481f47cb15716eee08c57582a87ac Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sat, 25 Jan 2025 21:35:00 +0100 Subject: [PATCH 09/11] Fix linting --- src/components/FullscreenButton.tsx | 3 ++- src/components/image/ImageActions.tsx | 19 ++++++++++++++++--- src/components/image/ImageLarge.tsx | 5 ++++- src/components/image/ImageWithFallback.tsx | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx index 4b6124d7..5a650396 100644 --- a/src/components/FullscreenButton.tsx +++ b/src/components/FullscreenButton.tsx @@ -55,7 +55,8 @@ export default function FullscreenButton({ title="Toggle Fullscreen" className={clsx( className, - 'text-medium absolute bottom-2 right-2 bg-white p-2 rounded hidden md:block', + 'text-medium absolute bottom-2 right-2 bg-white p-2 rounded', + 'hidden md:block', )} icon={isFullscreen ? : } diff --git a/src/components/image/ImageActions.tsx b/src/components/image/ImageActions.tsx index 1bfe6326..32ed935e 100644 --- a/src/components/image/ImageActions.tsx +++ b/src/components/image/ImageActions.tsx @@ -4,7 +4,15 @@ import 'viewerjs/dist/viewer.css'; import { clsx } from 'clsx/lite'; import FullscreenButton from '../FullscreenButton'; -export default function ImageActions({ children, enableImageActions = false, className }: { children: React.ReactNode, enableImageActions?: boolean, className?: string }) { +export default function ImageActions({ + children, + enableImageActions = false, + className, +}: { + children: React.ReactNode; + enableImageActions?: boolean; + className?: string; +}) { const containerRef = useRef(null); const viewerRef = useRef(null); @@ -46,9 +54,14 @@ export default function ImageActions({ children, enableImageActions = false, cla background-image: none; } `} -
+
{children} - {enableImageActions && } + {enableImageActions && ( + + )}
); diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index e9bcf92d..6e178bcb 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -10,7 +10,10 @@ export default function ImageLarge(props: ImageProps) { ...rest } = props; return ( - + -
+
); } From 6f924a7bb19c7480d558bbbba164dbb1cfc8dfe8 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sat, 25 Jan 2025 21:52:36 +0100 Subject: [PATCH 10/11] Move ImageActions component from ImageLarge.tsx to PhotoLarge.tsx and revert ImageProps --- src/components/image/ImageLarge.tsx | 21 +++++++-------------- src/components/image/index.ts | 1 - src/photo/PhotoLarge.tsx | 27 ++++++++++++++++----------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index 6e178bcb..339e9ba9 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -1,25 +1,18 @@ import { IMAGE_WIDTH_LARGE, ImageProps } from '.'; import ImageWithFallback from './ImageWithFallback'; -import ImageActions from './ImageActions'; export default function ImageLarge(props: ImageProps) { const { aspectRatio, blurCompatibilityMode, - enableImageActions = false, ...rest } = props; return ( - - - + ); -}; +}; \ No newline at end of file diff --git a/src/components/image/index.ts b/src/components/image/index.ts index 8a46d1ac..eb7d6582 100644 --- a/src/components/image/index.ts +++ b/src/components/image/index.ts @@ -14,5 +14,4 @@ export interface ImageProps { alt: string blurDataURL?: string priority?: boolean - enableImageActions?: boolean } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 9bb49d1b..32702d94 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -36,6 +36,7 @@ import { useRef } from 'react'; import useOnVisible from '@/utility/useOnVisible'; import PhotoDate from './PhotoDate'; import { useAppState } from '@/state/AppState'; +import ImageActions from '@/components/image/ImageActions'; export default function PhotoLarge({ photo, @@ -136,18 +137,22 @@ export default function PhotoLarge({ ? 'h-[80%]' : 'h-[90%]', )}> - + className="flex relative items-center justify-center h-full" + > + +
} contentSide={ From bd7ef2d01c20846a742e594c27de466dcea902b4 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Sat, 25 Jan 2025 22:02:50 +0100 Subject: [PATCH 11/11] Disable fullscreen keyboard shortkut when searchbar is open --- src/components/FullscreenButton.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx index 5a650396..96a1a053 100644 --- a/src/components/FullscreenButton.tsx +++ b/src/components/FullscreenButton.tsx @@ -13,18 +13,19 @@ export default function FullscreenButton({ className?: string; imageRef: RefObject; }) { - const { isFullscreen, setIsFullscreen } = useAppState(); + const { isFullscreen, setIsFullscreen, isCommandKOpen } = useAppState(); // Toggle fullscreen mode const toggleFullscreen = useCallback(async () => { if (!document.fullscreenElement) { + if (isCommandKOpen) return; await imageRef.current?.requestFullscreen(); setIsFullscreen?.(true); } else { await document.exitFullscreen(); setIsFullscreen?.(false); } - }, [imageRef, setIsFullscreen]); + }, [imageRef, setIsFullscreen, isCommandKOpen]); // Toggle fullscreen on 'f' key press const handleKeyDown = useCallback((event: KeyboardEvent) => {