import { getIcon, iconNames } from '@sd/assets/icons/util'; import clsx from 'clsx'; import { ImgHTMLAttributes, memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { ExplorerItem, useLibraryContext } from '@sd/client'; import { ExternalObject } from '~/components'; import { useCallbackToWatchResize, useExplorerItemData, useExplorerStore, useIsDark } from '~/hooks'; import { usePlatform } from '~/util/Platform'; import { pdfViewerEnabled } from '~/util/pdfViewer'; import classes from './Thumb.module.scss'; interface ThumbnailProps { src: string; cover?: boolean; onLoad?: () => void; onError?: () => void; decoding?: ImgHTMLAttributes['decoding']; className?: string; crossOrigin?: ImgHTMLAttributes['crossOrigin']; videoBarsSize?: number; videoExtension?: string; } const Thumbnail = memo( ({ crossOrigin, videoBarsSize, videoExtension, ...props }: ThumbnailProps) => { const ref = useRef(null); const [size, setSize] = useState(null); useCallbackToWatchResize( (rect) => { const { width, height } = rect; setSize((width && height && { width, height }) || null); }, [], ref ); return ( <> = size.width ? { borderLeftWidth: videoBarsSize, borderRightWidth: videoBarsSize } : { borderTopWidth: videoBarsSize, borderBottomWidth: videoBarsSize } : {} } onLoad={props.onLoad} onError={() => { props.onError?.(); setSize(null); }} decoding={props.decoding} className={props.className} /> {videoExtension && (
{videoExtension}
)} ); } ); enum ThumbType { Icon, Original, Thumbnail } export interface ThumbProps { data: ExplorerItem; size: null | number; cover?: boolean; className?: string; loadOriginal?: boolean; mediaControls?: boolean; } function FileThumb({ size, cover, ...props }: ThumbProps) { const isDark = useIsDark(); const platform = usePlatform(); const itemData = useExplorerItemData(props.data); const { library } = useLibraryContext(); const [src, setSrc] = useState('#'); const [loaded, setLoaded] = useState(false); const [thumbType, setThumbType] = useState(ThumbType.Icon); const { locationId } = useExplorerStore(); // useLayoutEffect is required to ensure the thumbType is always updated before the onError listener can execute, // thus avoiding improper thumb types changes useLayoutEffect(() => { // Reset src when item changes, to allow detection of yet not updated src setSrc('#'); setLoaded(false); if (props.loadOriginal) { setThumbType(ThumbType.Original); } else if (itemData.hasThumbnail) { setThumbType(ThumbType.Thumbnail); } else { setThumbType(ThumbType.Icon); } }, [props.loadOriginal, itemData]); useEffect(() => { const { casId, kind, isDir, extension } = itemData; switch (thumbType) { case ThumbType.Original: if (locationId) { setSrc( platform.getFileUrl( library.uuid, locationId, props.data.item.id, // Workaround Linux webview not supporting playng video and audio through custom protocol urls kind == 'Video' || kind == 'Audio' ) ); } else { setThumbType(ThumbType.Thumbnail); } break; case ThumbType.Thumbnail: if (casId) { setSrc(platform.getThumbnailUrlById(casId)); } else { setThumbType(ThumbType.Icon); } break; default: setSrc(getIcon(kind, isDark, extension, isDir)); break; } }, [props.data.item.id, isDark, library.uuid, itemData, platform, thumbType, locationId]); const onLoad = () => setLoaded(true); const onError = () => { setLoaded(false); if (src !== '#') setThumbType((prevThumbType) => { return prevThumbType === ThumbType.Original && itemData.hasThumbnail ? ThumbType.Thumbnail : ThumbType.Icon; }); }; const { kind, extension } = itemData; const childClassName = 'max-h-full max-w-full object-contain'; return (
{(() => { switch (thumbType) { case ThumbType.Original: switch (extension === 'pdf' && pdfViewerEnabled() ? 'PDF' : kind) { case 'PDF': return ( ); case 'Video': return ( ); case 'Audio': return ( <> {props.mediaControls && ( )} ); } // eslint-disable-next-line no-fallthrough case ThumbType.Thumbnail: return ( 60 && 'border-2 border-app-line'), props.className )} crossOrigin={ThumbType.Original && 'anonymous'} // Here it is ok, because it is not a react attr videoBarsSize={ (kind === 'Video' && size && Math.floor(size / 10)) || 0 } videoExtension={ (kind === 'Video' && (cover || size == null || size > 80) && extension) || '' } /> ); default: return ( setLoaded(false)} decoding={size ? 'async' : 'sync'} className={clsx(childClassName, props.className)} /> ); } })()}
); } export default memo(FileThumb);