diff --git a/package.json b/package.json
index b25bd8f..6822503 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"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/@preview.image.tsx b/src/app/@preview.image.tsx
index 033a882..88a9eeb 100644
--- a/src/app/@preview.image.tsx
+++ b/src/app/@preview.image.tsx
@@ -25,28 +25,7 @@ export default function PreviewImage({ file }: Props) {
return;
}
const token = await CreateDownloadToken();
- await fetch(`/api/download/${file.encryptedId}?token=${token}`)
- .then((res) => {
- if (!res.ok) throw new Error("Failed to fetch image");
- return res.blob();
- })
- .then((blob) => {
- const reader = new FileReader();
- reader.onload = () => {
- setImgSrc(reader.result as string);
- };
- reader.onerror = (e) => {
- console.error(e);
- setError(
- "Could not preview this image, try downloading the file",
- );
- };
- reader.readAsDataURL(blob);
- })
- .catch((e) => {
- console.error(e.message);
- setError(e.message);
- });
+ setImgSrc(`/api/download/${file.encryptedId}?token=${token}&media=1`);
} catch (error) {
const e = error as Error;
console.error(e);
@@ -87,6 +66,10 @@ export default function PreviewImage({ file }: Props) {
src={imgSrc}
alt={file.name}
className='max-h-[70dvh] w-full rounded-[var(--radius)] bg-muted object-contain object-center'
+ onError={(e) => {
+ console.error(e);
+ setError("Could not preview this image, try downloading the file");
+ }}
/>
)}
diff --git a/src/app/@preview.layout.tsx b/src/app/@preview.layout.tsx
index 287dfce..f34bf0d 100644
--- a/src/app/@preview.layout.tsx
+++ b/src/app/@preview.layout.tsx
@@ -26,12 +26,13 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
const [view, setView] = useState<"markdown" | "raw">("markdown");
return (
- <>
+
@@ -70,6 +71,6 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
- >
+
);
}
diff --git a/src/app/@preview.video.tsx b/src/app/@preview.video.tsx
index 7c4d8af..df6e10c 100644
--- a/src/app/@preview.video.tsx
+++ b/src/app/@preview.video.tsx
@@ -1,15 +1,21 @@
"use client";
+import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
-import ReactPlayer from "react-player";
import { z } from "zod";
import { Schema_File } from "~/schema";
import { cn } from "~/utils";
import Icon from "~/components/Icon";
+import { decryptData } from "~/utils/encryptionHelper/hash";
+
import { CreateDownloadToken } from "./actions";
+const Plyr = dynamic(() => import("plyr-react"), {
+ ssr: false,
+});
+
type Props = {
file: z.infer;
};
@@ -26,7 +32,11 @@ export default function PreviewVideo({ file }: Props) {
return;
}
const token = await CreateDownloadToken();
- setVideoSrc(`/api/download/${file.encryptedId}?token=${token}`);
+ const id = await decryptData(file.encryptedId);
+ // setVideoSrc(
+ // `https://drive.usercontent.google.com/download?id=${id}&export=download&authuser=0`,
+ // );
+ setVideoSrc(`/api/stream/${file.encryptedId}?token=${token}`);
} catch (error) {
const e = error as Error;
console.error(e);
@@ -63,30 +73,62 @@ export default function PreviewVideo({ file }: Props) {
{error}
) : (
- (
-
- {children}
-
- )}
- style={{
- width: "100%",
- height: "100%",
- maxHeight: "60vh",
- }}
- onError={(error) => {
- console.error(error.message);
- if (error instanceof Error) {
- setError(error.message);
- } else {
- setError("Failed to load video. (Probably not supported?)");
- }
- }}
- />
+
+ // (
+ //
+ // {children}
+ //
+ // )}
+ // style={{
+ // width: "100%",
+ // height: "100%",
+ // maxHeight: "60vh",
+ // }}
+ // onError={(error) => {
+ // console.error(error.message);
+ // if (error instanceof Error) {
+ // setError(error.message);
+ // } else {
+ // setError("Failed to load video. (Probably not supported?)");
+ // }
+ // }}
+ // />
)}
);
diff --git a/src/app/@rich-header.tsx b/src/app/@rich-header.tsx
index e3a2179..3c984f5 100644
--- a/src/app/@rich-header.tsx
+++ b/src/app/@rich-header.tsx
@@ -1,37 +1,51 @@
"use client";
import { Button } from "~/components/ui/button";
-import { CardHeader, CardTitle } from "~/components/ui/card";
+import { CardHeader } from "~/components/ui/card";
import { Separator } from "~/components/ui/separator";
+import { getFileType } from "~/utils/previewHelper";
+
type Props = {
title: string;
view: "markdown" | "raw";
onViewChange: (value: "markdown" | "raw") => void;
+ fileType: ReturnType | "unknown";
};
-export default function RichHeader({ title, view, onViewChange }: Props) {
+export default function RichHeader({
+ title,
+ view,
+ onViewChange,
+ fileType,
+}: Props) {
return (
-
-
{title}
-
-
-
-
+
+ {/*
*/}
+
+ {title}
+
+ {/* */}
+ {["markdown", "code", "text"].includes(fileType) && (
+
+
+
+
+ )}
diff --git a/src/app/api/stream/[encryptedId]/helper.ts b/src/app/api/stream/[encryptedId]/helper.ts
new file mode 100644
index 0000000..d12e947
--- /dev/null
+++ b/src/app/api/stream/[encryptedId]/helper.ts
@@ -0,0 +1,24 @@
+import { Readable } from "stream";
+
+async function* nodeStreamToIterator(stream: any) {
+ for await (const chunk of stream) {
+ yield chunk;
+ }
+}
+
+function iteratorToStream(iterator: AsyncGenerator
) {
+ return new ReadableStream({
+ async pull(controller) {
+ const { done, value } = await iterator.next();
+ if (done) {
+ controller.close();
+ } else {
+ controller.enqueue(value);
+ }
+ },
+ });
+}
+export function streamFile(body: Readable): ReadableStream {
+ const data: ReadableStream = iteratorToStream(nodeStreamToIterator(body));
+ return data;
+}
diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts
new file mode 100644
index 0000000..f720c12
--- /dev/null
+++ b/src/app/api/stream/[encryptedId]/route.ts
@@ -0,0 +1,101 @@
+import { NextRequest, NextResponse } from "next/server";
+
+import { CheckDownloadToken } from "~/app/actions";
+
+import { decryptData } from "~/utils/encryptionHelper/hash";
+import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance";
+
+import config from "~/config/gIndex.config";
+
+import { streamFile } from "./helper";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(
+ request: NextRequest,
+ {
+ params: { encryptedId },
+ }: {
+ params: {
+ encryptedId: string;
+ };
+ },
+) {
+ try {
+ const sp = new URL(request.nextUrl).searchParams;
+ const token = sp.get("token");
+ if (!token) throw new Error("Token not found");
+
+ // Only allow if referrer is from the same site
+ if (!request.headers.get("Referer")?.includes(config.basePath)) {
+ throw new Error("Invalid request");
+ }
+
+ const tokenValidity = await CheckDownloadToken(token);
+ if (!tokenValidity.success) throw new Error(tokenValidity.message);
+
+ const decryptedId = await decryptData(encryptedId);
+ const _fileMeta = gdrive.files.get({
+ fileId: decryptedId,
+ fields: "id, name, mimeType, size, fileExtension, webContentLink",
+ supportsAllDrives: config.apiConfig.isTeamDrive,
+ });
+ const _fileContent = gdrive.files.get(
+ {
+ fileId: decryptedId,
+ alt: "media",
+ supportsAllDrives: config.apiConfig.isTeamDrive,
+ },
+ {
+ responseType: "stream",
+ },
+ );
+
+ const [fileMeta, fileContent] = await Promise.all([
+ _fileMeta,
+ _fileContent,
+ ]);
+
+ const fileSize = Number(fileMeta.data.size || 0);
+ if (!fileMeta.data.webContentLink)
+ throw new Error("No download link found");
+
+ const stream: ReadableStream = streamFile(fileContent.data);
+ const ranges = request.headers.get("Range");
+ if (ranges) {
+ const [start, end] = ranges.replace(/bytes=/, "").split("-");
+ const startByte = parseInt(start, 10);
+ const endByte = end ? parseInt(end, 10) : fileSize - 1;
+ return new NextResponse(stream, {
+ status: 206,
+ headers: {
+ "Content-Type": fileMeta.data.mimeType || "application/octet-stream",
+ "Content-Disposition": `attachment; filename="${encodeURIComponent(
+ fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`,
+ )}"`,
+ "Content-Length": (endByte - startByte + 1).toString(),
+ "Accept-Ranges": "bytes",
+ "Content-Range": `bytes ${startByte}-${endByte}/${fileSize}`,
+ },
+ });
+ }
+
+ return new NextResponse(stream, {
+ status: 200,
+ headers: {
+ "Content-Type": fileMeta.data.mimeType || "application/octet-stream",
+ "Content-Disposition": `attachment; filename="${encodeURIComponent(
+ fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`,
+ )}"`,
+ "Content-Length": fileSize.toString(),
+ "Accept-Ranges": "bytes",
+ },
+ });
+ } catch (error) {
+ const e = error as Error;
+ console.error(e.message);
+ return new NextResponse(e.message, {
+ status: 500,
+ });
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 79c57fa..8050acd 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import { Metadata } from "next";
import { JetBrains_Mono, Outfit, Source_Sans_3 } from "next/font/google";
+import "plyr-react/plyr.css";
import { cn } from "~/utils";
import { formatFooter } from "~/utils/footerFormatter";
diff --git a/yarn.lock b/yarn.lock
index de412a0..eeb60d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2859,6 +2859,13 @@ __metadata:
languageName: node
linkType: hard
+"core-js@npm:^3.26.1":
+ version: 3.37.0
+ resolution: "core-js@npm:3.37.0"
+ checksum: 10c0/7e00331f346318ca3f595c08ce9e74ddae744715aef137486c1399163afd79792fb94c3161280863adfdc3e30f8026912d56bd3036f93cacfc689d33e185f2ee
+ languageName: node
+ linkType: hard
+
"core-util-is@npm:~1.0.0":
version: 1.0.3
resolution: "core-util-is@npm:1.0.3"
@@ -2918,6 +2925,13 @@ __metadata:
languageName: node
linkType: hard
+"custom-event-polyfill@npm:^1.0.7":
+ version: 1.0.7
+ resolution: "custom-event-polyfill@npm:1.0.7"
+ checksum: 10c0/b73c90d646d78f4acdff5453fa0f165f6d5506e32d074ca57d27f2bb7d9412356dab8cec7c00a0956b069e4987a8546a2c2c4b866d155e9fc4c27d223a225b78
+ languageName: node
+ linkType: hard
+
"damerau-levenshtein@npm:^1.0.8":
version: 1.0.8
resolution: "damerau-levenshtein@npm:1.0.8"
@@ -5181,6 +5195,13 @@ __metadata:
languageName: node
linkType: hard
+"loadjs@npm:^4.2.0":
+ version: 4.3.0
+ resolution: "loadjs@npm:4.3.0"
+ checksum: 10c0/8884520a7c5f3b0f6e4d3bc01d200c73b9c468986bea26acb54939d4a3f5da08f40f712812fadc1ff6030fca936e8c9eeb842aaafd287e32ca0ce6ae9f10e759
+ languageName: node
+ linkType: hard
+
"locate-path@npm:^6.0.0":
version: 6.0.0
resolution: "locate-path@npm:6.0.0"
@@ -6229,6 +6250,7 @@ __metadata:
next: "npm:^14.1.4"
next-themes: "npm:^0.3.0"
nextjs-toploader: "npm:^1.6.11"
+ plyr-react: "npm:^5.3.0"
postcss: "npm:^8.4.38"
prettier: "npm:3.0.0"
prettier-plugin-tailwindcss: "npm:0.5.12"
@@ -6750,6 +6772,37 @@ __metadata:
languageName: node
linkType: hard
+"plyr-react@npm:^5.3.0":
+ version: 5.3.0
+ resolution: "plyr-react@npm:5.3.0"
+ dependencies:
+ plyr: "npm:^3.7.7"
+ react-aptor: "npm:^2.0.0"
+ peerDependencies:
+ plyr: ^3.7.7
+ react: ">=16.8"
+ peerDependenciesMeta:
+ plyr:
+ optional: false
+ react:
+ optional: true
+ checksum: 10c0/b338c5f07277c124663aa4f820dffb0700a67ac6eab0015b77985bb70c308da96bdffff6d103033544de6516cade63027f4c077871a0fa0feba996dfd3b6f2c0
+ languageName: node
+ linkType: hard
+
+"plyr@npm:^3.7.7":
+ version: 3.7.8
+ resolution: "plyr@npm:3.7.8"
+ dependencies:
+ core-js: "npm:^3.26.1"
+ custom-event-polyfill: "npm:^1.0.7"
+ loadjs: "npm:^4.2.0"
+ rangetouch: "npm:^2.0.1"
+ url-polyfill: "npm:^1.1.12"
+ checksum: 10c0/75c3e070f7829f76409e0d34784bf8070b827ad99c4713a36338af7ce8dff7cf38998403f34a4a0b4d6e99efd1856ee056443c7e838db1dbb98ed38828110a97
+ languageName: node
+ linkType: hard
+
"possible-typed-array-names@npm:^1.0.0":
version: 1.0.0
resolution: "possible-typed-array-names@npm:1.0.0"
@@ -7001,6 +7054,25 @@ __metadata:
languageName: node
linkType: hard
+"rangetouch@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "rangetouch@npm:2.0.1"
+ checksum: 10c0/5f7947d1c5e95f50630ed1e0cbaeb4c32a6be37b66d864a68f7e70a4e86f782eb869df9fa2357b981b1301d2158eff50002a199b0fbbbf6e1746bc9d213914d9
+ languageName: node
+ linkType: hard
+
+"react-aptor@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "react-aptor@npm:2.0.0"
+ peerDependencies:
+ react: ">=16.8"
+ peerDependenciesMeta:
+ react:
+ optional: true
+ checksum: 10c0/f2f00b494a2cb93b0ee73949d2031c2f5a8233d169c3d16792e7c6a5177a75310155e85282209a6f74883b3564d1f3a46d5d4d03ee1c16224fbdb5d5665504f8
+ languageName: node
+ linkType: hard
+
"react-colorful@npm:^5.6.1":
version: 5.6.1
resolution: "react-colorful@npm:5.6.1"
@@ -8554,6 +8626,13 @@ __metadata:
languageName: node
linkType: hard
+"url-polyfill@npm:^1.1.12":
+ version: 1.1.12
+ resolution: "url-polyfill@npm:1.1.12"
+ checksum: 10c0/69633c42e3182271d01d2f2f4acd889a9f54b88fd7b2f45fd84fed19eca7cfa98fb6bfbd43d9cd9fad222a11a2dffb562b8435cdaa67fca5e25e3da638741679
+ languageName: node
+ linkType: hard
+
"url-template@npm:^2.0.8":
version: 2.0.8
resolution: "url-template@npm:2.0.8"