Add viewerjs support

This commit is contained in:
carlobortolan 2025-01-15 19:23:55 +01:00
parent cc30c2ea49
commit 4a7c988f54
No known key found for this signature in database
GPG Key ID: 574D9F10F0EED1BE
8 changed files with 78 additions and 15 deletions

View File

@ -40,7 +40,8 @@
"sonner": "^1.7.1", "sonner": "^1.7.1",
"swr": "^2.3.0", "swr": "^2.3.0",
"ts-exif-parser": "^0.2.2", "ts-exif-parser": "^0.2.2",
"use-debounce": "^10.0.4" "use-debounce": "^10.0.4",
"viewerjs": "^1.11.7"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "15.1.4", "@next/bundle-analyzer": "15.1.4",

8
pnpm-lock.yaml generated
View File

@ -104,6 +104,9 @@ importers:
use-debounce: use-debounce:
specifier: ^10.0.4 specifier: ^10.0.4
version: 10.0.4(react@19.0.0) version: 10.0.4(react@19.0.0)
viewerjs:
specifier: ^1.11.7
version: 1.11.7
devDependencies: devDependencies:
'@next/bundle-analyzer': '@next/bundle-analyzer':
specifier: 15.1.4 specifier: 15.1.4
@ -4255,6 +4258,9 @@ packages:
resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==}
engines: {node: '>=10.12.0'} engines: {node: '>=10.12.0'}
viewerjs@1.11.7:
resolution: {integrity: sha512-0JuVqOmL5v1jmEAlG5EBDR3XquxY8DWFQbFMprOXgaBB0F7Q/X9xWdEaQc59D8xzwkdUgXEMSSknTpriq95igg==}
vue@3.4.27: vue@3.4.27:
resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==} resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==}
peerDependencies: peerDependencies:
@ -9467,6 +9473,8 @@ snapshots:
'@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0 convert-source-map: 2.0.0
viewerjs@1.11.7: {}
vue@3.4.27(typescript@5.7.3): vue@3.4.27(typescript@5.7.3):
dependencies: dependencies:
'@vue/compiler-dom': 3.4.27 '@vue/compiler-dom': 3.4.27

View File

@ -77,6 +77,13 @@ export default async function PhotoPage({
if (!photo) { redirect(PATH_ROOT); } if (!photo) { redirect(PATH_ROOT); }
return ( return (
<PhotoDetailPage {...{ photo, photos, photosGrid }} /> <PhotoDetailPage
{...{
photo,
photos,
photosGrid,
enableImageActions: true,
}}
/>
); );
} }

View File

@ -13,7 +13,6 @@ export default function ImageLarge(props: ImageProps) {
blurCompatibilityLevel: blurCompatibilityMode ? 'high' : 'none', blurCompatibilityLevel: blurCompatibilityMode ? 'high' : 'none',
width: IMAGE_WIDTH_LARGE, width: IMAGE_WIDTH_LARGE,
height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio), height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio),
allowFullscreen: true,
}} /> }} />
); );
}; };

View File

