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",
"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",

8
pnpm-lock.yaml generated
View File

@ -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

View File

@ -77,6 +77,13 @@ export default async function PhotoPage({
if (!photo) { redirect(PATH_ROOT); }
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',
width: IMAGE_WIDTH_LARGE,
height: Math.round(IMAGE_WIDTH_LARGE / aspectRatio),
allowFullscreen: true,
}} />
);
};

View File

@ -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<HTMLImageElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<Viewer | null>(null);
const imgRef = useRef<HTMLImageElement | null>(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) &&
<div className={clsx(
@ -99,15 +137,18 @@ export default function ImageWithFallback(props: ImageProps & {
'bg-gray-100/50 dark:bg-gray-900/50',
)} />}
</div>}
<Image {...{
...rest,
ref: imgRef,
priority,
className: imgClassName,
onLoad,
onError,
}} />
{allowFullscreen && <FullscreenButton imageRef={imgRef} />}
<Image
{...rest}
ref={imgRef}
priority={priority}
className={clsx(
imgClassName,
!isFullscreen && enableImageActions && 'cursor-zoom-in',
)}
onLoad={onLoad}
onError={onError}
/>
{enableImageActions && <FullscreenButton imageRef={imgRef} />}
</div>
);
}

View File

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

View File

@ -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}
/>,
]}
/>

View File

@ -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<HTMLDivElement>(null);
@ -143,6 +145,7 @@ export default function PhotoLarge({
blurDataURL={photo.blurData}
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
priority={priority}
enableImageActions={enableImageActions}
/>
</div>
</Link>}