mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-21 15:07:54 -04:00
* Fix controls flashing when switching videos, tooltip, and video switching in quickpreview * remove state * props
233 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
};
|