@ -7,11 +7,14 @@ import { clsx} from 'clsx/lite';
import Image, { ImageProps } from 'next/image'; import Image, { ImageProps } from 'next/image';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import FullscreenButton from '../FullscreenButton'; import FullscreenButton from '../FullscreenButton';
import Viewer from 'viewerjs';
import 'viewerjs/dist/viewer.css';
export default function ImageWithFallback(props: ImageProps & { export default function ImageWithFallback(props: ImageProps & {
blurCompatibilityLevel?: 'none' | 'low' | 'high' blurCompatibilityLevel?: 'none' | 'low' | 'high'
imgClassName?: string imgClassName?: string
allowFullscreen?: boolean allowFullscreen?: boolean
enableImageActions?: boolean
}) { }) {
const { const {
className, className,
@ -19,7 +22,7 @@ export default function ImageWithFallback(props: ImageProps & {
blurDataURL, blurDataURL,
blurCompatibilityLevel = 'low', blurCompatibilityLevel = 'low',
imgClassName = 'object-cover h-full', imgClassName = 'object-cover h-full',
allowFullscreen, enableImageActions = false,
...rest ...rest
} = props; } = props;
@ -34,7 +37,10 @@ export default function ImageWithFallback(props: ImageProps & {
const [hideFallback, setHideFallback] = useState(false); const [hideFallback, setHideFallback] = useState(false);
const imgRef = useRef<HTMLImageElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<Viewer | null>(null);
const imgRef = useRef<HTMLImageElement | null>(null);
const { isFullscreen } = useAppState();
useEffect(() => { useEffect(() => {
const timeout = setTimeout( const timeout = setTimeout(
@ -53,6 +59,37 @@ export default function ImageWithFallback(props: ImageProps & {
} }
}, [isLoading, didError]); }, [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 = const showFallback =
!wasCached && !wasCached &&
!hideFallback; !hideFallback;
@ -73,6 +110,7 @@ export default function ImageWithFallback(props: ImageProps & {
className, className,
'flex relative', 'flex relative',
)} )}
ref={containerRef}
> >
{(showFallback || shouldDebugImageFallbacks) && {(showFallback || shouldDebugImageFallbacks) &&
<div className={clsx( <div className={clsx(
@ -99,15 +137,18 @@ export default function ImageWithFallback(props: ImageProps & {
'bg-gray-100/50 dark:bg-gray-900/50', 'bg-gray-100/50 dark:bg-gray-900/50',
)} />} )} />}
</div>} </div>}
<Image {...{ <Image
...rest, {...rest}
ref: imgRef, ref={imgRef}
priority, priority={priority}
className: imgClassName, className={clsx(
onLoad, imgClassName,
onError, !isFullscreen && enableImageActions && 'cursor-zoom-in',
}} /> )}
{allowFullscreen && <FullscreenButton imageRef={imgRef} />} onLoad={onLoad}
onError={onError}
/>
{enableImageActions && <FullscreenButton imageRef={imgRef} />}
</div> </div>
); );
} }

View File

@ -14,4 +14,5 @@ export interface ImageProps {
alt: string alt: string
blurDataURL?: string blurDataURL?: string
priority?: boolean priority?: boolean
enableImageActions?: boolean
} }

View File

@ -25,6 +25,7 @@ export default function PhotoDetailPage({
dateRange, dateRange,
shouldShare, shouldShare,
includeFavoriteInAdminMenu, includeFavoriteInAdminMenu,
enableImageActions,
}: { }: {
photo: Photo photo: Photo
photos: Photo[] photos: Photo[]
@ -34,6 +35,7 @@ export default function PhotoDetailPage({
dateRange?: PhotoDateRange dateRange?: PhotoDateRange
shouldShare?: boolean shouldShare?: boolean
includeFavoriteInAdminMenu?: boolean includeFavoriteInAdminMenu?: boolean
enableImageActions?: boolean
} & PhotoSetCategory) { } & PhotoSetCategory) {
let customHeader: JSX.Element | undefined; let customHeader: JSX.Element | undefined;
@ -112,6 +114,7 @@ export default function PhotoDetailPage({
shouldShareSimulation={simulation !== undefined} shouldShareSimulation={simulation !== undefined}
shouldScrollOnShare={false} shouldScrollOnShare={false}
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
enableImageActions={enableImageActions}
/>, />,
]} ]}
/> />

View File

@ -55,6 +55,7 @@ export default function PhotoLarge({
shouldShareFocalLength, shouldShareFocalLength,
includeFavoriteInAdminMenu, includeFavoriteInAdminMenu,
onVisible, onVisible,
enableImageActions = false,
}: { }: {
photo: Photo photo: Photo
className?: string className?: string
@ -75,6 +76,7 @@ export default function PhotoLarge({
shouldScrollOnShare?: boolean shouldScrollOnShare?: boolean
includeFavoriteInAdminMenu?: boolean includeFavoriteInAdminMenu?: boolean
onVisible?: () => void onVisible?: () => void
enableImageActions?: boolean
}) { }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -143,6 +145,7 @@ export default function PhotoLarge({
blurDataURL={photo.blurData} blurDataURL={photo.blurData}
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)} blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
priority={priority} priority={priority}
enableImageActions={enableImageActions}
/> />
</div> </div>
</Link>} </Link>}