v2.4.2
This commit is contained in:
Arief Rachmawan
2025-03-10 22:39:56 +07:00
committed by GitHub
12 changed files with 220 additions and 191 deletions
BIN
View File
Binary file not shown.
+7 -2
View File
@@ -1,6 +1,6 @@
{
"name": "next-gdrive-index",
"version": "2.0.4",
"version": "2.4.2",
"private": true,
"scripts": {
"dev": "next dev -p 3000",
@@ -10,7 +10,8 @@
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\" --cache",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json}\" --cache"
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json}\" --cache",
"cli": "node ./scripts/cli.mjs"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
@@ -88,12 +89,16 @@
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"autoprefixer": "^10.4.19",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"encoding": "^0.1.13",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.1",
"ora": "^8.2.0",
"postcss": "^8.4.38",
"prettier": "3.0.0",
"prettier-plugin-tailwindcss": "0.5.12",
"prompts": "^2.4.2",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3"
}
+32
View File
@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { encryptionService } from "~/lib/utils.server";
import config from "~/config/gIndex.config";
export const dynamic = "force-dynamic";
export async function GET() {
try {
if (process.env.NODE_ENV !== "development") {
throw new Error("This route is only available in development environment");
}
const rootId = await encryptionService.decrypt(config.apiConfig.rootFolder);
const sharedDriveId = config.apiConfig.sharedDrive
? await encryptionService.decrypt(config.apiConfig.sharedDrive)
: undefined;
return NextResponse.json(
{
rootId,
sharedDriveId,
},
{ status: 200 },
);
} catch (error) {
const e = error as Error;
console.error(e);
return new Response(e.message, { status: 500 });
}
}
-111
View File
@@ -1,111 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import stream from "stream";
import { promisify } from "util";
import { gdrive } from "~/lib/utils.server";
const pipeline = promisify(stream.pipeline);
const fileId = "1KvMU9sy8fQKKmVKU5xf_u3Y2JZefe0kt";
export async function GET(request: NextRequest) {
try {
const start = Date.now();
console.log("Fetching metadata", Date.now() - start);
const meta = await gdrive.files.get({
fileId,
fields: "size,mimeType,name",
supportsAllDrives: true,
});
const { data: metadata } = meta;
console.log("Fetching file", Date.now() - start);
// const data = await gdrive.files.get(
// {
// fileId: fileId,
// alt: "media",
// acknowledgeAbuse: true,
// },
// {
// responseType: "stream",
// },
// );
console.log("Setting headers", Date.now() - start);
const headers = new Headers();
headers.set("Content-Type", metadata.mimeType ?? "application/octet-stream");
if (metadata.size) headers.set("Content-Length", metadata.size.toString());
headers.set("Content-Disposition", `attachment; filename="${metadata.name}"`);
// console.log("Streaming file", Date.now() - start);
// const contentStream = data.data;
// const webStream = new ReadableStream({
// async start(controller) {
// contentStream.on("data", (chunk) => {
// controller.enqueue(chunk);
// });
// contentStream.on("end", () => {
// controller.close();
// });
// contentStream.on("error", (err) => {
// console.error(err);
// controller.error(err);
// });
// },
// cancel() {
// contentStream.destroy();
// },
// });
// console.log("Returning response", Date.now() - start);
return new Response(
new ReadableStream({
async start(controller) {
try {
gdrive.files.get(
{
fileId: fileId,
alt: "media",
acknowledgeAbuse: true,
supportsAllDrives: true,
},
{
responseType: "stream",
},
(err, res) => {
if (err) throw err;
res?.data
.on("data", (chunk) => {
controller.enqueue(chunk);
})
.on("end", () => {
controller.close();
})
.on("error", (err) => {
console.error(err);
controller.error(err);
});
},
);
} catch (error) {
controller.error(error);
console.error(error);
}
},
}),
{
headers,
},
);
} catch (error) {
const e = error as Error;
console.error(e);
return NextResponse.json(
{
error: e.message,
},
{
status: 500,
},
);
}
}
@@ -906,6 +906,7 @@ function FooterField({ form, onResetField }: FormProps) {
onClick={() => {
setContent(formatFooterContent(form.watch("site.footer"), form.getValues("site")));
}}
type='button'
>
Reload Preview
</Button>
+3 -1
View File
@@ -256,7 +256,7 @@ export default function Navbar() {
className='mx-2 my-auto h-6'
/>
{!!config.showGuideButton && (
{!!config.showGuideButton ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -291,6 +291,8 @@ export default function Navbar() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<></>
)}
<Button
+106 -5
View File
@@ -21,6 +21,7 @@ import "@vidstack/react/player/styles/default/layouts/audio.css";
import "@vidstack/react/player/styles/default/layouts/video.css";
import "@vidstack/react/player/styles/default/theme.css";
import { ChevronLeft, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useCallback, useRef, useState } from "react";
import { type z } from "zod";
@@ -28,7 +29,8 @@ import { PageLoader } from "~/components/layout";
import Icon from "~/components/ui/icon";
import useLoading from "~/hooks/useLoading";
import { MediaPlayerIcons } from "~/lib/previewHelper";
import { MediaPlayerIcons, getPreviewIcon } from "~/lib/previewHelper";
import { cn, formatDate } from "~/lib/utils";
import { type Schema_File } from "~/types/schema";
@@ -37,8 +39,9 @@ import "~/styles/vidstack.css";
type Props = {
file: z.infer<typeof Schema_File>;
type: "video" | "audio";
playlist: z.infer<typeof Schema_File>[];
};
export default function PreviewMedia({ file, type }: Props) {
export default function PreviewMedia({ file, type, playlist }: Props) {
const player = useRef<MediaPlayerInstance>(null);
const [canPlay, setCanPlay] = useState<boolean>(false);
@@ -85,7 +88,7 @@ export default function PreviewMedia({ file, type }: Props) {
seekBackwardButton: null,
seekForwardButton: null,
playButton: (
<div className='flex w-full items-center justify-center gap-2 md:w-fit'>
<div className='flex w-full items-center justify-center gap-1 md:w-fit'>
<SeekButton
className='vds-button'
seconds={-10}
@@ -93,7 +96,7 @@ export default function PreviewMedia({ file, type }: Props) {
<MediaPlayerIcons.SeekButton.Backward />
</SeekButton>
<PlayButton
className='vds-button vds-play-button'
className='vds-button vds-play-button aspect-square'
disabled={canPlay === false}
>
{canPlay ? (
@@ -144,6 +147,12 @@ export default function PreviewMedia({ file, type }: Props) {
</div>
<div className='flex items-center'>
<PlaylistMenu
playlist={playlist}
file={file}
placement={"bottom end"}
/>
<Menu.Root className='vds-menu'>
<Menu.Button
className='vds-menu-button vds-button'
@@ -216,7 +225,7 @@ export default function PreviewMedia({ file, type }: Props) {
icons={MediaPlayerIcons}
colorScheme='default'
smallLayoutWhen={smallVideoLayoutQuery}
showTooltipDelay={150}
showTooltipDelay={200}
slots={{
currentTime: (
<Time
@@ -230,7 +239,14 @@ export default function PreviewMedia({ file, type }: Props) {
type='duration'
/>
),
beforeSettingsMenu: (
<PlaylistMenu
playlist={playlist}
file={file}
/>
),
settingsMenu: (
<>
<Menu.Root className='vds-menu'>
<Menu.Button
className='vds-menu-button vds-button'
@@ -293,6 +309,7 @@ export default function PreviewMedia({ file, type }: Props) {
<AudioGain />
</Menu.Items>
</Menu.Root>
</>
),
}}
/>
@@ -392,3 +409,87 @@ function AudioGain() {
</Menu.Root>
);
}
function PlaylistMenu({
playlist,
file,
placement = "top end",
}: {
playlist: z.infer<typeof Schema_File>[];
file: z.infer<typeof Schema_File>;
placement?: Menu.ItemsProps["placement"];
}) {
return (
<Menu.Root className='vds-menu'>
<Menu.Button
className='vds-menu-button vds-button'
aria-label='Playlist'
>
<MediaPlayerIcons.Menu.Chapters />
</Menu.Button>
<Menu.Items
className='vds-menu-items group space-y-1'
placement={placement}
>
{playlist.map((item) => {
const isCurrent = `${item.name}#${item.size}` === `${file.name}#${file.size}`;
const Wrapper = ({ children, className }: React.PropsWithChildren<{ className?: string }>) =>
isCurrent ? (
<div
className={className}
title={item.name}
>
{children}
</div>
) : (
<Link
href={`${item.name}`}
className={className}
title={item.name}
>
{children}
</Link>
);
return (
<Wrapper
key={`playlist-${item.encryptedId}`}
className={cn(
"flex w-full max-w-96 grid-cols-4 place-items-center gap-2 rounded-lg",
isCurrent ? "bg-primary/10" : "hover:bg-primary/5",
)}
>
{/* Thumbnail */}
<div
className={cn(
"aspect-video h-16 grow-0 overflow-hidden rounded-lg bg-black",
isCurrent && "border border-primary",
)}
>
{item.mimeType.includes("video") ? (
<img
src={`/api/thumb/${item.encryptedId}`}
alt={`Thumbnail for ${item.name}`}
className='aspect-video w-full object-contain'
/>
) : (
<div className={"grid aspect-video w-full place-items-center"}>
<Icon
name={getPreviewIcon(item.fileExtension ?? "", item.mimeType)}
className={"size-6"}
/>
</div>
)}
</div>
<div className={"col-span-3 w-full flex-1 p-2"}>
<p className={"line-clamp-1 break-all"}>{item.name}</p>
<span className={"text-sm text-muted-foreground"}>{formatDate(item.modifiedTime)}</span>
</div>
</Wrapper>
);
})}
</Menu.Items>
</Menu.Root>
);
}
+6 -7
View File
@@ -14,7 +14,7 @@ import {
PreviewUnknown,
} from "~/components/preview";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "~/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { type getFileType } from "~/lib/previewHelper";
import { cn } from "~/lib/utils";
@@ -23,8 +23,6 @@ import { type Schema_File } from "~/types/schema";
import config from "config";
import MediaPlaylistLayout from "./MediaPlaylistLayout";
type Props = {
data: z.infer<typeof Schema_File>;
fileType: "unknown" | ReturnType<typeof getFileType>;
@@ -32,7 +30,7 @@ type Props = {
playlist: z.infer<typeof Schema_File>[];
paths: string[];
};
export default function PreviewLayout({ data, paths, fileType, token, playlist }: Props) {
export default function PreviewLayout({ data, fileType, token, playlist }: Props) {
const [view, setView] = useState<"markdown" | "raw">("markdown");
const PreviewComponent = useMemo(() => {
switch (fileType) {
@@ -44,6 +42,7 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
<PreviewMedia
file={data}
type={fileType}
playlist={playlist}
/>
);
case "code":
@@ -82,7 +81,7 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
default:
return <PreviewUnknown />;
}
}, [fileType, data, view, token]);
}, [fileType, data, view, token, playlist]);
return (
<div
@@ -134,14 +133,14 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
)}
</CardContent>
<CardFooter>
{/* <CardFooter>
<MediaPlaylistLayout
type={"inside"}
paths={paths}
currentItem={data}
playlist={playlist}
/>
</CardFooter>
</CardFooter> */}
</Card>
<PreviewInformation
+1 -1
View File
@@ -8,7 +8,7 @@ const config: z.input<typeof Schema_Config> = {
* If possible, please don't change this value
* Even if you're creating a PR, just let me change it myself
*/
version: "2.0.4",
version: "2.4.2",
/**
* Base path of the app, used for generating links
*
+3 -3
View File
@@ -13,7 +13,7 @@ import config from "~/config/gIndex.config";
export const versionExpectMap: Record<"v1" | "v2" | "latest", string[]> = {
v1: ["1.0.0", "1.0.1", "1.0.2", "1.0.3"],
v2: ["2.0.0", "2.0.1", "2.0.2", "2.0.3"],
latest: ["2.0.4"],
latest: ["2.0.4", "2.4.0", "2.4.1", "2.4.2"],
};
export type PickFileResponse =
| {
@@ -166,7 +166,7 @@ export const initialConfiguration: z.input<typeof Schema_App_Configuration> = {
};
export const configurationTemplate = `import { type z } from "zod";
import { BASE_URL } from "~/constant";
import { BASE_URL, IS_DEV } from "~/constant";
import { type Schema_Config } from "~/types/schema";
@@ -184,7 +184,7 @@ const config: z.input<typeof Schema_Config> = {
* @default process.env.NEXT_PUBLIC_DOMAIN
* @fallback process.env.NEXT_PUBLIC_VERCEL_URL
*/
basePath: \`https://\${BASE_URL}\`,
basePath: IS_DEV ? "http://localhost:3000" : \`https://\${BASE_URL}\`,
/**
* Show deploy guide dropdown on navbar
+1 -1
View File
@@ -346,7 +346,7 @@ export const MediaPlayerIcons = {
Chapters: () => (
<Icon
hideWrapper
name='TableOfContents'
name='List'
className='vds-icon size-5'
/>
),
+1 -1
View File
@@ -112,7 +112,7 @@ const Schema_Config_API = z
allowDownloadProtectedFile: z.coerce.boolean(),
temporaryTokenDuration: z.coerce.number().positive(),
maxFileSize: z.coerce.number().positive(),
maxFileSize: z.coerce.number(),
})
.refine(
(data) => {