153 lines
4.1 KiB
TypeScript
153 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
/* eslint-disable jsx-a11y/alt-text */
|
|
import { BLUR_ENABLED } from '@/site/config';
|
|
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';
|
|
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,
|
|
priority,
|
|
blurDataURL,
|
|
blurCompatibilityLevel = 'low',
|
|
imgClassName = 'object-cover h-full',
|
|
enableImageActions = false,
|
|
...rest
|
|
} = props;
|
|
|
|
const { shouldDebugImageFallbacks } = useAppState();
|
|
|
|
const [wasCached, setWasCached] = useState(true);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [didError, setDidError] = useState(false);
|
|
|
|
const onLoad = useCallback(() => setIsLoading(false), []);
|
|
const onError = useCallback(() => setDidError(true), []);
|
|
|
|
const [hideFallback, setHideFallback] = useState(false);
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const viewerRef = useRef<Viewer | null>(null);
|
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
|
const { isFullscreen } = useAppState();
|
|
|
|
useEffect(() => {
|
|
const timeout = setTimeout(
|
|
() => setWasCached(imgRef.current?.complete ?? false),
|
|
100,
|
|
);
|
|
return () => clearTimeout(timeout);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !didError) {
|
|
const timeout = setTimeout(() => {
|
|
setHideFallback(true);
|
|
}, 1000);
|
|
return () => clearTimeout(timeout);
|
|
}
|
|
}, [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,
|
|
play: {
|
|
show: 0,
|
|
size: 'large',
|
|
},
|
|
tooltip: 1,
|
|
},
|
|
});
|
|
return () => {
|
|
viewerRef.current?.destroy();
|
|
};
|
|
}
|
|
}, [enableImageActions]);
|
|
|
|
const showFallback =
|
|
!wasCached &&
|
|
!hideFallback;
|
|
|
|
const getBlurClass = () => {
|
|
switch (blurCompatibilityLevel) {
|
|
case 'high':
|
|
// Fix poorly blurred placeholder data generated on client
|
|
return 'blur-[4px] @xs:blue-md scale-[1.05]';
|
|
case 'low':
|
|
return 'blur-[2px] @xs:blue-md scale-[1.01]';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<style jsx global>
|
|
{'.viewer-canvas { background-color: black !important; }'}
|
|
</style>
|
|
<div
|
|
className={clsx(
|
|
className,
|
|
'flex relative',
|
|
)}
|
|
ref={containerRef}
|
|
>
|
|
{(showFallback || shouldDebugImageFallbacks) &&
|
|
<div className={clsx(
|
|
'@container',
|
|
'absolute inset-0',
|
|
'overflow-hidden',
|
|
'transition-opacity duration-300 ease-in',
|
|
!(BLUR_ENABLED && blurDataURL) && 'bg-main',
|
|
(isLoading || shouldDebugImageFallbacks)
|
|
? 'opacity-100'
|
|
: 'opacity-0',
|
|
)}>
|
|
{(BLUR_ENABLED && blurDataURL)
|
|
? <img {...{
|
|
...rest,
|
|
src: blurDataURL,
|
|
className: clsx(
|
|
imgClassName,
|
|
getBlurClass(),
|
|
),
|
|
}} />
|
|
: <div className={clsx(
|
|
'w-full h-full',
|
|
'bg-gray-100/50 dark:bg-gray-900/50',
|
|
)} />}
|
|
</div>}
|
|
<Image
|
|
{...rest}
|
|
ref={imgRef}
|
|
priority={priority}
|
|
className={clsx(
|
|
imgClassName,
|
|
!isFullscreen && enableImageActions && 'cursor-zoom-in',
|
|
)}
|
|
onLoad={onLoad}
|
|
onError={onError}
|
|
/>
|
|
{enableImageActions && <FullscreenButton imageRef={imgRef} />}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|