Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new file preview #516

Merged
merged 6 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 113 additions & 20 deletions src/components/FilePreview/FilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from '@components/Buttons';
import ConsentViewer from '@components/ConsentViewer/ConsentViewer';
import { useLocales } from '@context/LocalesContext';
import { FileItem } from '@fairdatasociety/fdp-storage';
import { FC } from 'react';
import { FC, useEffect, useState } from 'react';

interface FilePreviewProps {
file: FileItem;
Expand All @@ -13,6 +13,28 @@ interface FilePreviewProps {
onError: () => void;
}

const MAX_TEXT_PREVIEW_LENGTH = 500;

enum PreviewType {
Consent,
Text,
Video,
Image,
Unknown,
}

const TEXT_FILE_EXTENSIONS = [
'.txt',
'.srt',
'.log',
'.csv',
'.sub',
'.md',
'.json',
];

const IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.svg', '.gif', '.bmp'];

const VIDEO_FILE_EXTENSIONS = [
'.mpg',
'.mp2',
Expand All @@ -32,10 +54,31 @@ const VIDEO_FILE_EXTENSIONS = [
'.swf',
];

export function isFilePreviewSupported(fileName: string): boolean {
return isTextFile(fileName) || isFileImage(fileName) || isFileVideo(fileName);
}

function isString(data: unknown): boolean {
return typeof data === 'string';
}

function checkFileExtension(fileName: string, extensions: string[]): boolean {
const lowercaseFileName = fileName.toLowerCase();
return extensions.some((extension) => lowercaseFileName.endsWith(extension));
}

function isTextFile(fileName: string): boolean {
return checkFileExtension(fileName, TEXT_FILE_EXTENSIONS);
}

function isFileImage(fileName: string): boolean {
return checkFileExtension(fileName, IMAGE_FILE_EXTENSIONS);
}

function isFileVideo(fileName: string): boolean {
return checkFileExtension(fileName, VIDEO_FILE_EXTENSIONS);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isFileConsent(data: any): boolean {
return (
Expand All @@ -54,13 +97,6 @@ function isFileConsent(data: any): boolean {
);
}

function isFileVideo(fileName: string): boolean {
const lowercaseFileName = fileName.toLowerCase();
return VIDEO_FILE_EXTENSIONS.some((extension) =>
lowercaseFileName.endsWith(extension)
);
}

const FilePreview: FC<FilePreviewProps> = ({
file,
pod,
Expand All @@ -69,12 +105,59 @@ const FilePreview: FC<FilePreviewProps> = ({
onError,
}) => {
const { intl } = useLocales();
const [type, setType] = useState<PreviewType | null>(null);
const [content, setContent] = useState<unknown>(source);

const preparePreview = async () => {
try {
const fileName = file.name;

if (isFileVideo(fileName)) {
setContent(window.URL.createObjectURL(source));
return setType(PreviewType.Video);
}

if (isFileImage(fileName)) {
setContent(window.URL.createObjectURL(source));
return setType(PreviewType.Image);
}

if (isTextFile(fileName)) {
const text = await source.text();

if (fileName.endsWith('.json')) {
const json = JSON.parse(text);

if (isFileConsent(json)) {
setType(PreviewType.Consent);
return setContent(json);
}
}

setContent(
text.substring(0, MAX_TEXT_PREVIEW_LENGTH) +
(text.length > MAX_TEXT_PREVIEW_LENGTH ? '...' : '')
);
return setType(PreviewType.Text);
}

throw new Error('Unknown type');
} catch (error) {
setType(PreviewType.Unknown);

onError();
}
};

useEffect(() => {
preparePreview();
}, [file, source]);

if (isFileConsent(source)) {
if (type === PreviewType.Consent) {
return (
<>
<div className="w-full h-auto my-10 rounded">
<ConsentViewer data={source} />
<ConsentViewer data={content} />
</div>
{(!directory || !directory.includes('/')) && (
<div className="mb-4">
Expand All @@ -97,22 +180,32 @@ const FilePreview: FC<FilePreviewProps> = ({
);
}

if (isFileVideo(file.name || '')) {
if (type === PreviewType.Text) {
return (
<p className="w-full h-auto my-10 max-h-48 overflow-auto">{content}</p>
);
}

if (type === PreviewType.Video) {
return (
<video className="w-full h-auto my-10 rounded" controls>
<source src={source} />
<source src={content as string} />
</video>
);
}

return (
<img
src={source}
alt="Preview Image"
className="w-full h-auto my-10 rounded"
onError={onError}
/>
);
if (type === PreviewType.Image) {
return (
<img
src={content as string}
alt="Preview Image"
className="w-full h-auto my-10 rounded"
onError={onError}
/>
);
}

return null;
};

export default FilePreview;
25 changes: 13 additions & 12 deletions src/components/Modals/PreviewFileModal/PreviewFileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import ShareDarkIcon from '@media/UI/share-dark.svg';
import DeleteLightIcon from '@media/UI/delete-light.svg';
import DeleteDarkIcon from '@media/UI/delete-dark.svg';
import Spinner from '@components/Spinner/Spinner';
import FilePreview from '@components/FilePreview/FilePreview';
import FilePreview, {
isFilePreviewSupported,
} from '@components/FilePreview/FilePreview';
import { FileItem } from '@fairdatasociety/fdp-storage';
import { extractFileExtension } from '@utils/filename';
import { useLocales } from '@context/LocalesContext';
Expand All @@ -54,13 +56,17 @@ const PreviewFileModal: FC<PreviewModalProps> = ({
const { activePod, directoryName } = useContext(PodContext);
const [loading, setLoading] = useState(false);

const [imageSource, setImageSource] = useState('');
const [fileContent, setFileContent] = useState<Blob | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [showShareFileModal, setShowShareFileModal] = useState(false);
const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false);
const { intl } = useLocales();

useEffect(() => {
if (!isFilePreviewSupported(previewFile?.name)) {
return;
}

setLoading(true);
downloadFile(fdpClientRef.current, {
filename: previewFile?.name,
Expand All @@ -71,12 +77,7 @@ const PreviewFileModal: FC<PreviewModalProps> = ({
const blob = await response.arrayBuffer();
const content = new Blob([blob]);

if (previewFile?.name.endsWith('.json')) {
const json = await content.text();
return setImageSource(JSON.parse(json));
}

setImageSource(window.URL.createObjectURL(content));
setFileContent(content);
})
.catch((e) => {
setErrorMessage(intl.get('FILE_PREVIEW_ERROR'));
Expand Down Expand Up @@ -152,24 +153,24 @@ const PreviewFileModal: FC<PreviewModalProps> = ({
headerTitle={intl.get('PREVIEW_FILE')}
className="w-full md:w-98"
>
{imageSource ? (
{fileContent ? (
<FilePreview
file={previewFile}
source={imageSource}
source={fileContent}
pod={activePod}
directory={directoryName}
onError={() => setErrorMessage(intl.get('FILE_PREVIEW_ERROR'))}
/>
) : null}
<Spinner isLoading={loading} />
<Spinner className="my-8" isLoading={loading} />

{errorMessage ? (
<div className="my-28 text-color-status-negative-day text-xs text-center leading-none">
{errorMessage}
</div>
) : null}

<h2 className="text-base text-color-accents-purple-black dark:text-color-shade-white-night">
<h2 className="text-base mt-8 text-color-accents-purple-black dark:text-color-shade-white-night">
{previewFile?.name}
</h2>

Expand Down