mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-01-15 01:14:27 -05:00
feat: add preview modal
This commit is contained in:
parent
f82099f36e
commit
c807d208d8
5 changed files with 226 additions and 142 deletions
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
Box,
|
||||||
Group,
|
Group,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
|
@ -8,9 +9,6 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useClipboard } from "@mantine/hooks";
|
import { useClipboard } from "@mantine/hooks";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import mime from "mime-types";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { TbDownload, TbEye, TbLink } from "react-icons/tb";
|
import { TbDownload, TbEye, TbLink } from "react-icons/tb";
|
||||||
import useConfig from "../../hooks/config.hook";
|
import useConfig from "../../hooks/config.hook";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
|
@ -18,6 +16,7 @@ import { FileMetaData } from "../../types/File.type";
|
||||||
import { Share } from "../../types/share.type";
|
import { Share } from "../../types/share.type";
|
||||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
import showFilePreviewModal from "./modals/showFilePreviewModal";
|
||||||
|
|
||||||
const FileList = ({
|
const FileList = ({
|
||||||
files,
|
files,
|
||||||
|
@ -53,6 +52,7 @@ const FileList = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -72,18 +72,19 @@ const FileList = ({
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
{shareService.doesFileSupportPreview(file.name) && (
|
{shareService.doesFileSupportPreview(file.name) && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={Link}
|
onClick={() =>
|
||||||
href={`/share/${share.id}/preview/${
|
showFilePreviewModal(share.id, file, modals)
|
||||||
file.id
|
}
|
||||||
}?type=${mime.contentType(file.name)}`}
|
|
||||||
target="_blank"
|
|
||||||
size={25}
|
size={25}
|
||||||
>
|
>
|
||||||
<TbEye />
|
<TbEye />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
{!share.hasPassword && (
|
{!share.hasPassword && (
|
||||||
<ActionIcon size={25} onClick={() => copyFileLink(file)}>
|
<ActionIcon
|
||||||
|
size={25}
|
||||||
|
onClick={() => copyFileLink(file)}
|
||||||
|
>
|
||||||
<TbLink />
|
<TbLink />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
|
@ -101,6 +102,7 @@ const FileList = ({
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
154
frontend/src/components/share/FilePreview.tsx
Normal file
154
frontend/src/components/share/FilePreview.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { Button, Center, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const FilePreviewContext = React.createContext<{
|
||||||
|
shareId: string;
|
||||||
|
fileId: string;
|
||||||
|
mimeType: string;
|
||||||
|
setIsNotSupported: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}>({
|
||||||
|
shareId: "",
|
||||||
|
fileId: "",
|
||||||
|
mimeType: "",
|
||||||
|
setIsNotSupported: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const FilePreview = ({
|
||||||
|
shareId,
|
||||||
|
fileId,
|
||||||
|
mimeType,
|
||||||
|
}: {
|
||||||
|
shareId: string;
|
||||||
|
fileId: string;
|
||||||
|
mimeType: string;
|
||||||
|
}) => {
|
||||||
|
const [isNotSupported, setIsNotSupported] = useState(false);
|
||||||
|
if (isNotSupported) return <UnSupportedFile />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<FilePreviewContext.Provider
|
||||||
|
value={{ shareId, fileId, mimeType, setIsNotSupported }}
|
||||||
|
>
|
||||||
|
<FileDecider />
|
||||||
|
</FilePreviewContext.Provider>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
component={Link}
|
||||||
|
onClick={() => modals.closeAll()}
|
||||||
|
target="_blank"
|
||||||
|
href={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
>
|
||||||
|
View original file
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileDecider = () => {
|
||||||
|
const { mimeType, setIsNotSupported } = React.useContext(FilePreviewContext);
|
||||||
|
|
||||||
|
if (mimeType == "application/pdf") {
|
||||||
|
return <PdfPreview />;
|
||||||
|
} else if (mimeType.startsWith("video/")) {
|
||||||
|
return <VideoPreview />;
|
||||||
|
} else if (mimeType.startsWith("image/")) {
|
||||||
|
return <ImagePreview />;
|
||||||
|
} else if (mimeType.startsWith("audio/")) {
|
||||||
|
return <AudioPreview />;
|
||||||
|
} else if (mimeType == "text/plain") {
|
||||||
|
return <TextPreview />;
|
||||||
|
} else {
|
||||||
|
setIsNotSupported(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioPreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||||
|
<audio controls style={{ width: "100%" }}>
|
||||||
|
<source
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
</audio>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VideoPreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
<video width="100%" controls>
|
||||||
|
<source
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImagePreview = () => {
|
||||||
|
const { shareId, fileId, setIsNotSupported } =
|
||||||
|
React.useContext(FilePreviewContext);
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
||||||
|
alt={`${fileId}_preview`}
|
||||||
|
width="100%"
|
||||||
|
onError={() => setIsNotSupported(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextPreview = () => {
|
||||||
|
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||||
|
const [text, setText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`/api/shares/${shareId}/files/${fileId}?download=false`)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => setText(text));
|
||||||
|
}, [shareId, fileId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
||||||
|
<Text size="sm">{text}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PdfPreview = () => {
|
||||||
|
const { shareId, fileId } = React.useContext(FilePreviewContext);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnSupportedFile = () => {
|
||||||
|
return (
|
||||||
|
<Center style={{ minHeight: 200 }}>
|
||||||
|
<Stack align="center" spacing={10}>
|
||||||
|
<Title order={3}>Preview not supported</Title>
|
||||||
|
<Text>
|
||||||
|
A preview for thise file type is unsupported. Please download the file
|
||||||
|
to view it.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilePreview;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
|
import mime from "mime-types";
|
||||||
|
import { FileMetaData } from "../../../types/File.type";
|
||||||
|
import FilePreview from "../FilePreview";
|
||||||
|
|
||||||
|
const showFilePreviewModal = (
|
||||||
|
shareId: string,
|
||||||
|
file: FileMetaData,
|
||||||
|
modals: ModalsContextProps
|
||||||
|
) => {
|
||||||
|
const mimeType = (mime.contentType(file.name) || "").split(";")[0];
|
||||||
|
return modals.openModal({
|
||||||
|
size: "xl",
|
||||||
|
title: file.name,
|
||||||
|
children: (
|
||||||
|
<FilePreview shareId={shareId} fileId={file.id} mimeType={mimeType} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default showFilePreviewModal;
|
|
@ -1,94 +0,0 @@
|
||||||
import { Center, Stack, Text, Title } from "@mantine/core";
|
|
||||||
import { GetServerSidePropsContext } from "next";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
|
||||||
const { shareId, fileId } = context.params!;
|
|
||||||
|
|
||||||
const mimeType = context.query.type as string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: { shareId, fileId, mimeType },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const UnSupportedFile = () => {
|
|
||||||
return (
|
|
||||||
<Center style={{ height: "70vh" }}>
|
|
||||||
<Stack align="center" spacing={10}>
|
|
||||||
<Title order={3}>Preview not supported</Title>
|
|
||||||
<Text>
|
|
||||||
A preview for thise file type is unsupported. Please download the file
|
|
||||||
to view it.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilePreview = ({
|
|
||||||
shareId,
|
|
||||||
fileId,
|
|
||||||
mimeType,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
fileId: string;
|
|
||||||
mimeType: string;
|
|
||||||
}) => {
|
|
||||||
const [isNotSupported, setIsNotSupported] = useState(false);
|
|
||||||
|
|
||||||
if (isNotSupported) return <UnSupportedFile />;
|
|
||||||
|
|
||||||
if (mimeType == "application/pdf") {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.location.href = `/api/shares/${shareId}/files/${fileId}?download=false`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else if (mimeType.startsWith("video/")) {
|
|
||||||
return (
|
|
||||||
<video
|
|
||||||
width="100%"
|
|
||||||
controls
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<source src={`/api/shares/${shareId}/files/${fileId}?download=false`} />
|
|
||||||
</video>
|
|
||||||
);
|
|
||||||
} else if (mimeType.startsWith("image/")) {
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
|
||||||
alt={`${fileId}_preview`}
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (mimeType.startsWith("audio/")) {
|
|
||||||
return (
|
|
||||||
<Center style={{ height: "70vh" }}>
|
|
||||||
<Stack align="center" spacing={10} style={{ width: "100%" }}>
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
onError={() => {
|
|
||||||
setIsNotSupported(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
|
|
||||||
/>
|
|
||||||
</audio>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <UnSupportedFile />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilePreview;
|
|
|
@ -53,7 +53,7 @@ const isShareIdAvailable = async (id: string): Promise<boolean> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const doesFileSupportPreview = (fileName: string) => {
|
const doesFileSupportPreview = (fileName: string) => {
|
||||||
const mimeType = mime.contentType(fileName);
|
const mimeType = (mime.contentType(fileName) || "").split(";")[0];
|
||||||
|
|
||||||
if (!mimeType) return false;
|
if (!mimeType) return false;
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ const doesFileSupportPreview = (fileName: string) => {
|
||||||
mimeType.startsWith("video/"),
|
mimeType.startsWith("video/"),
|
||||||
mimeType.startsWith("image/"),
|
mimeType.startsWith("image/"),
|
||||||
mimeType.startsWith("audio/"),
|
mimeType.startsWith("audio/"),
|
||||||
|
mimeType == "text/plain",
|
||||||
mimeType == "application/pdf",
|
mimeType == "application/pdf",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue