mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Finalize basic function
This commit is contained in:
@@ -58,7 +58,9 @@ Since Google Drive direct download need the file to be public, I implement the `
|
||||
- [x] Fetch file or folder `700~1400ms`
|
||||
- [x] Fetch breadcrumb `900~1500ms`
|
||||
- [x] Fetch readme `1100~1200ms`
|
||||
- [x] Fetch banner `500-900ms`
|
||||
- [ ] Fetch readme from specific folder `1200~1600ms`
|
||||
- [x] Fetch banner `500-800ms`
|
||||
- [x] Fetch banner from specific folder `500~800ms`
|
||||
- [ ] Protect folder
|
||||
|
||||
### Fetch files `/api/files` | `14/17 Completed`
|
||||
|
||||
@@ -9,21 +9,27 @@ export default function ErrorFeedback({ message }: Props) {
|
||||
Please check the following details and try again:`;
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col"}>
|
||||
<div className={"mx-auto flex items-center justify-center gap-4"}>
|
||||
<div className={"flex w-full flex-col"}>
|
||||
<div
|
||||
className={
|
||||
"mx-auto mb-2 flex items-center justify-center gap-4 max-tablet:flex-col"
|
||||
}
|
||||
>
|
||||
<MdWarning className={"h-8 w-8 text-red-500 dark:text-red-400"} />
|
||||
<p className={"text-start"}>{errorMessage}</p>
|
||||
</div>
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
<div className={"divider-horizontal mx-auto max-w-screen-md"} />
|
||||
<div
|
||||
className={
|
||||
"mx-auto flex w-full max-w-[50%] flex-col items-start justify-center"
|
||||
"mx-auto mt-2 flex w-full flex-col items-start justify-center"
|
||||
}
|
||||
>
|
||||
<span>Error details:</span>
|
||||
<pre className={"w-full py-2"}>
|
||||
<code>{message || "Internal server error"}</code>
|
||||
<code className={"whitespace-pre-wrap"}>
|
||||
{message || "Internal server error"}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BreadCrumbsResponse, TFileParent } from "types/googleapis";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { MdHome } from "react-icons/md";
|
||||
import ReactLoading from "react-loading";
|
||||
import siteConfig from "config/site.config";
|
||||
|
||||
type Props = {
|
||||
@@ -11,12 +10,14 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
const reverseBreadcrumbs = data?.breadcrumbs?.slice().reverse();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className='flex h-[1.5rem] w-32 animate-pulse items-center gap-2 rounded bg-zinc-300 dark:bg-zinc-600' />
|
||||
) : (
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<div className={"flex items-center gap-1"}>
|
||||
{/* Home */}
|
||||
<Link
|
||||
href={"/"}
|
||||
@@ -27,22 +28,35 @@ export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
</Link>
|
||||
{data?.isLimitReached && (
|
||||
<Fragment>
|
||||
<span>...</span>
|
||||
<span>{siteConfig.breadcrumb.limiter}</span>
|
||||
<span>...</span>
|
||||
</Fragment>
|
||||
)}
|
||||
{data?.breadcrumbs.map((parent: TFileParent, index: number) => (
|
||||
<Link
|
||||
href={`/folder/${parent.id}`}
|
||||
key={index}
|
||||
className={"flex items-center gap-1"}
|
||||
>
|
||||
<span>{parent.name}</span>
|
||||
{index !== data.breadcrumbs.length - 1 && (
|
||||
<div
|
||||
className={
|
||||
"line-clamp-1 flex w-auto flex-grow-0 items-center gap-1 overflow-hidden break-words break-all"
|
||||
}
|
||||
>
|
||||
{reverseBreadcrumbs?.map((parent: TFileParent, index: number) => (
|
||||
<Fragment key={index}>
|
||||
<span>{siteConfig.breadcrumb.limiter}</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href={`/folder/${parent.id}`}
|
||||
key={index}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
index === reverseBreadcrumbs.length - 1
|
||||
? "line-clamp-1 w-auto flex-grow-0 overflow-hidden break-words font-bold"
|
||||
: "whitespace-nowrap"
|
||||
}`}
|
||||
>
|
||||
{parent.name}
|
||||
</span>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function GridFile({ data }: Props) {
|
||||
{/* Thumbnail */}
|
||||
<div
|
||||
className={`relative mx-auto grid h-40 w-full place-items-center overflow-hidden rounded-lg tablet:h-32 tablet:rounded-xl ${
|
||||
!allowThumbnail && "border border-zinc-700"
|
||||
!allowThumbnail && "border border-zinc-300 dark:border-zinc-700"
|
||||
}`}
|
||||
>
|
||||
{data.thumbnailLink && allowThumbnail ? (
|
||||
|
||||
@@ -2,10 +2,9 @@ import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import config from "config/site.config";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import H5AudioPlayer from "react-h5-audio-player";
|
||||
import "react-h5-audio-player/lib/styles.css";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
@@ -13,13 +12,11 @@ type Props = {
|
||||
|
||||
export default function AudioPreview({ data }: Props) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [audioSrc, setAudioSrc] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsError(true);
|
||||
setErrorMessage("Audio took too long to load");
|
||||
setIsLoading(false);
|
||||
}, config.preview.timeout);
|
||||
@@ -32,7 +29,6 @@ export default function AudioPreview({ data }: Props) {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
audio.onerror = (error: any) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(error.message);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeout);
|
||||
@@ -45,17 +41,11 @@ export default function AudioPreview({ data }: Props) {
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading audio preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : isError ? (
|
||||
<ErrorFeedback
|
||||
message={errorMessage}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={data}
|
||||
error={errorMessage}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<div className={"preview-audio w-full"}>
|
||||
<H5AudioPlayer
|
||||
src={audioSrc}
|
||||
@@ -64,7 +54,7 @@ export default function AudioPreview({ data }: Props) {
|
||||
customAdditionalControls={[]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import MarkdownRender from "components/utility/MarkdownRender";
|
||||
import fetcher from "utils/swrFetch";
|
||||
import { getCodeLanguage } from "utils/mimeTypesHelper";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
data: drive_v3.Schema$File;
|
||||
};
|
||||
|
||||
export default function CodePreview({ data }: Props) {
|
||||
const [codeContent, setCodeContent] = useState<string>("");
|
||||
|
||||
const fileId = createFileId(data);
|
||||
const {
|
||||
data: swrData,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
});
|
||||
error,
|
||||
} = useSWR(`/api/files/${fileId}?download=1`);
|
||||
|
||||
useEffect(() => {
|
||||
if (swrData) {
|
||||
@@ -41,21 +33,15 @@ export default function CodePreview({ data }: Props) {
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading code preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : error ? (
|
||||
<ErrorFeedback
|
||||
message={error.message || "Failed to load code"}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={codeContent}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<div className={"w-full"}>
|
||||
<MarkdownRender content={codeContent} />
|
||||
</div>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import config from "config/site.config";
|
||||
import { reverseString } from "utils/hashHelper";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
@@ -13,13 +12,11 @@ type Props = {
|
||||
|
||||
export default function ImagePreview({ data, hash }: Props) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [imageSrc, setImageSrc] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsError(true);
|
||||
setErrorMessage("Image took too long to load");
|
||||
setIsLoading(false);
|
||||
}, config.preview.timeout);
|
||||
@@ -31,7 +28,6 @@ export default function ImagePreview({ data, hash }: Props) {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
image.onerror = (error: any) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(error.message);
|
||||
setIsLoading(false);
|
||||
clearTimeout(timeout);
|
||||
@@ -44,23 +40,17 @@ export default function ImagePreview({ data, hash }: Props) {
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading image preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : isError ? (
|
||||
<ErrorFeedback
|
||||
message={errorMessage}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={data}
|
||||
error={errorMessage}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={(data.name as string) || reverseString(hash as string)}
|
||||
className='max-h-full max-w-full rounded-lg'
|
||||
/>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,32 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import useSWR from "swr";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import MarkdownRender from "components/utility/MarkdownRender";
|
||||
import fetcher from "utils/swrFetch";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
data: drive_v3.Schema$File;
|
||||
};
|
||||
|
||||
export default function MarkdownPreview({ data }: Props) {
|
||||
const fileId = createFileId(data);
|
||||
const {
|
||||
data: swrData,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
});
|
||||
} = useSWR(`/api/files/${fileId}?download=1`);
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading code preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : error ? (
|
||||
<ErrorFeedback
|
||||
message={error.message || "Failed to load code"}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={swrData}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<div className={"w-full"}>
|
||||
<MarkdownRender content={swrData as string} />
|
||||
</div>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@ export default function ModelPreview({ data }: Props) {
|
||||
if (!getLoader) {
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
<ErrorFeedback
|
||||
message={"Failed to load model"}
|
||||
useContainer={false}
|
||||
/>
|
||||
<ErrorFeedback message={"Failed to load model"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useState } from "react";
|
||||
import config from "config/site.config";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
data: drive_v3.Schema$File;
|
||||
};
|
||||
|
||||
export default function PDFPreview({ data }: Props) {
|
||||
const fileId = createFileId(data);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
|
||||
const fileURL = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${data.id}/download`;
|
||||
const fileURL = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
|
||||
let providerURL;
|
||||
switch (config.preview.pdfProvider) {
|
||||
case "google":
|
||||
providerURL = `https://drive.google.com/viewerng/viewer?embedded=true&url=${fileURL}`;
|
||||
break;
|
||||
case "microsoft":
|
||||
providerURL = `https://view.officeapps.live.com/op/embed.aspx?src=${fileURL}`;
|
||||
break;
|
||||
case "mozilla":
|
||||
providerURL = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${fileURL}`;
|
||||
break;
|
||||
@@ -33,34 +29,28 @@ export default function PDFPreview({ data }: Props) {
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading PDF preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : isError ? (
|
||||
<ErrorFeedback
|
||||
message={errorMessage}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={data}
|
||||
error={errorMessage}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<></>
|
||||
)}
|
||||
</SWRLayout>
|
||||
<div
|
||||
className={`${
|
||||
isLoading || isError ? "hidden" : "flex"
|
||||
} mx-auto min-h-[70vh] w-full overflow-hidden rounded-lg`}
|
||||
isLoading ? "hidden" : "block"
|
||||
} mx-auto h-full w-full overflow-hidden rounded-lg`}
|
||||
>
|
||||
<iframe
|
||||
width={"100%"}
|
||||
className={"h-auto"}
|
||||
height={"100%"}
|
||||
className={"h-[50vh] tablet:h-[75vh]"}
|
||||
src={providerURL}
|
||||
title={data.name as string}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onError={(error: any) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(error.message);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
|
||||
@@ -1,47 +1,32 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import useSWR from "swr";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import MarkdownRender from "components/utility/MarkdownRender";
|
||||
import fetcher from "utils/swrFetch";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
data: drive_v3.Schema$File;
|
||||
};
|
||||
|
||||
export default function TextPreview({ data }: Props) {
|
||||
const fileId = createFileId(data);
|
||||
const {
|
||||
data: swrData,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
});
|
||||
} = useSWR(`/api/files/${fileId}?download=1`);
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading code preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : error ? (
|
||||
<ErrorFeedback
|
||||
message={error.message || "Failed to load code"}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<SWRLayout
|
||||
data={swrData}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<div className={"w-full"}>
|
||||
<MarkdownRender content={swrData as string} />
|
||||
</div>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,22 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import ReactPlayer from "react-player";
|
||||
import { useState } from "react";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
data: drive_v3.Schema$File;
|
||||
};
|
||||
|
||||
export default function VideoPreview({ data }: Props) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const fileId = createFileId(data);
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{isLoading ? (
|
||||
<LoadingFeedback
|
||||
message={"Loading video preview..."}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : isError ? (
|
||||
<ErrorFeedback
|
||||
message={errorMessage}
|
||||
useContainer={false}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
isLoading || isError ? "hidden" : "block"
|
||||
} aspect-video h-full max-h-[70vh] w-full`}
|
||||
>
|
||||
<div className={`aspect-video h-full max-h-[70vh] w-full`}>
|
||||
<ReactPlayer
|
||||
url={`/download/${data.id}/${data.name}`}
|
||||
url={`/api/files/${fileId}?download=1`}
|
||||
controls={true}
|
||||
width='100%'
|
||||
height='100%'
|
||||
onReady={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onError={(error) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(error.message);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,14 @@ import { BreadCrumbsResponse } from "types/googleapis";
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
fileId: string;
|
||||
renderSwitchLayout?: boolean;
|
||||
};
|
||||
|
||||
export default function DefaultLayout({ children, fileId }: Props) {
|
||||
export default function DefaultLayout({
|
||||
children,
|
||||
fileId,
|
||||
renderSwitchLayout = true,
|
||||
}: Props) {
|
||||
const { data, isLoading } = useSWR<BreadCrumbsResponse>(
|
||||
`/api/files/${fileId}/getPath`,
|
||||
fetcher,
|
||||
@@ -18,12 +23,12 @@ export default function DefaultLayout({ children, fileId }: Props) {
|
||||
|
||||
return (
|
||||
<div className={"mx-auto flex max-w-screen-xl flex-col gap-2"}>
|
||||
<div className={"flex items-center justify-between"}>
|
||||
<div className={"flex w-full items-center justify-between gap-2"}>
|
||||
<Breadcrumb
|
||||
data={fileId === "root" ? undefined : data}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<SwitchLayout />
|
||||
{renderSwitchLayout && <SwitchLayout />}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { MdCopyAll, MdDownload } from "react-icons/md";
|
||||
import Link from "next/link";
|
||||
import config from "config/site.config";
|
||||
import { useEffect, useState } from "react";
|
||||
import useCopyText from "hooks/useCopyText";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
export default function DetailsButtons({ data, hash }: Props) {
|
||||
const [downloadURL, setDownloadURL] = useState<string>("");
|
||||
const [viewURL, setViewURL] = useState<string>("");
|
||||
const copyText = useCopyText();
|
||||
|
||||
useEffect(() => {
|
||||
setDownloadURL(`/download/${data.id}/${data.name}`);
|
||||
setViewURL(`${window.location.host}/media/${data.id}/${data.name}`);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<Link
|
||||
href={downloadURL}
|
||||
target={"_blank"}
|
||||
rel={"noopener noreferrer"}
|
||||
>
|
||||
<button
|
||||
className={
|
||||
"primary flex w-full items-center justify-center gap-2 py-4 tablet:py-2"
|
||||
}
|
||||
>
|
||||
<MdDownload />
|
||||
Download file
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className={`flex w-full items-center justify-center gap-2 py-4 tablet:py-2 ${
|
||||
config.files.allowDownloadProtectedWithoutAccess
|
||||
? "secondary"
|
||||
: "danger"
|
||||
}`}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
copyText(viewURL);
|
||||
}}
|
||||
>
|
||||
<MdCopyAll />
|
||||
Copy direct link
|
||||
</button>
|
||||
{hash && !config.files.allowDownloadProtectedWithoutAccess && (
|
||||
<div className={"banner warning text-sm"}>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<div className={"font-bold"}>
|
||||
Copying the direct link will also copy the access token.
|
||||
</div>
|
||||
<p className={"text-sm"}>
|
||||
Only share the direct link with people you trust.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { TFile } from "types/googleapis";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import { formatBytes, formatDate, formatDuration } from "utils/formatHelper";
|
||||
import { useEffect, useState } from "react";
|
||||
import DetailsButtons from "components/layout/FileDetails/DetailsButtons";
|
||||
import { getFilePreview } from "utils/mimeTypesHelper";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
export default function FileDetails({ data, hash }: Props) {
|
||||
const [metadata, setMetadata] = useState<{ label: string; value: string }[]>(
|
||||
[],
|
||||
);
|
||||
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const Preview = getFilePreview(
|
||||
data.fileExtension as string,
|
||||
data.mimeType as string,
|
||||
);
|
||||
setPreviewComponent(
|
||||
<Preview
|
||||
data={data}
|
||||
hash={hash || ""}
|
||||
/>,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const _metadata = [
|
||||
{
|
||||
label: "File name",
|
||||
value: (data.name as string).split(
|
||||
`.${data.fileExtension as string}`,
|
||||
)[0],
|
||||
},
|
||||
{ label: "Size", value: formatBytes(data.size as string) },
|
||||
{ label: "Type", value: data.mimeType as string },
|
||||
{
|
||||
label: "Created",
|
||||
value: formatDate(new Date(data.createdTime as string)),
|
||||
},
|
||||
{
|
||||
label: "Modified",
|
||||
value: formatDate(new Date(data.modifiedTime as string)),
|
||||
},
|
||||
];
|
||||
// Insert at index 3
|
||||
if (
|
||||
data.imageMediaMetadata &&
|
||||
!(data.mimeType as string).endsWith("svg+xml")
|
||||
) {
|
||||
const { width, height } = data.imageMediaMetadata;
|
||||
_metadata.splice(3, 0, {
|
||||
label: "Dimension",
|
||||
value: `${width || 0}px x ${height || 0}px`,
|
||||
});
|
||||
}
|
||||
if (data.videoMediaMetadata) {
|
||||
const { width, height, durationMillis } = data.videoMediaMetadata;
|
||||
_metadata.splice(3, 0, {
|
||||
label: "Dimension",
|
||||
value: `${width || 0}px x ${height || 0}px`,
|
||||
});
|
||||
_metadata.splice(4, 0, {
|
||||
label: "Duration",
|
||||
value: formatDuration(durationMillis as string),
|
||||
});
|
||||
}
|
||||
setMetadata(_metadata);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data ? (
|
||||
<LoadingFeedback message={"Loading file details..."} />
|
||||
) : (
|
||||
<div className={"grid grid-cols-1 gap-4 tablet:grid-cols-4"}>
|
||||
<div className={"card h-fit tablet:col-span-3"}>
|
||||
<div className='flex w-full items-center justify-between rounded-lg'>
|
||||
<span className='font-bold'>Preview</span>
|
||||
</div>
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
{PreviewComponent}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"col-span-1 flex h-fit flex-col gap-2 tablet:sticky tablet:top-16"
|
||||
}
|
||||
>
|
||||
<div className={"card"}>
|
||||
<div className='flex w-full items-center justify-between rounded-lg'>
|
||||
<span className='font-bold'>Details</span>
|
||||
</div>
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
<div className='flex w-full flex-col items-center justify-center gap-4 px-2'>
|
||||
{metadata.map((item, index) => (
|
||||
<div
|
||||
className='flex w-full flex-col justify-center'
|
||||
key={index}
|
||||
>
|
||||
<span className='font-bold text-inherit'>{item.label}</span>
|
||||
<span className='whitespace-pre-wrap break-words text-inherit'>
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"card"}>
|
||||
<DetailsButtons
|
||||
data={data}
|
||||
hash={hash || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,13 +21,18 @@ export default function Readme({
|
||||
}
|
||||
}, [isReadmeExist, isReadmeLoading, readmeData]);
|
||||
|
||||
if (!isMounted) return <></>;
|
||||
return (
|
||||
<div className={"card"}>
|
||||
{isReadmeLoading && <LoadingFeedback useContainer={false} />}
|
||||
{!isReadmeLoading && readmeData && (
|
||||
<MarkdownRender content={readmeData} />
|
||||
<>
|
||||
{isReadmeLoading && (
|
||||
<div className={"card"}>
|
||||
<LoadingFeedback />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isReadmeLoading && readmeData && isMounted && (
|
||||
<div className={"card"}>
|
||||
<MarkdownRender content={readmeData} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,9 +20,17 @@ export default function SWRLayout({ data, error, isLoading, children }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <LoadingFeedback />}
|
||||
{!isLoading && error && <ErrorFeedback />}
|
||||
{isMounted && <>{children}</>}
|
||||
{isLoading && (
|
||||
<div className={"card"}>
|
||||
<LoadingFeedback />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<div className={"card"}>
|
||||
<ErrorFeedback />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && isMounted && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function SwitchLayout() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"relative flex w-fit flex-col"}
|
||||
className={"relative flex w-fit flex-shrink-0 flex-grow-0 flex-col"}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
const newValue = JSON.parse(event.newValue);
|
||||
setStoredValue(newValue);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error("Error setting localStorage:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,28 +30,16 @@ export default initMiddleware(async function handler(
|
||||
responseTime: Date.now() - _start,
|
||||
};
|
||||
|
||||
const findFolder = await driveClient.files.list({
|
||||
q: `name = '${name}' and trashed = false and 'me' in owners`,
|
||||
fields: "files(id, name, mimeType)",
|
||||
const findBanner = await driveClient.files.list({
|
||||
q: `name contains '.banner' and trashed = false and 'me' in owners`,
|
||||
fields: "files(id, name, mimeType, parents)",
|
||||
});
|
||||
const folder = findFolder.data.files?.find(
|
||||
const banner = findBanner.data.files?.find(
|
||||
(file) =>
|
||||
file.name === decodeURIComponent(name) &&
|
||||
(file.id as string).startsWith(partialId),
|
||||
file.parents?.[0].startsWith(partialId) &&
|
||||
file.name?.startsWith(".banner"),
|
||||
);
|
||||
if (!folder) {
|
||||
throw new ExtendedError("Folder not found.", 404, "notFound");
|
||||
}
|
||||
|
||||
const listFiles = await driveClient.files.list({
|
||||
q: `'${folder.id}' in parents and trashed = false and 'me' in owners`,
|
||||
fields: "files(id, name, mimeType)",
|
||||
});
|
||||
|
||||
const banner = listFiles.data.files?.filter((file) =>
|
||||
file.name?.startsWith(".banner"),
|
||||
)[0];
|
||||
if (!banner || !banner.mimeType?.startsWith("image")) {
|
||||
if (!banner) {
|
||||
payload.success = false;
|
||||
return response.status(200).json(payload);
|
||||
}
|
||||
|
||||
@@ -12,27 +12,20 @@ export default initMiddleware(async function handler(
|
||||
const _start = Date.now();
|
||||
|
||||
try {
|
||||
const query: string[] = [
|
||||
"name contains '.banner'",
|
||||
`parents = '${apiConfig.files.rootFolder}'`,
|
||||
"trashed = false",
|
||||
"'me' in owners",
|
||||
];
|
||||
const getRootBanner = await driveClient.files.list({
|
||||
q: query.join(" and "),
|
||||
fields: "files(id, name, mimeType)",
|
||||
});
|
||||
|
||||
const payload: BannerResponse = {
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
responseTime: Date.now() - _start,
|
||||
};
|
||||
|
||||
const banner = getRootBanner.data.files?.filter((item) =>
|
||||
item.name?.startsWith(".banner"),
|
||||
)[0];
|
||||
if (!banner || !banner.mimeType?.startsWith("image")) {
|
||||
const findBanner = await driveClient.files.list({
|
||||
q: `name contains '.banner' and trashed = false and 'me' in owners and parents = '${apiConfig.files.rootFolder}'`,
|
||||
fields: "files(id, name, mimeType, parents)",
|
||||
});
|
||||
const banner = findBanner.data.files?.find((file) =>
|
||||
file.name?.startsWith(".banner"),
|
||||
);
|
||||
if (!banner) {
|
||||
payload.success = false;
|
||||
return response.status(200).json(payload);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default initMiddleware(async function handler(
|
||||
|
||||
let tempParent: string[] = file.parents || [];
|
||||
while (tempParent.length > 0) {
|
||||
if (breadcrumbs.length === apiConfig.files.breadcrumbDepth) {
|
||||
if (breadcrumbs.length > apiConfig.files.breadcrumbDepth) {
|
||||
isLimitReached = true;
|
||||
break;
|
||||
}
|
||||
@@ -99,7 +99,10 @@ export default initMiddleware(async function handler(
|
||||
success: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
responseTime: Date.now() - _start,
|
||||
breadcrumbs,
|
||||
breadcrumbs:
|
||||
breadcrumbs.length > apiConfig.files.breadcrumbDepth
|
||||
? breadcrumbs.slice(0, apiConfig.files.breadcrumbDepth)
|
||||
: breadcrumbs,
|
||||
isLimitReached,
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -66,8 +66,6 @@ export default initMiddleware(async function handler(
|
||||
{ responseType: "stream" },
|
||||
);
|
||||
|
||||
console.log(file);
|
||||
|
||||
response.setHeader(
|
||||
"Content-Type",
|
||||
file.mimeType || "application/octet-stream",
|
||||
|
||||
+36
-18
@@ -32,29 +32,47 @@ export default async function handler(request: NextRequest) {
|
||||
|
||||
let fileImage;
|
||||
if (fileId && isImage) {
|
||||
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`,
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.headers.get("content-type")?.startsWith("image")) {
|
||||
return { success: true };
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.success) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new Error(err);
|
||||
});
|
||||
} else if (fileId && !isImage) {
|
||||
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`;
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`,
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.headers.get("content-type")?.startsWith("image")) {
|
||||
return { success: true };
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.success) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new Error(err);
|
||||
});
|
||||
} else {
|
||||
throw new Error("Default image");
|
||||
}
|
||||
|
||||
await fetch(fileImage)
|
||||
.then((res) => {
|
||||
if (res.headers.get("content-type")?.startsWith("image")) {
|
||||
return { success: true };
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.success) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new Error(err);
|
||||
});
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ErrorResponse } from "types/googleapis";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import initMiddleware from "utils/apiMiddleware";
|
||||
import { ExtendedError } from "utils/driveHelper";
|
||||
import driveClient from "utils/driveClient";
|
||||
|
||||
export default initMiddleware(async function handler(
|
||||
request: NextApiRequest,
|
||||
response: NextApiResponse,
|
||||
) {
|
||||
const _start = Date.now();
|
||||
|
||||
try {
|
||||
const { folderId } = request.query;
|
||||
|
||||
const [name, partialId] = (folderId as string).split(":");
|
||||
|
||||
if (!name || !partialId || partialId.length !== 8) {
|
||||
throw new ExtendedError(
|
||||
"Can't resolve name and id provided.",
|
||||
400,
|
||||
"invalidId",
|
||||
);
|
||||
}
|
||||
|
||||
const findReadme = await driveClient.files.list({
|
||||
q: `name = '.readme.md' and trashed = false and 'me' in owners`,
|
||||
fields: "files(id, name, mimeType, parents)",
|
||||
});
|
||||
const readme = findReadme.data.files?.find((file) =>
|
||||
file.parents?.[0].startsWith(partialId),
|
||||
);
|
||||
if (!readme) {
|
||||
return response.status(200);
|
||||
}
|
||||
response.setHeader(
|
||||
"Content-Type",
|
||||
readme.mimeType || "application/octet-stream",
|
||||
);
|
||||
response.setHeader(
|
||||
"Content-Disposition",
|
||||
`inline; filename="${readme.name}"`,
|
||||
);
|
||||
response.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
|
||||
const readmeStream = await driveClient.files.get(
|
||||
{
|
||||
fileId: readme.id as string,
|
||||
alt: "media",
|
||||
},
|
||||
{ responseType: "text" },
|
||||
);
|
||||
|
||||
return response.status(200).send(readmeStream.data);
|
||||
} catch (error: any) {
|
||||
const payload: ErrorResponse = {
|
||||
success: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
responseTime: Date.now() - _start,
|
||||
code: error.code || 500,
|
||||
errors: {
|
||||
message: error.errors?.[0].message || error.message || "Unknown error",
|
||||
reason: error.errors?.[0].reason || error.cause || "internalError",
|
||||
},
|
||||
};
|
||||
|
||||
return response.status(payload.code || 500).json(payload);
|
||||
}
|
||||
});
|
||||
+193
-150
@@ -1,193 +1,236 @@
|
||||
import useSWR from "swr";
|
||||
import { ErrorResponse, FileResponse, TFileParent } from "types/googleapis";
|
||||
import Breadcrumb from "components/Breadcrumb";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import { useRouter } from "next/router";
|
||||
import FileDetails from "components/layout/FileDetails";
|
||||
import useLocalStorage from "hooks/useLocalStorage";
|
||||
import { GetServerSideProps } from "next";
|
||||
import axios from "axios";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Password from "components/layout/Password";
|
||||
import { ErrorResponse, FileResponse } from "types/googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { urlDecrypt } from "utils/encryptionHelper";
|
||||
import DefaultLayout from "components/layout/DefaultLayout";
|
||||
import { NextSeo } from "next-seo";
|
||||
import config from "config/site.config";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
import { getFilePreview, getFileType } from "utils/mimeTypesHelper";
|
||||
import { capitalize, formatBytes, formatDate } from "utils/formatHelper";
|
||||
import Link from "next/link";
|
||||
import useCopyText from "hooks/useCopyText";
|
||||
|
||||
type Props = {
|
||||
passwordParent?: string;
|
||||
fileName?: string;
|
||||
id: string;
|
||||
fileName: string;
|
||||
};
|
||||
export default function File({ passwordParent, fileName }: Props) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
type Metadata = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
export default function File({ id, fileName }: Props) {
|
||||
const [data, setData] = useState<FileResponse>();
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
|
||||
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element>();
|
||||
const [metadata, setMetadata] = useState<Metadata[]>([]);
|
||||
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
const [password, setPassword] = useState<{ [p: string]: string }>(
|
||||
passwordStorage,
|
||||
);
|
||||
const copyLink = useCopyText();
|
||||
|
||||
/**
|
||||
* ===========================
|
||||
* START - fetch file data
|
||||
* ===========================
|
||||
*/
|
||||
const {
|
||||
data: swrData,
|
||||
error,
|
||||
isLoading,
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWR<FileResponse, ErrorResponse>(
|
||||
`/api/files/${id}`,
|
||||
(url, headers) =>
|
||||
axios
|
||||
.get<FileResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
password?.[passwordParent as string] ||
|
||||
password?.[id as string] ||
|
||||
passwordStorage?.[passwordParent as string] ||
|
||||
passwordStorage?.[id as string] ||
|
||||
""
|
||||
}`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
} = useSWR<FileResponse, ErrorResponse>(`/api/files/${id}`);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalLoading(true);
|
||||
if (swrData) {
|
||||
const parentsArray: TFileParent[] | undefined = swrData.parents;
|
||||
parentsArray?.unshift({
|
||||
id: swrData.file.id as string,
|
||||
name: swrData.file.name as string,
|
||||
});
|
||||
const payload: FileResponse = {
|
||||
parents: parentsArray,
|
||||
const decryptedData: FileResponse = {
|
||||
...swrData,
|
||||
file: {
|
||||
...swrData.file,
|
||||
id: urlDecrypt(swrData.file.id as string),
|
||||
webContentLink: urlDecrypt(swrData.file.webContentLink as string),
|
||||
},
|
||||
};
|
||||
setData(payload);
|
||||
setGlobalLoading(false);
|
||||
setData(decryptedData);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [swrData, error, isLoading, isValidating, password]);
|
||||
}, [swrData]);
|
||||
/**
|
||||
* ===========================
|
||||
* END - fetch file data
|
||||
* ===========================
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isValidating) {
|
||||
setGlobalLoading(false);
|
||||
} else {
|
||||
setGlobalLoading(true);
|
||||
if (data) {
|
||||
const Preview = getFilePreview(
|
||||
data.file.fileExtension as string,
|
||||
data.file.mimeType as string,
|
||||
);
|
||||
setPreviewComponent(<Preview data={data.file} />);
|
||||
|
||||
const defaultMetadata: Metadata[] = [
|
||||
{
|
||||
label: "Name",
|
||||
value: data.file.name as string,
|
||||
},
|
||||
{
|
||||
label: "Type",
|
||||
value: capitalize(
|
||||
getFileType(
|
||||
data.file.fileExtension as string,
|
||||
data.file.mimeType as string,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Size",
|
||||
value: formatBytes(data.file.size as string),
|
||||
},
|
||||
{
|
||||
label: "Created",
|
||||
value: formatDate(new Date(data.file.createdTime as string)),
|
||||
},
|
||||
{
|
||||
label: "Modified",
|
||||
value: formatDate(new Date(data.file.modifiedTime as string)),
|
||||
},
|
||||
];
|
||||
if (data.file.imageMediaMetadata) {
|
||||
defaultMetadata.push({
|
||||
label: "Dimensions",
|
||||
value: `${data.file.imageMediaMetadata.width} x ${data.file.imageMediaMetadata.height}`,
|
||||
});
|
||||
}
|
||||
if (data.file.videoMediaMetadata) {
|
||||
defaultMetadata.push({
|
||||
label: "Duration",
|
||||
value: `${data.file.videoMediaMetadata.durationMillis} ms`,
|
||||
});
|
||||
}
|
||||
setMetadata(defaultMetadata);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading, isValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
}).then((r) => {
|
||||
return r;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
|
||||
const inputPassCallback = useCallback(
|
||||
(data: { [p: string]: string }) => {
|
||||
setGlobalLoading(true);
|
||||
setPasswordStorage(data);
|
||||
setPassword(data);
|
||||
},
|
||||
[setPasswordStorage],
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<DefaultLayout
|
||||
fileId={id}
|
||||
renderSwitchLayout={false}
|
||||
>
|
||||
<NextSeo
|
||||
title={fileName || "File preview"}
|
||||
title={`Viewing ${fileName.split(".").slice(0, -1).join(".")}`}
|
||||
openGraph={{
|
||||
type: "website",
|
||||
title: `${(id as string).split(":")[0]} @${config.siteName}`,
|
||||
description: config.siteDescription,
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/file/${id}`,
|
||||
title: fileName.split(".").slice(0, -1).join("."),
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/file/${encodeURIComponent(
|
||||
id,
|
||||
)}`,
|
||||
images: [
|
||||
{
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/api/og?fileId=${id}`,
|
||||
url: `${
|
||||
process.env.NEXT_PUBLIC_DOMAIN
|
||||
}/api/og?fileId=${encodeURIComponent(id)}`,
|
||||
alt: fileName,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: config.siteName,
|
||||
},
|
||||
],
|
||||
siteName: config.siteName,
|
||||
}}
|
||||
/>
|
||||
<SWRLayout
|
||||
data={data}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"relative grid grid-cols-1 gap-2 tablet:grid-cols-4 tablet:gap-4"
|
||||
}
|
||||
>
|
||||
{/* File Preview */}
|
||||
<div className={"card h-fit tablet:col-span-3"}>
|
||||
<div className='flex w-full items-center justify-between rounded-lg'>
|
||||
<span className='font-bold'>Preview</span>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={globalLoading}
|
||||
/>
|
||||
</div>
|
||||
{globalLoading && <LoadingFeedback message={"Loading file details..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message} />
|
||||
)}
|
||||
{!globalLoading && !error && data && (
|
||||
<>
|
||||
{data.passwordRequired && !data.passwordValidated && (
|
||||
<Password
|
||||
folderId={(passwordParent as string) || (id as string)}
|
||||
inputCallback={inputPassCallback}
|
||||
/>
|
||||
)}
|
||||
{(data.passwordValidated || !data.passwordRequired) && (
|
||||
<>
|
||||
<FileDetails
|
||||
data={data.file}
|
||||
hash={passwordStorage?.[passwordParent as string] || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
{PreviewComponent}
|
||||
</div>
|
||||
|
||||
{/* Details and download */}
|
||||
<div
|
||||
className={
|
||||
"sticky top-16 flex h-fit flex-col gap-2 max-tablet:flex-col-reverse tablet:col-span-1 tablet:gap-4"
|
||||
}
|
||||
>
|
||||
<div className={"card"}>
|
||||
<div className='flex w-full items-center justify-between rounded-lg'>
|
||||
<span className='font-bold'>Details</span>
|
||||
</div>
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
{metadata.map((item, index) => (
|
||||
<div
|
||||
key={`fileDetails-${index}`}
|
||||
className={"mb-2 flex w-full flex-col justify-center"}
|
||||
>
|
||||
<span className={"font-bold text-inherit"}>{item.label}</span>
|
||||
<span
|
||||
className={"whitespace-pre-wrap break-words text-inherit"}
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={"card"}>
|
||||
<div className='flex w-full items-center justify-between rounded-lg'>
|
||||
<span className='font-bold'>Download</span>
|
||||
</div>
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
<div className={"flex w-full flex-col justify-center gap-2"}>
|
||||
<Link
|
||||
href={`/api/files/${id}?download=1`}
|
||||
className={"w-full"}
|
||||
target={"_blank"}
|
||||
rel={"noopener noreferrer"}
|
||||
>
|
||||
<button className={"primary w-full"}>Download</button>
|
||||
</Link>
|
||||
<button
|
||||
className={"secondary"}
|
||||
onClick={() => {
|
||||
copyLink(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}?download=1`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy direct link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SWRLayout>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const { id } = context.query;
|
||||
const data = await axios.get(
|
||||
|
||||
const fetchFileMetadata = await axios.get<FileResponse>(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
|
||||
);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (data) {
|
||||
if (!fetchFileMetadata.data.success) {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: data.data.protectedId || null,
|
||||
fileName: data.data.file.name,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: "",
|
||||
fileName: "",
|
||||
},
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
id,
|
||||
fileName: fetchFileMetadata.data.file.name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+148
-219
@@ -1,46 +1,38 @@
|
||||
import useSWR from "swr";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import DefaultLayout from "components/layout/DefaultLayout";
|
||||
import { LayoutContext, TLayoutContext } from "context/layoutContext";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import fetcher, { buildNextKey } from "utils/swrFetch";
|
||||
import { ErrorResponse, FilesResponse, TFile } from "types/googleapis";
|
||||
import Breadcrumb from "components/Breadcrumb";
|
||||
import { buildNextKey } from "utils/swrFetch";
|
||||
import axios, { AxiosHeaders } from "axios";
|
||||
import { BannerResponse, ErrorResponse, FilesResponse } from "types/googleapis";
|
||||
import SWRLayout from "components/layout/SWRLayout";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import MarkdownRender from "components/utility/MarkdownRender";
|
||||
import config from "config/site.config";
|
||||
import siteConfig from "config/site.config";
|
||||
import Readme from "components/layout/Readme";
|
||||
import GridLayout from "components/layout/Files/GridLayout";
|
||||
import useLocalStorage from "hooks/useLocalStorage";
|
||||
import SwitchLayout from "components/utility/SwitchLayout";
|
||||
import ListLayout from "components/layout/Files/ListLayout";
|
||||
import LoadingFeedback from "components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "components/APIFeedback/Error";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import Password from "components/layout/Password";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
type Props = {
|
||||
passwordParent?: string;
|
||||
folderName?: string;
|
||||
id: string;
|
||||
folderName: string;
|
||||
bannerFileId?: string;
|
||||
};
|
||||
export default function Folder({ passwordParent, folderName }: Props) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
export default function Home({ id, folderName, bannerFileId }: Props) {
|
||||
const { layout } = useContext<TLayoutContext>(LayoutContext);
|
||||
const [data, setData] = useState<FilesResponse>();
|
||||
|
||||
const [isReadmeExists, setIsReadmeExists] = useState<boolean>(false);
|
||||
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
|
||||
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
|
||||
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
|
||||
const [password, setPassword] = useState<{ [p: string]: string }>(
|
||||
passwordStorage,
|
||||
);
|
||||
const [isReadmeLoading, setIsReadmeLoading] = useState<boolean>(false);
|
||||
const [readmeData, setReadmeData] = useState<string>();
|
||||
|
||||
/**
|
||||
* ===========================
|
||||
* START - fetch file data
|
||||
* ===========================
|
||||
* **/
|
||||
const getNextKey = buildNextKey(`/api/files/${id}`);
|
||||
const {
|
||||
data: swrData,
|
||||
@@ -48,231 +40,168 @@ export default function Folder({ passwordParent, folderName }: Props) {
|
||||
isLoading,
|
||||
size,
|
||||
setSize,
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWRInfinite<FilesResponse, ErrorResponse>(
|
||||
getNextKey,
|
||||
(url, headers) =>
|
||||
(url: string, headers: AxiosHeaders) =>
|
||||
axios
|
||||
.get<FilesResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
password?.[passwordParent as string] ||
|
||||
password?.[id as string] ||
|
||||
passwordStorage?.[passwordParent as string] ||
|
||||
passwordStorage?.[id as string] ||
|
||||
""
|
||||
}`,
|
||||
Authorization: `Bearer TODO:ADD`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: readmeData,
|
||||
error: readmeError,
|
||||
isLoading: readmeLoading,
|
||||
} = useSWR(`/api/readme/${id}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
const isLoadingInitialData = !swrData && !error;
|
||||
const isLoadingMore =
|
||||
isLoadingInitialData ||
|
||||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined");
|
||||
const isEmpty =
|
||||
swrData?.[0]?.files?.length === 0 && swrData?.[0]?.folders?.length === 0;
|
||||
const isReachingEnd =
|
||||
isEmpty ||
|
||||
(swrData &&
|
||||
typeof swrData[swrData.length - 1]?.nextPageToken === "undefined");
|
||||
const filePagination = {
|
||||
isLoadingInitialData: !swrData && !error,
|
||||
isLoadingMore:
|
||||
(!swrData && !error) ||
|
||||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined"),
|
||||
isEmpty: swrData?.[0]?.files?.length === 0,
|
||||
isReachingEnd:
|
||||
swrData && swrData[swrData.length - 1]?.nextPageToken === undefined,
|
||||
};
|
||||
|
||||
// Since SWRInfinite returning array of response, we need to flatten it.
|
||||
useEffect(() => {
|
||||
setGlobalLoading(true);
|
||||
const files: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
swrData?.flatMap((item: FilesResponse) => item.files);
|
||||
const folders: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
swrData?.flatMap((item: FilesResponse) => item.folders);
|
||||
const newData: FilesResponse = {
|
||||
...(swrData?.[size - 1] as FilesResponse),
|
||||
files: (files as drive_v3.Schema$File[]) || [],
|
||||
folders: (folders as drive_v3.Schema$File[]) || [],
|
||||
};
|
||||
setData(newData);
|
||||
if (newData.isReadmeExists) setIsReadmeExists(true);
|
||||
setGlobalLoading(false);
|
||||
if (swrData) {
|
||||
// Flatten files and folders, in case someone have more folders too.
|
||||
const files: drive_v3.Schema$File[] = swrData.flatMap(
|
||||
(item) => item.files,
|
||||
);
|
||||
const folders: drive_v3.Schema$File[] = swrData.flatMap(
|
||||
(item) => item.folders,
|
||||
);
|
||||
const flattenData: FilesResponse = {
|
||||
...swrData[size - 1],
|
||||
files,
|
||||
folders,
|
||||
};
|
||||
setData(flattenData);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [swrData, error, isLoading, size, isValidating, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isValidating) {
|
||||
setGlobalLoading(false);
|
||||
} else {
|
||||
setGlobalLoading(true);
|
||||
if (flattenData.isReadmeExists) {
|
||||
setIsReadmeExists(true);
|
||||
setIsReadmeLoading(true);
|
||||
axios
|
||||
.get<string>(`/api/readme/${id}`)
|
||||
.then((res) => {
|
||||
setReadmeData(res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsReadmeLoading(false);
|
||||
});
|
||||
} else {
|
||||
setIsReadmeExists(false);
|
||||
setReadmeData("");
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading, isValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
|
||||
const inputPassCallback = useCallback(
|
||||
(data: { [p: string]: string }) => {
|
||||
setGlobalLoading(true);
|
||||
setPasswordStorage(data);
|
||||
setPassword(data);
|
||||
},
|
||||
[setPasswordStorage],
|
||||
);
|
||||
}, [swrData, size, id]);
|
||||
/**
|
||||
* ===========================
|
||||
* END - fetch file data
|
||||
* ===========================
|
||||
* **/
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<DefaultLayout fileId={id}>
|
||||
<NextSeo
|
||||
title={folderName || "Folder"}
|
||||
title={folderName}
|
||||
openGraph={{
|
||||
type: "website",
|
||||
title: `${(id as string).split(":")[0]} @${config.siteName}`,
|
||||
description: config.siteDescription,
|
||||
title: folderName,
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/folder/${id}`,
|
||||
images: [
|
||||
{
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/api/og-folder?fileId=${id}`,
|
||||
url: `${
|
||||
process.env.NEXT_PUBLIC_DOMAIN
|
||||
}/api/og?fileId=${encodeURIComponent(bannerFileId as string)}`,
|
||||
alt: siteConfig.siteName,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: config.siteName,
|
||||
},
|
||||
],
|
||||
siteName: config.siteName,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={globalLoading}
|
||||
/>
|
||||
<SwitchLayout setLayoutStyle={setLayoutStyle} />
|
||||
</div>
|
||||
|
||||
{globalLoading && <LoadingFeedback message={"Loading file..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message} />
|
||||
)}
|
||||
{!globalLoading && !error && data && (
|
||||
<>
|
||||
{data.passwordRequired && !data.passwordValidated && (
|
||||
<Password
|
||||
folderId={(passwordParent as string) || (id as string)}
|
||||
inputCallback={inputPassCallback}
|
||||
<SWRLayout
|
||||
data={data}
|
||||
error={error}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{siteConfig.readme.position === "start" && (
|
||||
<Readme
|
||||
isReadmeExist={isReadmeExists}
|
||||
isReadmeLoading={isReadmeLoading}
|
||||
readmeData={readmeData}
|
||||
/>
|
||||
)}
|
||||
<div className={"card"}>
|
||||
{layout === "grid" && (
|
||||
<GridLayout
|
||||
data={data}
|
||||
pagination={{
|
||||
swrData,
|
||||
size,
|
||||
setSize,
|
||||
isLoadingMore: filePagination.isLoadingMore,
|
||||
isReachingEnd: filePagination.isReachingEnd,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(data.passwordValidated || !data.passwordRequired) && (
|
||||
<>
|
||||
{isReadmeExists && config.readme.position === "start" && (
|
||||
<div className='card w-full'>
|
||||
{readmeLoading && (
|
||||
<LoadingFeedback message={"Loading readme..."} />
|
||||
)}
|
||||
{readmeError && !readmeLoading && (
|
||||
<ErrorFeedback message={readmeError.errors?.message} />
|
||||
)}
|
||||
{readmeData && !readmeLoading && (
|
||||
<MarkdownRender content={readmeData as string} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={"card"}>
|
||||
{layoutStyle === "list" && (
|
||||
<ListLayout
|
||||
data={data}
|
||||
pagination={{
|
||||
swrData,
|
||||
isLoadingMore,
|
||||
isReachingEnd,
|
||||
size,
|
||||
setSize,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{layoutStyle === "grid" && (
|
||||
<GridLayout
|
||||
data={data}
|
||||
pagination={{
|
||||
swrData,
|
||||
isLoadingMore,
|
||||
isReachingEnd,
|
||||
size,
|
||||
setSize,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isReadmeExists && config.readme.position === "end" && (
|
||||
<div className='card w-full'>
|
||||
{readmeLoading && (
|
||||
<LoadingFeedback message={"Loading readme..."} />
|
||||
)}
|
||||
{readmeError && !readmeLoading && (
|
||||
<ErrorFeedback message={readmeError.errors?.message} />
|
||||
)}
|
||||
{readmeData && !readmeLoading && (
|
||||
<MarkdownRender content={readmeData as string} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{layout === "list" && (
|
||||
<ListLayout
|
||||
data={data}
|
||||
pagination={{
|
||||
swrData,
|
||||
size,
|
||||
setSize,
|
||||
isLoadingMore: filePagination.isLoadingMore,
|
||||
isReachingEnd: filePagination.isReachingEnd,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{siteConfig.readme.position === "end" && (
|
||||
<Readme
|
||||
isReadmeExist={isReadmeExists}
|
||||
isReadmeLoading={isReadmeLoading}
|
||||
readmeData={readmeData}
|
||||
/>
|
||||
)}
|
||||
</SWRLayout>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const { id } = context.query;
|
||||
const data = await axios.get(
|
||||
|
||||
const fetchFolder = await axios.get<FilesResponse>(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
|
||||
);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (data) {
|
||||
if (!fetchFolder.data.success) {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: data.data.protectedId || null,
|
||||
folderName: data.data.parents?.[0]?.name || null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: "",
|
||||
folderName: "",
|
||||
},
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
const folderName = decodeURIComponent((id as string).split(":")[0]);
|
||||
|
||||
const fetchBanner = await axios.get<BannerResponse>(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/banner/${id}`,
|
||||
);
|
||||
|
||||
if (!fetchBanner.data.banner) {
|
||||
return {
|
||||
props: { id, folderName },
|
||||
};
|
||||
}
|
||||
|
||||
const bannerFileId = createFileId(fetchBanner.data.banner, true);
|
||||
return {
|
||||
props: {
|
||||
id,
|
||||
folderName,
|
||||
bannerFileId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ import GridLayout from "components/layout/Files/GridLayout";
|
||||
import ListLayout from "components/layout/Files/ListLayout";
|
||||
import { createFileId } from "utils/driveHelper";
|
||||
import { NextSeo } from "next-seo";
|
||||
import { GetStaticProps } from "next";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
type Props = {
|
||||
bannerFileId?: string;
|
||||
@@ -165,7 +165,7 @@ export default function Home({ bannerFileId }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
const fetchBanner = await axios.get<BannerResponse>(
|
||||
`${process.env.NEXT_PUBLIC_DOMAIN}/api/banner`,
|
||||
);
|
||||
|
||||
@@ -254,10 +254,10 @@ html, body {
|
||||
}
|
||||
|
||||
div.divider-horizontal {
|
||||
@apply w-full h-px my-2 bg-zinc-400 dark:bg-zinc-500 tablet:my-4;
|
||||
@apply w-full h-px my-2 bg-zinc-400 dark:bg-zinc-500 tablet:my-2;
|
||||
}
|
||||
div.divider-vertical {
|
||||
@apply h-full w-px mx-2 bg-zinc-400 dark:bg-zinc-500 tablet:mx-4;
|
||||
@apply h-full w-px mx-2 bg-zinc-400 dark:bg-zinc-500 tablet:mx-2;
|
||||
}
|
||||
|
||||
.grid-auto-fit {
|
||||
|
||||
@@ -76,3 +76,7 @@ export function formatRelativeDate(
|
||||
style: "long",
|
||||
}).format(diff, unit);
|
||||
}
|
||||
|
||||
export function capitalize(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import mime from "mime-types";
|
||||
import { IconType } from "react-icons";
|
||||
import {
|
||||
BsBoxFill,
|
||||
@@ -33,10 +32,6 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
|
||||
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
|
||||
|
||||
function findMimeType(extension: string): string {
|
||||
return mime.lookup(extension) || "application/octet-stream";
|
||||
}
|
||||
|
||||
const type = {
|
||||
"3d": "3d", // model preview
|
||||
"audio": "audio", // audio preview
|
||||
@@ -95,12 +90,12 @@ const extToTypeMap: { [key: string]: string } = {
|
||||
"wav": type.audio,
|
||||
|
||||
// Archives
|
||||
"7z": type.default,
|
||||
"bz2": type.default,
|
||||
"gz": type.default,
|
||||
"rar": type.default,
|
||||
"tar": type.default,
|
||||
"zip": type.default,
|
||||
"7z": type.archive,
|
||||
"bz2": type.archive,
|
||||
"gz": type.archive,
|
||||
"rar": type.archive,
|
||||
"tar": type.archive,
|
||||
"zip": type.archive,
|
||||
|
||||
// Rich text
|
||||
"md": type.rich_text,
|
||||
@@ -255,6 +250,9 @@ export function getFileType(extension: string, mimeType?: string): string {
|
||||
if (type === "video") {
|
||||
return "video";
|
||||
}
|
||||
if (type === "image") {
|
||||
return "image";
|
||||
}
|
||||
}
|
||||
|
||||
return extToTypeMap[extension] || type.default;
|
||||
|
||||
Reference in New Issue
Block a user