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}
/>
}