28
common/resources/client/uploads/preview/available-previews.ts
Executable file
28
common/resources/client/uploads/preview/available-previews.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
import {ImageFilePreview} from './file-preview/image-file-preview';
|
||||
import {FileEntry} from '../file-entry';
|
||||
import {DefaultFilePreview} from './file-preview/default-file-preview';
|
||||
import {TextFilePreview} from './file-preview/text-file-preview';
|
||||
import {VideoFilePreview} from './file-preview/video-file-preview';
|
||||
import {AudioFilePreview} from './file-preview/audio-file-preview';
|
||||
import {PdfFilePreview} from './file-preview/pdf-file-preview';
|
||||
import {WordDocumentFilePreview} from './file-preview/word-document-file-preview';
|
||||
|
||||
export const AvailablePreviews = {
|
||||
text: TextFilePreview,
|
||||
video: VideoFilePreview,
|
||||
audio: AudioFilePreview,
|
||||
image: ImageFilePreview,
|
||||
pdf: PdfFilePreview,
|
||||
spreadsheet: WordDocumentFilePreview,
|
||||
powerPoint: WordDocumentFilePreview,
|
||||
word: WordDocumentFilePreview,
|
||||
'text/rtf': DefaultFilePreview,
|
||||
} as const;
|
||||
|
||||
export function getPreviewForEntry(entry: FileEntry) {
|
||||
const mime = entry?.mime as keyof typeof AvailablePreviews;
|
||||
const type = entry?.type as keyof typeof AvailablePreviews;
|
||||
return (
|
||||
AvailablePreviews[mime] || AvailablePreviews[type] || DefaultFilePreview
|
||||
);
|
||||
}
|
||||
209
common/resources/client/uploads/preview/file-preview-container.tsx
Executable file
209
common/resources/client/uploads/preview/file-preview-container.tsx
Executable file
@@ -0,0 +1,209 @@
|
||||
import {AnimatePresence, m} from 'framer-motion';
|
||||
import {Fragment, ReactNode, useContext, useMemo} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {getPreviewForEntry} from './available-previews';
|
||||
import {FileEntry} from '../file-entry';
|
||||
import {FilePreviewContext} from './file-preview-context';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {ChevronLeftIcon} from '../../icons/material/ChevronLeft';
|
||||
import {ChevronRightIcon} from '../../icons/material/ChevronRight';
|
||||
import {FileDownloadIcon} from '../../icons/material/FileDownload';
|
||||
import {downloadFileFromUrl} from '../utils/download-file-from-url';
|
||||
import {useFileEntryUrls} from '../hooks/file-entry-urls';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {CloseIcon} from '../../icons/material/Close';
|
||||
import {FileThumbnail} from '../file-type-icon/file-thumbnail';
|
||||
import {useMediaQuery} from '../../utils/hooks/use-media-query';
|
||||
import {KeyboardArrowLeftIcon} from '../../icons/material/KeyboardArrowLeft';
|
||||
import {KeyboardArrowRightIcon} from '../../icons/material/KeyboardArrowRight';
|
||||
import {useControlledState} from '@react-stately/utils';
|
||||
import {opacityAnimation} from '../../ui/animation/opacity-animation';
|
||||
|
||||
export interface FilePreviewContainerProps {
|
||||
entries: FileEntry[];
|
||||
activeIndex?: number;
|
||||
defaultActiveIndex?: number;
|
||||
onActiveIndexChange?: (index: number) => void;
|
||||
onClose?: () => void;
|
||||
showHeader?: boolean;
|
||||
headerActionsLeft?: ReactNode;
|
||||
className?: string;
|
||||
allowDownload?: boolean;
|
||||
}
|
||||
export function FilePreviewContainer({
|
||||
entries,
|
||||
onClose,
|
||||
showHeader = true,
|
||||
className,
|
||||
headerActionsLeft,
|
||||
allowDownload = true,
|
||||
...props
|
||||
}: FilePreviewContainerProps) {
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
const [activeIndex, setActiveIndex] = useControlledState(
|
||||
props.activeIndex,
|
||||
props.defaultActiveIndex || 0,
|
||||
props.onActiveIndexChange
|
||||
);
|
||||
|
||||
const activeEntry = entries[activeIndex];
|
||||
const contextValue = useMemo(() => {
|
||||
return {entries, activeIndex};
|
||||
}, [entries, activeIndex]);
|
||||
const Preview = getPreviewForEntry(activeEntry);
|
||||
|
||||
if (!activeEntry) {
|
||||
onClose?.();
|
||||
return null;
|
||||
}
|
||||
|
||||
const canOpenNext = entries.length - 1 > activeIndex;
|
||||
const openNext = () => {
|
||||
setActiveIndex(activeIndex + 1);
|
||||
};
|
||||
const canOpenPrevious = activeIndex > 0;
|
||||
const openPrevious = () => {
|
||||
setActiveIndex(activeIndex - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<FilePreviewContext.Provider value={contextValue}>
|
||||
{showHeader && (
|
||||
<Header
|
||||
actionsLeft={headerActionsLeft}
|
||||
isMobile={isMobile}
|
||||
onClose={onClose}
|
||||
onNext={canOpenNext ? openNext : undefined}
|
||||
onPrevious={canOpenPrevious ? openPrevious : undefined}
|
||||
allowDownload={allowDownload}
|
||||
/>
|
||||
)}
|
||||
<div className={clsx('overflow-hidden relative flex-auto', className)}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
size="lg"
|
||||
className="text-muted absolute left-0 top-1/2 transform -translate-y-1/2 z-10"
|
||||
disabled={!canOpenPrevious}
|
||||
onClick={openPrevious}
|
||||
>
|
||||
<KeyboardArrowLeftIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
<m.div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
key={activeEntry.id}
|
||||
{...opacityAnimation}
|
||||
>
|
||||
<Preview
|
||||
className="max-h-[calc(100%-30px)]"
|
||||
entry={activeEntry}
|
||||
allowDownload={allowDownload}
|
||||
/>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
size="lg"
|
||||
className="text-muted absolute right-0 top-1/2 transform -translate-y-1/2 z-10"
|
||||
disabled={!canOpenNext}
|
||||
onClick={openNext}
|
||||
>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</FilePreviewContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
onNext?: () => void;
|
||||
onPrevious?: () => void;
|
||||
onClose?: () => void;
|
||||
isMobile: boolean | null;
|
||||
actionsLeft?: ReactNode;
|
||||
allowDownload?: boolean;
|
||||
}
|
||||
function Header({
|
||||
onNext,
|
||||
onPrevious,
|
||||
onClose,
|
||||
isMobile,
|
||||
actionsLeft,
|
||||
allowDownload,
|
||||
}: HeaderProps) {
|
||||
const {entries, activeIndex} = useContext(FilePreviewContext);
|
||||
const activeEntry = entries[activeIndex];
|
||||
const {downloadUrl} = useFileEntryUrls(activeEntry);
|
||||
|
||||
const desktopDownloadButton = (
|
||||
<Button
|
||||
startIcon={<FileDownloadIcon />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
if (downloadUrl) {
|
||||
downloadFileFromUrl(downloadUrl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans message="Download" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const mobileDownloadButton = (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (downloadUrl) {
|
||||
downloadFileFromUrl(downloadUrl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FileDownloadIcon />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const downloadButton = isMobile
|
||||
? mobileDownloadButton
|
||||
: desktopDownloadButton;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-20 bg-paper border-b flex-shrink-0 text-sm min-h-50 px-10 text-muted">
|
||||
<div className="flex items-center gap-4 w-1/3 justify-start">
|
||||
{actionsLeft}
|
||||
{allowDownload ? downloadButton : undefined}
|
||||
</div>
|
||||
<div className="flex items-center gap-10 w-1/3 justify-center flex-nowrap text-main">
|
||||
<FileThumbnail
|
||||
file={activeEntry}
|
||||
iconClassName="w-16 h-16"
|
||||
showImage={false}
|
||||
/>
|
||||
<div className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{activeEntry.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/3 flex items-center gap-10 justify-end whitespace-nowrap">
|
||||
{!isMobile && (
|
||||
<Fragment>
|
||||
<IconButton disabled={!onPrevious} onClick={onPrevious}>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
<div>{activeIndex + 1}</div>
|
||||
<div>/</div>
|
||||
<div>{entries.length}</div>
|
||||
<IconButton disabled={!onNext} onClick={onNext}>
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
<div className="bg-divider w-1 h-24 mx-20" />
|
||||
</Fragment>
|
||||
)}
|
||||
<IconButton radius="rounded-none" onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
common/resources/client/uploads/preview/file-preview-context.ts
Executable file
11
common/resources/client/uploads/preview/file-preview-context.ts
Executable file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import {FileEntry} from '../file-entry';
|
||||
|
||||
export interface FilePreviewContextValue {
|
||||
entries: FileEntry[];
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
export const FilePreviewContext = React.createContext<FilePreviewContextValue>(
|
||||
null!
|
||||
);
|
||||
24
common/resources/client/uploads/preview/file-preview-dialog.tsx
Executable file
24
common/resources/client/uploads/preview/file-preview-dialog.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
FilePreviewContainer,
|
||||
FilePreviewContainerProps,
|
||||
} from './file-preview-container';
|
||||
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../ui/overlays/dialog/dialog';
|
||||
|
||||
interface Props extends Omit<FilePreviewContainerProps, 'onClose'> {}
|
||||
export function FilePreviewDialog(props: Props) {
|
||||
return (
|
||||
<Dialog
|
||||
size="fullscreenTakeover"
|
||||
background="bg-alt"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Content {...props} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Content(props: Props) {
|
||||
const {close} = useDialogContext();
|
||||
return <FilePreviewContainer onClose={close} {...props} />;
|
||||
}
|
||||
37
common/resources/client/uploads/preview/file-preview/audio-file-preview.tsx
Executable file
37
common/resources/client/uploads/preview/file-preview/audio-file-preview.tsx
Executable file
@@ -0,0 +1,37 @@
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
export function AudioFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
const ref = useRef<HTMLAudioElement>(null);
|
||||
const [mediaInvalid, setMediaInvalid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMediaInvalid(!ref.current?.canPlayType(entry.mime));
|
||||
}, [entry]);
|
||||
|
||||
if (mediaInvalid || !previewUrl) {
|
||||
return <DefaultFilePreview {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<audio
|
||||
className={className}
|
||||
ref={ref}
|
||||
controls
|
||||
controlsList="nodownload noremoteplayback"
|
||||
autoPlay
|
||||
>
|
||||
<source
|
||||
src={previewUrl}
|
||||
type={entry.mime}
|
||||
onError={() => {
|
||||
setMediaInvalid(true);
|
||||
}}
|
||||
/>
|
||||
</audio>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {ReactNode, useContext} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {Button} from '../../../ui/buttons/button';
|
||||
import {downloadFileFromUrl} from '../../utils/download-file-from-url';
|
||||
import {FilePreviewContext} from '../file-preview-context';
|
||||
import {Trans} from '../../../i18n/trans';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
|
||||
interface Props extends FilePreviewProps {
|
||||
message?: ReactNode;
|
||||
}
|
||||
export function DefaultFilePreview({message, className, allowDownload}: Props) {
|
||||
const {entries, activeIndex} = useContext(FilePreviewContext);
|
||||
const activeEntry = entries[activeIndex];
|
||||
const content = message || <Trans message="No file preview available" />;
|
||||
const {downloadUrl} = useFileEntryUrls(activeEntry);
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
'shadow bg-paper max-w-400 w-[calc(100%-40px)] text-center p-40 rounded'
|
||||
)}
|
||||
>
|
||||
<div className="text-lg">{content}</div>
|
||||
{allowDownload && (
|
||||
<div className="block mt-20 text-center">
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (downloadUrl) {
|
||||
downloadFileFromUrl(downloadUrl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans message="Download" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {FileEntry} from '../../file-entry';
|
||||
|
||||
export interface FilePreviewProps {
|
||||
entry: FileEntry;
|
||||
className?: string;
|
||||
allowDownload?: boolean;
|
||||
}
|
||||
26
common/resources/client/uploads/preview/file-preview/image-file-preview.tsx
Executable file
26
common/resources/client/uploads/preview/file-preview/image-file-preview.tsx
Executable file
@@ -0,0 +1,26 @@
|
||||
import clsx from 'clsx';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
import {useTrans} from '../../../i18n/use-trans';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
|
||||
export function ImageFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {trans} = useTrans();
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
|
||||
if (!previewUrl) {
|
||||
return <DefaultFilePreview {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={clsx(className, 'shadow')}
|
||||
src={previewUrl}
|
||||
alt={trans({
|
||||
message: 'Preview for :name',
|
||||
values: {name: entry.name},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
common/resources/client/uploads/preview/file-preview/pdf-file-preview.tsx
Executable file
26
common/resources/client/uploads/preview/file-preview/pdf-file-preview.tsx
Executable file
@@ -0,0 +1,26 @@
|
||||
import clsx from 'clsx';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
import {useTrans} from '../../../i18n/use-trans';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
|
||||
export function PdfFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {trans} = useTrans();
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
|
||||
if (!previewUrl) {
|
||||
return <DefaultFilePreview {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe
|
||||
title={trans({
|
||||
message: 'Preview for :name',
|
||||
values: {name: entry.name},
|
||||
})}
|
||||
className={clsx(className, 'w-full h-full')}
|
||||
src={`${previewUrl}#toolbar=0`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
91
common/resources/client/uploads/preview/file-preview/text-file-preview.tsx
Executable file
91
common/resources/client/uploads/preview/file-preview/text-file-preview.tsx
Executable file
@@ -0,0 +1,91 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
import {ProgressCircle} from '@common/ui/progress/progress-circle';
|
||||
import {useFileEntryUrls} from '@common/uploads/hooks/file-entry-urls';
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
|
||||
const FIVE_MB = 5242880;
|
||||
|
||||
export function TextFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {trans} = useTrans();
|
||||
const [tooLarge, setTooLarge] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isFailed, setIsFailed] = useState(false);
|
||||
const [contents, setContents] = useState<string | null>(null);
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
|
||||
useEffect(() => {
|
||||
if (!entry) return;
|
||||
if (!previewUrl) {
|
||||
setIsFailed(true);
|
||||
} else if (entry.file_size! >= FIVE_MB) {
|
||||
setTooLarge(true);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
getFileContents(previewUrl)
|
||||
.then(response => {
|
||||
setContents(response.data);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsFailed(true);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [entry, previewUrl]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ProgressCircle
|
||||
isIndeterminate
|
||||
aria-label={trans({message: 'Loading file contents'})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (tooLarge) {
|
||||
return (
|
||||
<DefaultFilePreview
|
||||
{...props}
|
||||
message={<Trans message="This file is too large to preview." />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFailed) {
|
||||
return (
|
||||
<DefaultFilePreview
|
||||
{...props}
|
||||
message={<Trans message="There was an issue previewing this file" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={clsx(
|
||||
'rounded bg-paper p-20 text-sm whitespace-pre-wrap break-words h-full overflow-y-auto w-full',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="container mx-auto">{`${contents}`}</div>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
function getFileContents(src: string) {
|
||||
return apiClient.get(src, {
|
||||
responseType: 'text',
|
||||
// required for s3 presigned url to work
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
Accept: 'text/plain',
|
||||
},
|
||||
});
|
||||
}
|
||||
38
common/resources/client/uploads/preview/file-preview/video-file-preview.tsx
Executable file
38
common/resources/client/uploads/preview/file-preview/video-file-preview.tsx
Executable file
@@ -0,0 +1,38 @@
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
|
||||
export function VideoFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const [mediaInvalid, setMediaInvalid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMediaInvalid(!ref.current?.canPlayType(entry.mime));
|
||||
}, [entry]);
|
||||
|
||||
if (mediaInvalid || !previewUrl) {
|
||||
return <DefaultFilePreview {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
className={className}
|
||||
ref={ref}
|
||||
controls
|
||||
controlsList="nodownload noremoteplayback"
|
||||
playsInline
|
||||
autoPlay
|
||||
>
|
||||
<source
|
||||
src={previewUrl}
|
||||
type={entry.mime}
|
||||
onError={() => {
|
||||
setMediaInvalid(true);
|
||||
}}
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import clsx from 'clsx';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {FilePreviewProps} from './file-preview-props';
|
||||
import {DefaultFilePreview} from './default-file-preview';
|
||||
import {ProgressCircle} from '../../../ui/progress/progress-circle';
|
||||
import {FileEntry} from '../../file-entry';
|
||||
import {useFileEntryUrls} from '../../hooks/file-entry-urls';
|
||||
import {useTrans} from '../../../i18n/use-trans';
|
||||
import {apiClient} from '../../../http/query-client';
|
||||
|
||||
export function WordDocumentFilePreview(props: FilePreviewProps) {
|
||||
const {entry, className} = props;
|
||||
const {trans} = useTrans();
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const [showDefault, setShowDefault] = useState(false);
|
||||
const timeoutId = useRef<any>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {previewUrl} = useFileEntryUrls(entry);
|
||||
|
||||
useEffect(() => {
|
||||
// Google Docs viewer only supports files up to 25MB
|
||||
if (!previewUrl) {
|
||||
setShowDefault(true);
|
||||
} else if (entry.file_size && entry.file_size > 25000000) {
|
||||
setShowDefault(true);
|
||||
} else if (ref.current) {
|
||||
ref.current.onload = () => {
|
||||
clearTimeout(timeoutId.current);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
buildPreviewUrl(previewUrl, entry).then(url => {
|
||||
if (ref.current) {
|
||||
ref.current.src = url;
|
||||
}
|
||||
});
|
||||
|
||||
// if preview iframe is not loaded
|
||||
// after 5 seconds, bail and show default preview
|
||||
timeoutId.current = setTimeout(() => {
|
||||
setShowDefault(true);
|
||||
}, 5000);
|
||||
}
|
||||
}, [entry, previewUrl]);
|
||||
|
||||
if (showDefault) {
|
||||
return <DefaultFilePreview {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'w-full h-full')}>
|
||||
{isLoading && <ProgressCircle />}
|
||||
<iframe
|
||||
ref={ref}
|
||||
title={trans({
|
||||
message: 'Preview for :name',
|
||||
values: {name: entry.name},
|
||||
})}
|
||||
className={clsx('w-full h-full', isLoading && 'hidden')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function buildPreviewUrl(
|
||||
urlString: string,
|
||||
entry: FileEntry
|
||||
): Promise<string> {
|
||||
const url = new URL(urlString);
|
||||
// if we're not trying to preview shareable link we will need to generate
|
||||
// preview token, otherwise it won't be publicly accessible
|
||||
if (!url.searchParams.has('shareable_link')) {
|
||||
const {data} = await apiClient.post(
|
||||
`file-entries/${entry.id}/add-preview-token`
|
||||
);
|
||||
url.searchParams.append('preview_token', data.preview_token);
|
||||
}
|
||||
|
||||
return buildOfficeLivePreviewUrl(url);
|
||||
}
|
||||
|
||||
function buildOfficeLivePreviewUrl(url: URL) {
|
||||
// https://docs.google.com/gview?embedded=true&url=
|
||||
return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
|
||||
url.toString()
|
||||
)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user