From b74f83694b9186d968a726e923bae903ad74c649 Mon Sep 17 00:00:00 2001 From: carlobortolan Date: Mon, 13 Jan 2025 18:10:07 +0100 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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) => { From d8f6fbaafb0941d170ee762b6a72aa8909e46946 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 25 Jan 2025 20:37:15 -0600 Subject: [PATCH 12/28] Rename zoom controls configuration --- README.md | 2 +- src/photo/PhotoDetailPage.tsx | 4 ++-- src/site/SiteChecklistClient.tsx | 20 ++++++++++---------- src/site/config.ts | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a4a2b907..15a85110 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Application behavior can be changed by configuring the following environment var #### Settings - `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage - `NEXT_PUBLIC_DEFAULT_THEME = light | dark` sets preferred initial theme (defaults to `system` when not configured) +- `NEXT_PUBLIC_ZOOM_CONTROLS = 1` enables fullscreen photo zoom controls - `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios) - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information) - `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage) @@ -127,7 +128,6 @@ 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/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index aab19b82..f5bccfe7 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -11,7 +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'; +import { ZOOM_CONTROLS_ENABLED } from '@/site/config'; export default function PhotoDetailPage({ photo, @@ -113,7 +113,7 @@ export default function PhotoDetailPage({ shouldShareSimulation={simulation !== undefined} shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} - enableImageActions={IMAGE_ACTIONS_ENABLED} + enableImageActions={ZOOM_CONTROLS_ENABLED} />, ]} /> diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 29438d9d..1234536b 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -68,6 +68,7 @@ export default function SiteChecklistClient({ isGridHomepageEnabled, hasDefaultTheme, defaultTheme, + areZoomControlsEnabled, arePhotosMatted, isGeoPrivacyEnabled, gridAspectRatio, @@ -78,7 +79,6 @@ export default function SiteChecklistClient({ isPublicApiEnabled, isPriorityOrderEnabled, isOgTextBottomAligned, - isImageActionsEnabled, // Misc baseUrl, commitSha, @@ -538,6 +538,15 @@ export default function SiteChecklistClient({ (defaults to {'\'system\''}): {renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])} + + Set environment variable to {'"1"'} to enable + fullscreen photo zoom controls: + {renderEnvVars(['NEXT_PUBLIC_ZOOM_CONTROLS'])} + - - 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 249c59c4..40e6b9d7 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -184,6 +184,8 @@ export const DEFAULT_THEME = : process.env.NEXT_PUBLIC_DEFAULT_THEME === 'light' ? 'light' : 'system'; +export const ZOOM_CONTROLS_ENABLED = + process.env.NEXT_PUBLIC_ZOOM_CONTROLS === '1'; export const MATTE_PHOTOS = process.env.NEXT_PUBLIC_MATTE_PHOTOS === '1'; export const GEO_PRIVACY_ENABLED = @@ -205,8 +207,6 @@ export const PRIORITY_ORDER_ENABLED = process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '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'; // INTERNAL @@ -272,6 +272,7 @@ export const CONFIG_CHECKLIST_STATUS = { isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED, hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME), defaultTheme: DEFAULT_THEME, + areZoomControlsEnabled: ZOOM_CONTROLS_ENABLED, arePhotosMatted: MATTE_PHOTOS, isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED, gridAspectRatio: GRID_ASPECT_RATIO, @@ -283,7 +284,6 @@ export const CONFIG_CHECKLIST_STATUS = { isPublicApiEnabled: PUBLIC_API_ENABLED, isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED, isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT, - isImageActionsEnabled: IMAGE_ACTIONS_ENABLED, // MISC baseUrl: BASE_URL, commitSha: VERCEL_GIT_COMMIT_SHA_SHORT, From 5139abcdba723fefe66f4c729ca960ea4305ea6b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 25 Jan 2025 21:16:45 -0600 Subject: [PATCH 13/28] Rename ImageActions --- .../image/{ImageActions.tsx => ImageZoomControls.tsx} | 2 +- src/photo/PhotoLarge.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/components/image/{ImageActions.tsx => ImageZoomControls.tsx} (97%) diff --git a/src/components/image/ImageActions.tsx b/src/components/image/ImageZoomControls.tsx similarity index 97% rename from src/components/image/ImageActions.tsx rename to src/components/image/ImageZoomControls.tsx index 32ed935e..da9a3cd5 100644 --- a/src/components/image/ImageActions.tsx +++ b/src/components/image/ImageZoomControls.tsx @@ -4,7 +4,7 @@ import 'viewerjs/dist/viewer.css'; import { clsx } from 'clsx/lite'; import FullscreenButton from '../FullscreenButton'; -export default function ImageActions({ +export default function ImageZoomControls({ children, enableImageActions = false, className, diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index b0648b27..eb7ad10e 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -36,7 +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'; +import ImageZoomControls from '@/components/image/ImageZoomControls'; export default function PhotoLarge({ photo, @@ -146,7 +146,7 @@ export default function PhotoLarge({ arePhotosMatted && 'h-[90%]', arePhotosMatted && matteContentWidthForAspectRatio(), )}> - @@ -161,7 +161,7 @@ export default function PhotoLarge({ blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)} priority={priority} /> - +
} contentSide={ From 2195379b749fdbfb1021e6e8690cda98f4a2bdf7 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 10:24:00 -0600 Subject: [PATCH 14/28] Convert zoom controls into hook --- src/app/layout.tsx | 1 + src/components/image/ImageZoomControls.tsx | 68 -------------------- src/components/image/useImageZoomControls.ts | 29 +++++++++ src/photo/PhotoDetailPage.tsx | 2 +- src/photo/PhotoLarge.tsx | 19 +++--- src/site/viewerjs.css | 16 +++++ 6 files changed, 58 insertions(+), 77 deletions(-) delete mode 100644 src/components/image/ImageZoomControls.tsx create mode 100644 src/components/image/useImageZoomControls.ts create mode 100644 src/site/viewerjs.css diff --git a/src/app/layout.tsx b/src/app/layout.tsx index feeadc9a..fddaab5f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -23,6 +23,7 @@ import ShareModals from '@/share/ShareModals'; import '../site/globals.css'; import '../site/sonner.css'; +import '../site/viewerjs.css'; const ibmPlexMono = IBM_Plex_Mono({ subsets: ['latin'], diff --git a/src/components/image/ImageZoomControls.tsx b/src/components/image/ImageZoomControls.tsx deleted file mode 100644 index da9a3cd5..00000000 --- a/src/components/image/ImageZoomControls.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 ImageZoomControls({ - 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/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts new file mode 100644 index 00000000..324c639c --- /dev/null +++ b/src/components/image/useImageZoomControls.ts @@ -0,0 +1,29 @@ +import { RefObject, useEffect, useRef } from 'react'; +import Viewer from 'viewerjs'; + +export default function useImageZoomControls( + imageRef: RefObject, + isEnabled?: boolean, +) { + const viewerRef = useRef(null); + + useEffect(() => { + if (imageRef.current && isEnabled) { + viewerRef.current = new Viewer(imageRef.current, { + inline: false, + button: true, + navbar: false, + title: false, + toolbar: { + zoomIn: 1, + zoomOut: 1, + reset: 1, + tooltip: 1, + }, + }); + return () => { + viewerRef.current?.destroy(); + }; + } + }, [imageRef, isEnabled]); +} diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index f5bccfe7..49e8d8e7 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -113,7 +113,7 @@ export default function PhotoDetailPage({ shouldShareSimulation={simulation !== undefined} shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} - enableImageActions={ZOOM_CONTROLS_ENABLED} + shouldShowZoomControls={ZOOM_CONTROLS_ENABLED} />, ]} /> diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index eb7ad10e..b2ab48d3 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -36,7 +36,7 @@ import { useRef } from 'react'; import useOnVisible from '@/utility/useOnVisible'; import PhotoDate from './PhotoDate'; import { useAppState } from '@/state/AppState'; -import ImageZoomControls from '@/components/image/ImageZoomControls'; +import useImageZoomControls from '@/components/image/useImageZoomControls'; export default function PhotoLarge({ photo, @@ -55,9 +55,9 @@ export default function PhotoLarge({ shouldShareCamera, shouldShareSimulation, shouldShareFocalLength, + shouldShowZoomControls, includeFavoriteInAdminMenu, onVisible, - enableImageActions = false, }: { photo: Photo className?: string @@ -76,11 +76,12 @@ export default function PhotoLarge({ shouldShareSimulation?: boolean shouldShareFocalLength?: boolean shouldScrollOnShare?: boolean + shouldShowZoomControls?: boolean includeFavoriteInAdminMenu?: boolean onVisible?: () => void - enableImageActions?: boolean }) { const ref = useRef(null); + const refZoomControls = useRef(null); const tags = sortTags(photo.tags, primaryTag); @@ -92,6 +93,8 @@ export default function PhotoLarge({ useOnVisible(ref, onVisible); + useImageZoomControls(refZoomControls, shouldShowZoomControls); + const { arePhotosMatted, isUserSignedIn } = useAppState(); const hasTitle = @@ -146,14 +149,14 @@ export default function PhotoLarge({ arePhotosMatted && 'h-[90%]', arePhotosMatted && matteContentWidthForAspectRatio(), )}> - - +
} contentSide={ diff --git a/src/site/viewerjs.css b/src/site/viewerjs.css new file mode 100644 index 00000000..e28f93cb --- /dev/null +++ b/src/site/viewerjs.css @@ -0,0 +1,16 @@ +@import 'viewerjs/dist/viewer.css'; + +.viewer-canvas { + background-color: black !important; +} +.viewer-reset::before { + content: '1:1'; + font-size: 13px; + font-weight: 600; + color: #fff; + display: inline-block; + position: relative; + bottom: -9px; + letter-spacing: -2px; + background-image: none; +} From bbe49d3a0dbbad8c9ad73e1e77e5cfb7667ccd07 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 12:54:20 -0600 Subject: [PATCH 15/28] Consolidate event handling to zoom hook --- src/components/FullscreenButton.tsx | 69 -------------------- src/components/image/useImageZoomControls.ts | 46 ++++++++++++- src/state/AppState.ts | 3 - src/state/AppStateProvider.tsx | 6 -- 4 files changed, 45 insertions(+), 79 deletions(-) delete mode 100644 src/components/FullscreenButton.tsx diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx deleted file mode 100644 index 96a1a053..00000000 --- a/src/components/FullscreenButton.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { useEffect, useCallback, 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, 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, isCommandKOpen]); - - // 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(() => { - document.addEventListener('keydown', handleKeyDown); - document.addEventListener('fullscreenchange', handleFullscreenChange); - - return () => { - document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('fullscreenchange', handleFullscreenChange); - }; - }, [handleKeyDown, handleFullscreenChange]); - - return ( - - : } - spinnerColor="light-gray" - styleAs="link" - onClick={toggleFullscreen} - /> - ); -} diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index 324c639c..f4e9b80f 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -1,12 +1,19 @@ -import { RefObject, useEffect, useRef } from 'react'; +import { useAppState } from '@/state/AppState'; +import { RefObject, useCallback, useEffect, useRef } from 'react'; import Viewer from 'viewerjs'; +const EVENT_SHOWN = 'shown'; +const EVENT_HIDDEN = 'hidden'; +const EVENT_KEYDOWN = 'keydown'; + export default function useImageZoomControls( imageRef: RefObject, isEnabled?: boolean, ) { const viewerRef = useRef(null); + const { isCommandKOpen, setShouldRespondToKeyboardCommands } = useAppState(); + useEffect(() => { if (imageRef.current && isEnabled) { viewerRef.current = new Viewer(imageRef.current, { @@ -26,4 +33,41 @@ export default function useImageZoomControls( }; } }, [imageRef, isEnabled]); + + // On shown, disable keyboard commands + const onShown = useCallback(() => + setShouldRespondToKeyboardCommands?.(false), + [setShouldRespondToKeyboardCommands]); + useEffect(() => { + const imageRefCurrent = imageRef.current; + imageRefCurrent?.addEventListener(EVENT_SHOWN, onShown); + return () => { + imageRefCurrent?.removeEventListener(EVENT_SHOWN, onShown); + }; + }, [imageRef, onShown]); + + // On hide, reenable keyboard commands + const onHide = useCallback(() => + setShouldRespondToKeyboardCommands?.(true), + [setShouldRespondToKeyboardCommands]); + useEffect(() => { + const imageRefCurrent = imageRef.current; + imageRefCurrent?.addEventListener(EVENT_HIDDEN, onHide); + return () => { + imageRefCurrent?.removeEventListener(EVENT_HIDDEN, onHide); + }; + }, [imageRef, onHide]); + + // On 'F' keydown, toggle fullscreen + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (!isCommandKOpen && e.key.toUpperCase() === 'F') { + viewerRef.current?.show(); + } + }, [isCommandKOpen]); + useEffect(() => { + document.addEventListener(EVENT_KEYDOWN, handleKeyDown); + return () => { + document.removeEventListener(EVENT_KEYDOWN, handleKeyDown); + }; + }, [handleKeyDown]); } diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 9dcde3ca..69975e9e 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -39,9 +39,6 @@ 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 0f04d631..e8619fec 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -52,9 +52,6 @@ export default function AppStateProvider({ useState(false); const [shouldShowBaselineGrid, setShouldShowBaselineGrid] = useState(false); - // FULLSCREEN - const [isFullscreen, setIsFullscreen] = - useState(false); const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); @@ -125,9 +122,6 @@ export default function AppStateProvider({ setShouldDebugImageFallbacks, shouldShowBaselineGrid, setShouldShowBaselineGrid, - // FULLSCREEN - isFullscreen, - setIsFullscreen, }} > {children} From 8444a7cfbb881558c11310b87f44b0627d5b1b9e Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 12:55:52 -0600 Subject: [PATCH 16/28] Reformat photo page code --- src/app/p/[photoId]/page.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/p/[photoId]/page.tsx b/src/app/p/[photoId]/page.tsx index 86ec9013..09f558ef 100644 --- a/src/app/p/[photoId]/page.tsx +++ b/src/app/p/[photoId]/page.tsx @@ -78,11 +78,7 @@ export default async function PhotoPage({ return ( ); } From c998b68b168c23de441e8781c0b0093f809ba3f5 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 12:56:21 -0600 Subject: [PATCH 17/28] Reformat large image code --- src/components/image/ImageLarge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/image/ImageLarge.tsx b/src/components/image/ImageLarge.tsx index 339e9ba9..18569d92 100644 --- a/src/components/image/ImageLarge.tsx +++ b/src/components/image/ImageLarge.tsx @@ -15,4 +15,4 @@ export default function ImageLarge(props: ImageProps) { height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio), }} /> ); -}; \ No newline at end of file +}; From ad83dbb2e3129c918ab011dc96d8d820afdb3dae Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 12:56:58 -0600 Subject: [PATCH 18/28] Format code --- src/app/p/[photoId]/page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/p/[photoId]/page.tsx b/src/app/p/[photoId]/page.tsx index 09f558ef..a84aa724 100644 --- a/src/app/p/[photoId]/page.tsx +++ b/src/app/p/[photoId]/page.tsx @@ -77,8 +77,6 @@ export default async function PhotoPage({ if (!photo) { redirect(PATH_ROOT); } return ( - + ); } From c9c470d43cf2cad2d15aa23d13faf61c037044a0 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 13:05:41 -0600 Subject: [PATCH 19/28] Add zoom controls to admin debugging --- src/components/cmdk/CommandKClient.tsx | 6 ++++++ src/photo/PhotoLarge.tsx | 14 ++++++++------ src/state/AppState.ts | 2 ++ src/state/AppStateProvider.tsx | 10 +++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index bd598885..9cf33da6 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -92,6 +92,7 @@ export default function CommandKClient({ selectedPhotoIds, setSelectedPhotoIds, isGridHighDensity, + areZoomControlsEnabled, arePhotosMatted, shouldShowBaselineGrid, shouldDebugImageFallbacks, @@ -99,6 +100,7 @@ export default function CommandKClient({ setShouldRespondToKeyboardCommands, setShouldShowBaselineGrid, setIsGridHighDensity, + setAreZoomControlsEnabled, setArePhotosMatted, setShouldDebugImageFallbacks, } = useAppState(); @@ -250,6 +252,10 @@ export default function CommandKClient({ heading: 'Debug Tools', accessory: , items: [{ + label: 'Toggle Zoom Controls', + action: () => setAreZoomControlsEnabled?.(prev => !prev), + annotation: areZoomControlsEnabled ? : undefined, + }, { label: 'Toggle Photo Matting', action: () => setArePhotosMatted?.(prev => !prev), annotation: arePhotosMatted ? : undefined, diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index b2ab48d3..5233ec3a 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -55,7 +55,6 @@ export default function PhotoLarge({ shouldShareCamera, shouldShareSimulation, shouldShareFocalLength, - shouldShowZoomControls, includeFavoriteInAdminMenu, onVisible, }: { @@ -76,13 +75,18 @@ export default function PhotoLarge({ shouldShareSimulation?: boolean shouldShareFocalLength?: boolean shouldScrollOnShare?: boolean - shouldShowZoomControls?: boolean includeFavoriteInAdminMenu?: boolean onVisible?: () => void }) { const ref = useRef(null); const refZoomControls = useRef(null); + const { + areZoomControlsEnabled, + arePhotosMatted, + isUserSignedIn, + } = useAppState(); + const tags = sortTags(photo.tags, primaryTag); const camera = cameraFromPhoto(photo); @@ -93,9 +97,7 @@ export default function PhotoLarge({ useOnVisible(ref, onVisible); - useImageZoomControls(refZoomControls, shouldShowZoomControls); - - const { arePhotosMatted, isUserSignedIn } = useAppState(); + useImageZoomControls(refZoomControls, areZoomControlsEnabled); const hasTitle = showTitle && @@ -151,7 +153,7 @@ export default function PhotoLarge({ )}>
> + areZoomControlsEnabled?: boolean + setAreZoomControlsEnabled?: Dispatch> arePhotosMatted?: boolean setArePhotosMatted?: Dispatch> shouldDebugImageFallbacks?: boolean diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index e8619fec..7cd7b225 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -6,7 +6,11 @@ import { AnimationConfig } from '@/components/AnimateItems'; import usePathnames from '@/utility/usePathnames'; import { getAuthAction } from '@/auth/actions'; import useSWR from 'swr'; -import { HIGH_DENSITY_GRID, MATTE_PHOTOS } from '@/site/config'; +import { + HIGH_DENSITY_GRID, + MATTE_PHOTOS, + ZOOM_CONTROLS_ENABLED, +} from '@/site/config'; import { getPhotosHiddenMetaCachedAction } from '@/photo/actions'; import { ShareModalProps } from '@/share'; import { storeTimezoneCookie } from '@/utility/timezone'; @@ -46,6 +50,8 @@ export default function AppStateProvider({ // DEBUG const [isGridHighDensity, setIsGridHighDensity] = useState(HIGH_DENSITY_GRID); + const [areZoomControlsEnabled, setAreZoomControlsEnabled] = + useState(ZOOM_CONTROLS_ENABLED); const [arePhotosMatted, setArePhotosMatted] = useState(MATTE_PHOTOS); const [shouldDebugImageFallbacks, setShouldDebugImageFallbacks] = @@ -116,6 +122,8 @@ export default function AppStateProvider({ // DEBUG isGridHighDensity, setIsGridHighDensity, + areZoomControlsEnabled, + setAreZoomControlsEnabled, arePhotosMatted, setArePhotosMatted, shouldDebugImageFallbacks, From d81e6775309b625043953df9861b15f533465984 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 13:53:17 -0600 Subject: [PATCH 20/28] Refine zoom button styles/positions --- src/components/image/useImageZoomControls.ts | 7 ++- src/site/viewerjs.css | 48 +++++++++++++++++++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index f4e9b80f..b6c83472 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -23,9 +23,8 @@ export default function useImageZoomControls( title: false, toolbar: { zoomIn: 1, - zoomOut: 1, - reset: 1, - tooltip: 1, + reset: 2, + zoomOut: 3, }, }); return () => { @@ -46,7 +45,7 @@ export default function useImageZoomControls( }; }, [imageRef, onShown]); - // On hide, reenable keyboard commands + // On hidden, reenable keyboard commands const onHide = useCallback(() => setShouldRespondToKeyboardCommands?.(true), [setShouldRespondToKeyboardCommands]); diff --git a/src/site/viewerjs.css b/src/site/viewerjs.css index e28f93cb..d6ede4ae 100644 --- a/src/site/viewerjs.css +++ b/src/site/viewerjs.css @@ -1,12 +1,12 @@ @import 'viewerjs/dist/viewer.css'; .viewer-canvas { - background-color: black !important; + background-color: black; } .viewer-reset::before { content: '1:1'; font-size: 13px; - font-weight: 600; + font-weight: 500; color: #fff; display: inline-block; position: relative; @@ -14,3 +14,47 @@ letter-spacing: -2px; background-image: none; } +.viewer-close { + display: flex; + align-items: center; + justify-content: center; + top: 20px; + right: 20px; +} +.viewer-close::before { + transform: scale(1.1); +} +.viewer-button { + width: 40px; + height: 40px; +} +.viewer-button::before { + bottom: auto; + left: auto; +} +.viewer-button:focus { + box-shadow: none; +} +.viewer-button:active { + opacity: 0.5; +} +.viewer-toolbar > ul { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2rem 0.25rem; +} +.viewer-toolbar > ul > li { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; +} +.viewer-toolbar > ul > li:focus { + box-shadow: none; +} +.viewer-reset::before { + left:-1px; +} From c089d9a6ca59831b6203b9a4054340fd2c3dbcbc Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 13:55:28 -0600 Subject: [PATCH 21/28] Remove old prop --- src/photo/PhotoDetailPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index 49e8d8e7..beb67cf4 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -11,7 +11,6 @@ import HiddenHeader from '@/tag/HiddenHeader'; import FocalLengthHeader from '@/focal/FocalLengthHeader'; import PhotoHeader from './PhotoHeader'; import { JSX } from 'react'; -import { ZOOM_CONTROLS_ENABLED } from '@/site/config'; export default function PhotoDetailPage({ photo, @@ -113,7 +112,6 @@ export default function PhotoDetailPage({ shouldShareSimulation={simulation !== undefined} shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} - shouldShowZoomControls={ZOOM_CONTROLS_ENABLED} />, ]} /> From cb13496a34fe2811a7d18d24e88889339c0e0c84 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 14:17:40 -0600 Subject: [PATCH 22/28] Add explicit zoom button --- src/components/image/useImageZoomControls.ts | 13 +++++++++++++ src/photo/PhotoLarge.tsx | 18 +++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index b6c83472..4b5ac312 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -33,6 +33,14 @@ export default function useImageZoomControls( } }, [imageRef, isEnabled]); + const open = useCallback(() => { + viewerRef.current?.show(); + }, [viewerRef]); + + const close = useCallback(() => { + viewerRef.current?.hide(); + }, [viewerRef]); + // On shown, disable keyboard commands const onShown = useCallback(() => setShouldRespondToKeyboardCommands?.(false), @@ -69,4 +77,9 @@ export default function useImageZoomControls( document.removeEventListener(EVENT_KEYDOWN, handleKeyDown); }; }, [handleKeyDown]); + + return { + open, + close, + }; } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 5233ec3a..55b03576 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -37,6 +37,8 @@ import useOnVisible from '@/utility/useOnVisible'; import PhotoDate from './PhotoDate'; import { useAppState } from '@/state/AppState'; import useImageZoomControls from '@/components/image/useImageZoomControls'; +import { LuZoomIn } from 'react-icons/lu'; +import LoaderButton from '@/components/primitives/LoaderButton'; export default function PhotoLarge({ photo, @@ -79,7 +81,7 @@ export default function PhotoLarge({ onVisible?: () => void }) { const ref = useRef(null); - const refZoomControls = useRef(null); + const refZoomControlsContainer = useRef(null); const { areZoomControlsEnabled, @@ -97,7 +99,10 @@ export default function PhotoLarge({ useOnVisible(ref, onVisible); - useImageZoomControls(refZoomControls, areZoomControlsEnabled); + const { open } = useImageZoomControls( + refZoomControlsContainer, + areZoomControlsEnabled, + ); const hasTitle = showTitle && @@ -152,7 +157,7 @@ export default function PhotoLarge({ arePhotosMatted && matteContentWidthForAspectRatio(), )}>
} + {areZoomControlsEnabled && + } + onClick={open} + styleAs="link" + className="text-medium" + />} {ALLOW_PUBLIC_DOWNLOADS && Date: Sun, 26 Jan 2025 14:23:38 -0600 Subject: [PATCH 23/28] Add title text to photo buttons --- src/photo/PhotoLarge.tsx | 2 ++ src/share/ShareButton.tsx | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 55b03576..eeec93ef 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -289,6 +289,7 @@ export default function PhotoLarge({ )}> {shouldShare && } {areZoomControlsEnabled && } onClick={open} styleAs="link" diff --git a/src/share/ShareButton.tsx b/src/share/ShareButton.tsx index f3a4d604..bc95e4af 100644 --- a/src/share/ShareButton.tsx +++ b/src/share/ShareButton.tsx @@ -11,11 +11,13 @@ import { useRouter } from 'next/navigation'; let prefetchedImage: HTMLImageElement | null = null; export default function ShareButton({ + title, dim, prefetch, className, ...rest }: { + title?: string dim?: boolean prefetch?: boolean className?: string @@ -35,6 +37,7 @@ export default function ShareButton({ return ( setShareModalProps?.({ ...rest })} className={clsx( className, From a9e0db8392cc8d23bce9184dfe5dfdb23bea11ba Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 14:36:16 -0600 Subject: [PATCH 24/28] Add zoom control prop to for potential future use --- src/photo/PhotoDetailPage.tsx | 1 - src/photo/PhotoLarge.tsx | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index beb67cf4..2a0ea013 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -110,7 +110,6 @@ export default function PhotoDetailPage({ shouldShareTag={tag !== undefined} shouldShareCamera={camera !== undefined} shouldShareSimulation={simulation !== undefined} - shouldScrollOnShare={false} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} />, ]} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index eeec93ef..81203416 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -52,6 +52,7 @@ export default function PhotoLarge({ showTitleAsH1, showCamera = true, showSimulation = true, + showZoomControls: showZoomControlsProp = true, shouldShare = true, shouldShareTag, shouldShareCamera, @@ -71,12 +72,12 @@ export default function PhotoLarge({ showTitleAsH1?: boolean showCamera?: boolean showSimulation?: boolean + showZoomControls?: boolean shouldShare?: boolean shouldShareTag?: boolean shouldShareCamera?: boolean shouldShareSimulation?: boolean shouldShareFocalLength?: boolean - shouldScrollOnShare?: boolean includeFavoriteInAdminMenu?: boolean onVisible?: () => void }) { @@ -89,6 +90,8 @@ export default function PhotoLarge({ isUserSignedIn, } = useAppState(); + const showZoomControls = showZoomControlsProp && areZoomControlsEnabled; + const tags = sortTags(photo.tags, primaryTag); const camera = cameraFromPhoto(photo); @@ -101,7 +104,7 @@ export default function PhotoLarge({ const { open } = useImageZoomControls( refZoomControlsContainer, - areZoomControlsEnabled, + showZoomControls, ); const hasTitle = @@ -158,7 +161,7 @@ export default function PhotoLarge({ )}>
} - {areZoomControlsEnabled && + {showZoomControls && } From 8ff534a65bb051b29eaa433d261b806d170e9a6b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 14:42:17 -0600 Subject: [PATCH 25/28] Enable zoom controls by default --- README.md | 2 +- src/components/cmdk/CommandKClient.tsx | 8 ++++---- src/photo/PhotoLarge.tsx | 4 ++-- src/site/SiteChecklistClient.tsx | 20 ++++++++++---------- src/site/config.ts | 6 +++--- src/state/AppState.ts | 4 ++-- src/state/AppStateProvider.tsx | 10 +++++----- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 15a85110..8bf08af1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Application behavior can be changed by configuring the following environment var #### Display - `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography) +- `NEXT_PUBLIC_HIDE_ZOOM_CONTROLS = 1` hides fullscreen photo zoom controls - `NEXT_PUBLIC_HIDE_TAKEN_AT_TIME = 1` hides taken at time from photo meta - `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X button from share modal - `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results @@ -119,7 +120,6 @@ Application behavior can be changed by configuring the following environment var #### Settings - `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage - `NEXT_PUBLIC_DEFAULT_THEME = light | dark` sets preferred initial theme (defaults to `system` when not configured) -- `NEXT_PUBLIC_ZOOM_CONTROLS = 1` enables fullscreen photo zoom controls - `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios) - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information) - `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage) diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 9cf33da6..9bbf79a0 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -92,7 +92,7 @@ export default function CommandKClient({ selectedPhotoIds, setSelectedPhotoIds, isGridHighDensity, - areZoomControlsEnabled, + areZoomControlsShown, arePhotosMatted, shouldShowBaselineGrid, shouldDebugImageFallbacks, @@ -100,7 +100,7 @@ export default function CommandKClient({ setShouldRespondToKeyboardCommands, setShouldShowBaselineGrid, setIsGridHighDensity, - setAreZoomControlsEnabled, + setAreZoomControlsShown, setArePhotosMatted, setShouldDebugImageFallbacks, } = useAppState(); @@ -253,8 +253,8 @@ export default function CommandKClient({ accessory: , items: [{ label: 'Toggle Zoom Controls', - action: () => setAreZoomControlsEnabled?.(prev => !prev), - annotation: areZoomControlsEnabled ? : undefined, + action: () => setAreZoomControlsShown?.(prev => !prev), + annotation: areZoomControlsShown ? : undefined, }, { label: 'Toggle Photo Matting', action: () => setArePhotosMatted?.(prev => !prev), diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 81203416..6de07a27 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -85,12 +85,12 @@ export default function PhotoLarge({ const refZoomControlsContainer = useRef(null); const { - areZoomControlsEnabled, + areZoomControlsShown, arePhotosMatted, isUserSignedIn, } = useAppState(); - const showZoomControls = showZoomControlsProp && areZoomControlsEnabled; + const showZoomControls = showZoomControlsProp && areZoomControlsShown; const tags = sortTags(photo.tags, primaryTag); diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index 1234536b..38861a81 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -60,6 +60,7 @@ export default function SiteChecklistClient({ isBlurEnabled, // Display showExifInfo, + showZoomControls, showTakenAtTimeHidden, showSocial, showFilmSimulations, @@ -68,7 +69,6 @@ export default function SiteChecklistClient({ isGridHomepageEnabled, hasDefaultTheme, defaultTheme, - areZoomControlsEnabled, arePhotosMatted, isGeoPrivacyEnabled, gridAspectRatio, @@ -474,6 +474,15 @@ export default function SiteChecklistClient({ Set environment variable to {'"1"'} to hide EXIF data: {renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])} + + Set environment variable to {'"1"'} to hide + fullscreen photo zoom controls: + {renderEnvVars(['NEXT_PUBLIC_HIDE_ZOOM_CONTROLS'])} + - - Set environment variable to {'"1"'} to enable - fullscreen photo zoom controls: - {renderEnvVars(['NEXT_PUBLIC_ZOOM_CONTROLS'])} - > - areZoomControlsEnabled?: boolean - setAreZoomControlsEnabled?: Dispatch> + areZoomControlsShown?: boolean + setAreZoomControlsShown?: Dispatch> arePhotosMatted?: boolean setArePhotosMatted?: Dispatch> shouldDebugImageFallbacks?: boolean diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 7cd7b225..39e7382e 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -9,7 +9,7 @@ import useSWR from 'swr'; import { HIGH_DENSITY_GRID, MATTE_PHOTOS, - ZOOM_CONTROLS_ENABLED, + SHOW_ZOOM_CONTROLS, } from '@/site/config'; import { getPhotosHiddenMetaCachedAction } from '@/photo/actions'; import { ShareModalProps } from '@/share'; @@ -50,8 +50,8 @@ export default function AppStateProvider({ // DEBUG const [isGridHighDensity, setIsGridHighDensity] = useState(HIGH_DENSITY_GRID); - const [areZoomControlsEnabled, setAreZoomControlsEnabled] = - useState(ZOOM_CONTROLS_ENABLED); + const [areZoomControlsShown, setAreZoomControlsShown] = + useState(SHOW_ZOOM_CONTROLS); const [arePhotosMatted, setArePhotosMatted] = useState(MATTE_PHOTOS); const [shouldDebugImageFallbacks, setShouldDebugImageFallbacks] = @@ -122,8 +122,8 @@ export default function AppStateProvider({ // DEBUG isGridHighDensity, setIsGridHighDensity, - areZoomControlsEnabled, - setAreZoomControlsEnabled, + areZoomControlsShown, + setAreZoomControlsShown, arePhotosMatted, setArePhotosMatted, shouldDebugImageFallbacks, From 637e5cd2d3597404e860e558aa85709299d4ad52 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 15:10:22 -0600 Subject: [PATCH 26/28] Disable zoom F key listener on root --- src/components/image/useImageZoomControls.ts | 9 ++- src/photo/PhotoLarge.tsx | 67 ++++++++++++-------- src/photo/PhotosLarge.tsx | 1 + src/site/viewerjs.css | 2 +- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index 4b5ac312..73c36671 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -9,6 +9,7 @@ const EVENT_KEYDOWN = 'keydown'; export default function useImageZoomControls( imageRef: RefObject, isEnabled?: boolean, + shouldExpandOnFKeydown?: boolean, ) { const viewerRef = useRef(null); @@ -67,10 +68,14 @@ export default function useImageZoomControls( // On 'F' keydown, toggle fullscreen const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (!isCommandKOpen && e.key.toUpperCase() === 'F') { + if ( + shouldExpandOnFKeydown && + !isCommandKOpen && + e.key.toUpperCase() === 'F' + ) { viewerRef.current?.show(); } - }, [isCommandKOpen]); + }, [shouldExpandOnFKeydown, isCommandKOpen]); useEffect(() => { document.addEventListener(EVENT_KEYDOWN, handleKeyDown); return () => { diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 6de07a27..3458a50c 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -53,6 +53,7 @@ export default function PhotoLarge({ showCamera = true, showSimulation = true, showZoomControls: showZoomControlsProp = true, + shouldZoomOnFKeydown = true, shouldShare = true, shouldShareTag, shouldShareCamera, @@ -73,6 +74,7 @@ export default function PhotoLarge({ showCamera?: boolean showSimulation?: boolean showZoomControls?: boolean + shouldZoomOnFKeydown?: boolean shouldShare?: boolean shouldShareTag?: boolean shouldShareCamera?: boolean @@ -105,6 +107,7 @@ export default function PhotoLarge({ const { open } = useImageZoomControls( refZoomControlsContainer, showZoomControls, + shouldZoomOnFKeydown, ); const hasTitle = @@ -141,41 +144,49 @@ export default function PhotoLarge({ } }; + const largePhotoContent = +
+
+ +
+
; + + const largePhotoContainerClassName = clsx(arePhotosMatted && + 'flex items-center justify-center aspect-[3/2] bg-gray-100', + ); + return ( + {largePhotoContent} +
+ : -
-
- -
-
+ {largePhotoContent} } contentSide={ ul > li { display: flex; From cae1da9f6ad89d1c6cd145f8bb64b9a19ec80be6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 16:42:47 -0600 Subject: [PATCH 27/28] Refactor escape handling --- src/components/Modal.tsx | 3 ++ src/components/image/useImageZoomControls.ts | 21 ++++--------- src/photo/PhotoEscapeHandler.tsx | 24 +++++---------- src/photo/useEscapeHandler.ts | 12 ++++++++ src/share/ShareModal.tsx | 12 ++++++-- src/utility/useKeydownHandler.ts | 32 ++++++++++++++++++++ 6 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 src/photo/useEscapeHandler.ts create mode 100644 src/utility/useKeydownHandler.ts diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index ac0f7526..20dd1482 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -9,6 +9,7 @@ import AnimateItems from './AnimateItems'; import { PATH_ROOT } from '@/site/paths'; import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion'; import useMetaThemeColor from '@/site/useMetaThemeColor'; +import useEscapeHandler from '@/photo/useEscapeHandler'; export default function Modal({ onClosePath, @@ -55,6 +56,8 @@ export default function Modal({ }, }); + useEscapeHandler(onClose, true); + return ( , @@ -13,7 +13,7 @@ export default function useImageZoomControls( ) { const viewerRef = useRef(null); - const { isCommandKOpen, setShouldRespondToKeyboardCommands } = useAppState(); + const { setShouldRespondToKeyboardCommands } = useAppState(); useEffect(() => { if (imageRef.current && isEnabled) { @@ -67,21 +67,12 @@ export default function useImageZoomControls( }, [imageRef, onHide]); // On 'F' keydown, toggle fullscreen - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if ( - shouldExpandOnFKeydown && - !isCommandKOpen && - e.key.toUpperCase() === 'F' - ) { + const handleKeyDown = useCallback(() => { + if (shouldExpandOnFKeydown) { viewerRef.current?.show(); } - }, [shouldExpandOnFKeydown, isCommandKOpen]); - useEffect(() => { - document.addEventListener(EVENT_KEYDOWN, handleKeyDown); - return () => { - document.removeEventListener(EVENT_KEYDOWN, handleKeyDown); - }; - }, [handleKeyDown]); + }, [shouldExpandOnFKeydown]); + useKeydownHandler(handleKeyDown, ['F']); return { open, diff --git a/src/photo/PhotoEscapeHandler.tsx b/src/photo/PhotoEscapeHandler.tsx index b8da15c9..afaf1019 100644 --- a/src/photo/PhotoEscapeHandler.tsx +++ b/src/photo/PhotoEscapeHandler.tsx @@ -1,32 +1,22 @@ 'use client'; import { getEscapePath } from '@/site/paths'; -import { useAppState } from '@/state/AppState'; import { useRouter, usePathname } from 'next/navigation'; -import { useEffect } from 'react'; - -const LISTENER_KEYUP = 'keyup'; +import { useCallback } from 'react'; +import useEscapeHandler from './useEscapeHandler'; export default function PhotoEscapeHandler() { const router = useRouter(); const pathname = usePathname(); - const { shouldRespondToKeyboardCommands } = useAppState(); - const escapePath = getEscapePath(pathname); - useEffect(() => { - if (shouldRespondToKeyboardCommands) { - const onKeyUp = (e: KeyboardEvent) => { - if (e.key?.toUpperCase() === 'ESCAPE' && escapePath) { - router.push(escapePath, { scroll: false }); - }; - }; - window.addEventListener(LISTENER_KEYUP, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); - } - }, [shouldRespondToKeyboardCommands, router, escapePath]); + const escapeHandler = useCallback(() => { + if (escapePath) { router.push(escapePath, { scroll: false }); } + }, [escapePath, router]); + + useEscapeHandler(escapeHandler); return null; } diff --git a/src/photo/useEscapeHandler.ts b/src/photo/useEscapeHandler.ts new file mode 100644 index 00000000..2218f0d5 --- /dev/null +++ b/src/photo/useEscapeHandler.ts @@ -0,0 +1,12 @@ +import useKeydownHandler from '@/utility/useKeydownHandler'; + +export default function useEscapeHandler( + onEscape?: () => void, + ignoreShouldRespondToKeyboardCommands?: boolean, +) { + useKeydownHandler( + onEscape, + ['ESCAPE'], + ignoreShouldRespondToKeyboardCommands, + ); +} diff --git a/src/share/ShareModal.tsx b/src/share/ShareModal.tsx index 97d42c92..1b4b0379 100644 --- a/src/share/ShareModal.tsx +++ b/src/share/ShareModal.tsx @@ -4,7 +4,7 @@ import Modal from '@/components/Modal'; import { TbPhotoShare } from 'react-icons/tb'; import { clsx } from 'clsx/lite'; import { BiCopy } from 'react-icons/bi'; -import { JSX, ReactNode } from 'react'; +import { JSX, ReactNode, useEffect } from 'react'; import { shortenUrl } from '@/utility/url'; import { toastSuccess } from '@/toast'; import { PiXLogo } from 'react-icons/pi'; @@ -24,7 +24,15 @@ export default function ShareModal({ socialText: string children: ReactNode }) { - const { setShareModalProps } = useAppState(); + const { + setShareModalProps, + setShouldRespondToKeyboardCommands, + } = useAppState(); + + useEffect(() => { + setShouldRespondToKeyboardCommands?.(false); + return () => setShouldRespondToKeyboardCommands?.(true); + }, [setShouldRespondToKeyboardCommands]); const renderIcon = ( icon: JSX.Element, diff --git a/src/utility/useKeydownHandler.ts b/src/utility/useKeydownHandler.ts new file mode 100644 index 00000000..b6f2af40 --- /dev/null +++ b/src/utility/useKeydownHandler.ts @@ -0,0 +1,32 @@ +import { useAppState } from '@/state/AppState'; +import { useCallback, useEffect } from 'react'; + +const LISTENER_KEYDOWN = 'keydown'; + +export default function useKeydownHandler( + onKeydown?: (e: KeyboardEvent) => void, + keys: string[] = [], + ignoreShouldRespondToKeyboardCommands?: boolean, +) { + const { shouldRespondToKeyboardCommands } = useAppState(); + + const onKeyUp = useCallback((e: KeyboardEvent) => { + if (keys.some(key => key.toUpperCase() === e.key?.toUpperCase())) { + onKeydown?.(e); + } + }, [onKeydown, keys]); + + useEffect(() => { + if ( + shouldRespondToKeyboardCommands || + ignoreShouldRespondToKeyboardCommands + ) { + window.addEventListener(LISTENER_KEYDOWN, onKeyUp); + return () => window.removeEventListener(LISTENER_KEYDOWN, onKeyUp); + } + }, [ + shouldRespondToKeyboardCommands, + ignoreShouldRespondToKeyboardCommands, + onKeyUp, + ]); +} From 99c22f0af162a0f799133dadf3b0d21b7f7c2566 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 26 Jan 2025 16:43:13 -0600 Subject: [PATCH 28/28] Fix css height bug for matted photos --- src/photo/PhotoLarge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 3458a50c..237ba6e4 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -153,7 +153,7 @@ export default function PhotoLarge({ )}>