diff --git a/package.json b/package.json index b25bd8f..06ccf5e 100644 --- a/package.json +++ b/package.json @@ -46,14 +46,15 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.0.2", + "fflate": "^0.8.2", "googleapis": "^118.0.0", "input-otp": "^1.2.3", "jsonwebtoken": "^9.0.0", - "jszip": "^3.10.1", "lucide-react": "^0.363.0", "next": "^14.1.4", "next-themes": "^0.3.0", "nextjs-toploader": "^1.6.11", + "plyr-react": "^5.3.0", "react": "^18", "react-colorful": "^5.6.1", "react-day-picker": "^8.10.0", diff --git a/src/app/@file.grid.tsx b/src/app/@file.grid.tsx index 236a60d..ac5d9ba 100644 --- a/src/app/@file.grid.tsx +++ b/src/app/@file.grid.tsx @@ -51,6 +51,9 @@ export default function FileGrid({ data }: Props) { return new URL(path, config.basePath).pathname; }, [data, pathname]); + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const [actionOpen, setActionOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -220,13 +223,23 @@ export default function FileGrid({ data }: Props) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} className='rounded-top-[var(--radius)] absolute -z-0 h-32 w-full flex-shrink-0 flex-grow-0 object-cover opacity-50' /> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} className='relative z-0 h-32 w-full flex-shrink-0 flex-grow-0 object-contain backdrop-blur' /> diff --git a/src/app/@file.list.tsx b/src/app/@file.list.tsx index 87041dd..0513e51 100644 --- a/src/app/@file.list.tsx +++ b/src/app/@file.list.tsx @@ -29,6 +29,7 @@ import { import useMediaQuery from "~/hooks/useMediaQuery"; import bytesToReadable from "~/utils/bytesFormat"; +import { durationToReadable } from "~/utils/durationFormat"; import { getPreviewIcon } from "~/utils/previewHelper"; import config from "~/config/gIndex.config"; @@ -42,14 +43,16 @@ export default function FileList({ data }: Props) { const pathname = usePathname(); const filePath = useMemo(() => { - // const currentPath = pathname.startsWith("/e") ? pathname : `/e${pathname}`; - // Set to pathname to remove the /e prefix const path = [pathname, encodeURIComponent(data.name)] .join("/") .replace(/\/+/g, "/"); return new URL(path, config.basePath).pathname; }, [data, pathname]); + + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const [actionOpen, setActionOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -218,9 +221,14 @@ export default function FileList({ data }: Props) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} + className='size-16 flex-shrink-0 flex-grow-0 rounded-[var(--radius)] object-cover tablet:size-20' /> {data.mimeType.startsWith("video") && ( @@ -230,6 +238,11 @@ export default function FileList({ data }: Props) { className='absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2 rounded-full bg-muted-foreground fill-muted p-1.5 text-muted opacity-75' size={24} /> +
+ {durationToReadable( + data.videoMediaMetadata?.durationMillis || 0, + )} +
)} @@ -240,13 +253,13 @@ export default function FileList({ data }: Props) { ? "Folder" : getPreviewIcon(data.fileExtension || "", data.mimeType) } - className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-12' + className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-20' /> )} {/* File data */} -
+
{config.siteConfig.showFileExtension ? data.name diff --git a/src/app/@header.button.tsx b/src/app/@header.button.tsx index c8e99f1..680dfc3 100644 --- a/src/app/@header.button.tsx +++ b/src/app/@header.button.tsx @@ -503,6 +503,9 @@ export default function HeaderButton({ children }: PropsWithChildren) { } function SearchResultItem({ data }: { data: z.infer }) { + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const router = useRouter(); return (
}) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} + className='size-16 flex-shrink-0 flex-grow-0 rounded-[var(--radius)] object-cover tablet:size-20' /> {data.mimeType.startsWith("video") && ( @@ -564,7 +572,7 @@ function SearchResultItem({ data }: { data: z.infer }) { ? "Folder" : getPreviewIcon(data.fileExtension || "", data.mimeType) } - className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-12' + className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-20' /> )}
diff --git a/src/app/@password.tsx b/src/app/@password.tsx index 7bc2cac..06e285f 100644 --- a/src/app/@password.tsx +++ b/src/app/@password.tsx @@ -105,14 +105,14 @@ export default function Password({ path, checkPaths, errorMessage }: Props) { className={cn("h-48 w-64 object-contain")} />
-

+

{path === "global" ? "This site are password protected" : "The folder or file you are trying to access is password protected"}

- + {/* Please enter the password to access the content - + */}
(() => { + const fileExtensionFallback = file.name.split(".").pop(); + const fileExt = file.fileExtension ?? fileExtensionFallback; + if (!fileExt) return false; + const fileType = getFileType(fileExt, file.mimeType); + return fileType === "document" || fileType === "pdf"; + }, [file]); + const fileInfo = useMemo<{ label: string; value: string }[]>(() => { const value = [ { @@ -154,6 +163,38 @@ export default function PreviewAction({ file }: Props) { setDownloading(false); } }; + const onOpenViewer = async (e: React.MouseEvent) => { + e.stopPropagation(); + toast.loading("Creating view token...", { + id: `view-${file.encryptedId}`, + }); + try { + const token = await CreateDownloadToken(); + if (!token) throw new Error("Failed to create view token"); + const streamURL = new URL( + `/api/stream/${file.encryptedId}`, + config.basePath, + ); + streamURL.searchParams.set("token", token); + toast.success("Opening viewer link...", { + id: `view-${file.encryptedId}`, + }); + + const timeout = setTimeout(() => { + clearTimeout(timeout); + const viewerUrl = new URL(`/gview`, "https://docs.google.com"); + viewerUrl.searchParams.set("url", streamURL.toString()); + viewerUrl.searchParams.set("embedded", "true"); + window.open(viewerUrl.toString(), "_blank"); + }, 150); + } catch (error) { + const e = error as Error; + console.error(e.message); + toast.error(e.message, { + id: `view-${file.encryptedId}`, + }); + } + }; return (
@@ -279,6 +320,20 @@ export default function PreviewAction({ file }: Props) { Raw Link ) : null} + {showViewDoc ? ( + + ) : null}
) : (
- { - console.error(e); - setError( - "Could not preview this audio, try downloading the file", - ); + options={{ + controls: [ + "play-large", + "play", + "progress", + "current-time", + "duration", + "mute", + "volume", + "settings", + "fullscreen", + ], + volume: 0.5, + muted: false, + loop: { + active: false, + }, + speed: { + selected: 1, + options: [0.5, 1, 1.5, 2], + }, + keyboard: { + focused: true, + global: false, + }, }} />
diff --git a/src/app/@preview.doc.tsx b/src/app/@preview.doc.tsx index 3b6380a..85c6ceb 100644 --- a/src/app/@preview.doc.tsx +++ b/src/app/@preview.doc.tsx @@ -1,22 +1,75 @@ "use client"; -import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer"; +import DocViewer, { DocRenderer } from "@cyntler/react-doc-viewer"; import { useEffect, useState } from "react"; import { z } from "zod"; import { Schema_File } from "~/schema"; import { cn } from "~/utils"; import Icon from "~/components/Icon"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; + +import config from "~/config/gIndex.config"; import { CreateDownloadToken } from "./actions"; +const GoogleDocsViewerRenderer: DocRenderer = ({ + mainState: { currentDocument, documentLoading }, +}) => { + if (!currentDocument || !currentDocument.uri) return null; + + const viewerUrl = new URL(`/gview`, "https://docs.google.com"); + viewerUrl.searchParams.set("url", currentDocument.uri); + viewerUrl.searchParams.set("embedded", "true"); + + if (documentLoading) { + return ( +
+ +

Loading document...

+
+ ); + } + + return ( +