first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,7 @@
import {FileEntry} from '../../file-entry';
export interface FilePreviewProps {
entry: FileEntry;
className?: string;
allowDownload?: boolean;
}

View 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},
})}
/>
);
}

View 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`}
/>
);
}

View 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',
},
});
}

View 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>
);
}

View File

@@ -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()
)}`;
}