Files
spacedrive/interface/app/$libraryId/Explorer/FilePath/Original.tsx
ameer2468 eb225883e6 [ENG-1536] Quick preview fixes: Tooltip, video switching, and media controls flash (#1934)
* Fix controls flashing when switching videos, tooltip, and video switching in quickpreview

* remove state

* props
2024-01-12 13:58:33 +00:00

233 lines
6.3 KiB
TypeScript

import { getIcon, iconNames } from '@sd/assets/util';
import clsx from 'clsx';
import {
SyntheticEvent,
useEffect,
useMemo,
useRef,
useState,
type VideoHTMLAttributes
} from 'react';
import { getItemFilePath, useLibraryContext } from '@sd/client';
import i18n from '~/app/I18n';
import { PDFViewer, TextViewer } from '~/components';
import { useLocale } from '~/hooks';
import { pdfViewerEnabled } from '~/util/pdfViewer';
import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from '../Context';
import { explorerStore } from '../store';
import { ExplorerItemData } from '../util';
import { Image } from './Image';
import { useBlackBars, useSize } from './utils';
interface OriginalRendererProps {
src: string;
className: string;
frameClassName: string;
itemData: ExplorerItemData;
isDark: boolean;
childClassName?: string;
size?: number;
mediaControls?: boolean;
frame?: boolean;
isSidebarPreview?: boolean;
pauseVideo?: boolean;
blackBars?: boolean;
blackBarsSize?: number;
onLoad?(): void;
onError?(e: ErrorEvent | SyntheticEvent<Element, Event>): void;
}
export function Original({
itemData,
filePath,
...props
}: Omit<OriginalRendererProps, 'src'> & {
filePath: ReturnType<typeof getItemFilePath>;
}) {
const [error, setError] = useState(false);
if (error) throw new Error('onError');
const Renderer = useMemo(() => {
const kind = originalRendererKind(itemData);
return ORIGINAL_RENDERERS[kind];
}, [itemData]);
if (!Renderer) throw new Error('no renderer!');
const platform = usePlatform();
const { library } = useLibraryContext();
const { parent } = useExplorerContext();
const src = useMemo(() => {
const locationId =
itemData.locationId ?? (parent?.type === 'Location' ? parent.location.id : null);
if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) {
if ('id' in filePath && locationId)
return platform.getFileUrl(library.uuid, locationId, filePath.id);
else if ('path' in filePath) return platform.getFileUrlByPath(filePath.path);
}
}, [itemData, filePath, library.uuid, parent, platform]);
if (src === undefined) throw new Error('no src!');
return <Renderer src={src} itemData={itemData} onError={() => setError(true)} {...props} />;
}
const TEXT_RENDERER: OriginalRenderer = (props) => (
<TextViewer
src={props.src}
onLoad={props.onLoad}
onError={props.onError}
className={clsx(
'textviewer-scroll h-full w-full overflow-y-auto whitespace-pre-wrap break-words px-4 font-mono',
!props.mediaControls ? 'overflow-hidden' : 'overflow-auto',
props.className,
props.frame && [props.frameClassName, '!bg-none p-2']
)}
codeExtension={
((props.itemData.kind === 'Code' || props.itemData.kind === 'Config') &&
props.itemData.extension) ||
''
}
isSidebarPreview={props.isSidebarPreview}
/>
);
type OriginalRenderer = (props: OriginalRendererProps) => JSX.Element;
function originalRendererKind(itemData: ExplorerItemData) {
return itemData.extension === 'pdf' ? 'PDF' : itemData.kind;
}
type OriginalRendererKind = ReturnType<typeof originalRendererKind>;
const ORIGINAL_RENDERERS: {
[K in OriginalRendererKind]?: OriginalRenderer;
} = {
PDF: (props) => (
<PDFViewer
src={props.src}
onLoad={props.onLoad}
onError={props.onError}
className={clsx('h-full w-full', props.className, props.frame && props.frameClassName)}
crossOrigin="anonymous" // Here it is ok, because it is not a react attr
/>
),
Text: TEXT_RENDERER,
Code: TEXT_RENDERER,
Config: TEXT_RENDERER,
Video: (props) => (
<Video
src={props.src}
onLoadedData={props.onLoad}
onError={props.onError}
paused={props.pauseVideo}
controls={props.mediaControls}
blackBars={props.blackBars}
blackBarsSize={props.blackBarsSize}
className={clsx(
props.className,
props.frame && !props.blackBars && props.frameClassName
)}
/>
),
Audio: (props) => (
<>
<img
src={getIcon(iconNames.Audio, props.isDark, props.itemData.extension)}
onLoad={props.onLoad}
decoding={props.size ? 'async' : 'sync'}
className={props.childClassName}
draggable={false}
/>
{props.mediaControls && (
<audio
// Order matter for crossOrigin attr
crossOrigin="anonymous"
src={props.src}
onError={props.onError}
controls
autoPlay
className="absolute left-2/4 top-full w-full -translate-x-1/2 translate-y-[-150%]"
>
<p>{i18n.t('audio_preview_not_supported')}</p>
</audio>
)}
</>
),
Image: (props) => {
const ref = useRef<HTMLImageElement>(null);
const size = useSize(ref);
return (
<Image
ref={ref}
src={props.src}
size={size}
onLoad={props.onLoad}
onError={props.onError}
decoding={props.size ? 'async' : 'sync'}
className={clsx(props.className, props.frameClassName)}
crossOrigin="anonymous" // Here it is ok, because it is not a react attr
/>
);
}
};
interface VideoProps extends VideoHTMLAttributes<HTMLVideoElement> {
paused?: boolean;
blackBars?: boolean;
blackBarsSize?: number;
}
const Video = ({ paused, blackBars, blackBarsSize, className, ...props }: VideoProps) => {
const ref = useRef<HTMLVideoElement>(null);
const size = useSize(ref);
const { style: blackBarsStyle } = useBlackBars(size, blackBarsSize);
const { t } = useLocale();
useEffect(() => {
if (!ref.current) return;
paused ? ref.current.pause() : ref.current.play();
}, [paused]);
return (
<video
// Order matter for crossOrigin attr
crossOrigin="anonymous"
ref={ref}
autoPlay={!paused}
onVolumeChange={(e) => {
const video = e.target as HTMLVideoElement;
explorerStore.mediaPlayerVolume = video.volume;
}}
onCanPlay={(e) => {
const video = e.target as HTMLVideoElement;
// Why not use the element's attribute? Because React...
// https://github.com/facebook/react/issues/10389
video.loop = !props.controls;
video.muted = !props.controls;
video.volume = explorerStore.mediaPlayerVolume;
}}
playsInline
draggable={false}
style={{ ...(blackBars ? blackBarsStyle : {}) }}
className={clsx(blackBars && size.width === 0 && 'invisible', className)}
{...props}
key={props.src}
controls={false}
onTimeUpdate={(e) => {
const video = e.target as HTMLVideoElement;
if (video.currentTime > 0) {
video.controls = props.controls ?? true;
}
}}
>
<p>{t('video_preview_not_supported')}</p>
</video>
);
};