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