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", "name": "next-gdrive-index",
"version": "2.0.4", "version": "2.4.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",
@@ -10,7 +10,8 @@
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json}\" --cache", "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": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
@@ -88,12 +89,16 @@
"@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0", "@typescript-eslint/parser": "^8.1.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-next": "^15.0.1", "eslint-config-next": "^15.0.1",
"ora": "^8.2.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "3.0.0", "prettier": "3.0.0",
"prettier-plugin-tailwindcss": "0.5.12", "prettier-plugin-tailwindcss": "0.5.12",
"prompts": "^2.4.2",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.5.3" "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={() => { onClick={() => {
setContent(formatFooterContent(form.watch("site.footer"), form.getValues("site"))); setContent(formatFooterContent(form.watch("site.footer"), form.getValues("site")));
}} }}
type='button'
> >
Reload Preview Reload Preview
</Button> </Button>
+3 -1
View File
@@ -256,7 +256,7 @@ export default function Navbar() {
className='mx-2 my-auto h-6' className='mx-2 my-auto h-6'
/> />
{!!config.showGuideButton && ( {!!config.showGuideButton ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@@ -291,6 +291,8 @@ export default function Navbar() {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : (
<></>
)} )}
<Button <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/layouts/video.css";
import "@vidstack/react/player/styles/default/theme.css"; import "@vidstack/react/player/styles/default/theme.css";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import Link from "next/link";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { type z } from "zod"; import { type z } from "zod";
@@ -28,7 +29,8 @@ import { PageLoader } from "~/components/layout";
import Icon from "~/components/ui/icon"; import Icon from "~/components/ui/icon";
import useLoading from "~/hooks/useLoading"; 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"; import { type Schema_File } from "~/types/schema";
@@ -37,8 +39,9 @@ import "~/styles/vidstack.css";
type Props = { type Props = {
file: z.infer<typeof Schema_File>; file: z.infer<typeof Schema_File>;
type: "video" | "audio"; 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 player = useRef<MediaPlayerInstance>(null);
const [canPlay, setCanPlay] = useState<boolean>(false); const [canPlay, setCanPlay] = useState<boolean>(false);
@@ -85,7 +88,7 @@ export default function PreviewMedia({ file, type }: Props) {
seekBackwardButton: null, seekBackwardButton: null,
seekForwardButton: null, seekForwardButton: null,
playButton: ( 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 <SeekButton
className='vds-button' className='vds-button'
seconds={-10} seconds={-10}
@@ -93,7 +96,7 @@ export default function PreviewMedia({ file, type }: Props) {
<MediaPlayerIcons.SeekButton.Backward /> <MediaPlayerIcons.SeekButton.Backward />
</SeekButton> </SeekButton>
<PlayButton <PlayButton
className='vds-button vds-play-button' className='vds-button vds-play-button aspect-square'
disabled={canPlay === false} disabled={canPlay === false}
> >
{canPlay ? ( {canPlay ? (
@@ -144,6 +147,12 @@ export default function PreviewMedia({ file, type }: Props) {
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
<PlaylistMenu
playlist={playlist}
file={file}
placement={"bottom end"}
/>
<Menu.Root className='vds-menu'> <Menu.Root className='vds-menu'>
<Menu.Button <Menu.Button
className='vds-menu-button vds-button' className='vds-menu-button vds-button'
@@ -216,7 +225,7 @@ export default function PreviewMedia({ file, type }: Props) {
icons={MediaPlayerIcons} icons={MediaPlayerIcons}
colorScheme='default' colorScheme='default'
smallLayoutWhen={smallVideoLayoutQuery} smallLayoutWhen={smallVideoLayoutQuery}
showTooltipDelay={150} showTooltipDelay={200}
slots={{ slots={{
currentTime: ( currentTime: (
<Time <Time
@@ -230,7 +239,14 @@ export default function PreviewMedia({ file, type }: Props) {
type='duration' type='duration'
/> />
), ),
beforeSettingsMenu: (
<PlaylistMenu
playlist={playlist}
file={file}
/>
),
settingsMenu: ( settingsMenu: (
<>
<Menu.Root className='vds-menu'> <Menu.Root className='vds-menu'>
<Menu.Button <Menu.Button
className='vds-menu-button vds-button' className='vds-menu-button vds-button'
@@ -293,6 +309,7 @@ export default function PreviewMedia({ file, type }: Props) {
<AudioGain /> <AudioGain />
</Menu.Items> </Menu.Items>
</Menu.Root> </Menu.Root>
</>
), ),
}} }}
/> />
@@ -392,3 +409,87 @@ function AudioGain() {
</Menu.Root> </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, PreviewUnknown,
} from "~/components/preview"; } from "~/components/preview";
import { Button } from "~/components/ui/button"; 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 { type getFileType } from "~/lib/previewHelper";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
@@ -23,8 +23,6 @@ import { type Schema_File } from "~/types/schema";
import config from "config"; import config from "config";
import MediaPlaylistLayout from "./MediaPlaylistLayout";
type Props = { type Props = {
data: z.infer<typeof Schema_File>; data: z.infer<typeof Schema_File>;
fileType: "unknown" | ReturnType<typeof getFileType>; fileType: "unknown" | ReturnType<typeof getFileType>;
@@ -32,7 +30,7 @@ type Props = {
playlist: z.infer<typeof Schema_File>[]; playlist: z.infer<typeof Schema_File>[];
paths: string[]; 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 [view, setView] = useState<"markdown" | "raw">("markdown");
const PreviewComponent = useMemo(() => { const PreviewComponent = useMemo(() => {
switch (fileType) { switch (fileType) {
@@ -44,6 +42,7 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
<PreviewMedia <PreviewMedia
file={data} file={data}
type={fileType} type={fileType}
playlist={playlist}
/> />
); );
case "code": case "code":
@@ -82,7 +81,7 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
default: default:
return <PreviewUnknown />; return <PreviewUnknown />;
} }
}, [fileType, data, view, token]); }, [fileType, data, view, token, playlist]);
return ( return (
<div <div
@@ -134,14 +133,14 @@ export default function PreviewLayout({ data, paths, fileType, token, playlist }
)} )}
</CardContent> </CardContent>
<CardFooter> {/* <CardFooter>
<MediaPlaylistLayout <MediaPlaylistLayout
type={"inside"} type={"inside"}
paths={paths} paths={paths}
currentItem={data} currentItem={data}
playlist={playlist} playlist={playlist}
/> />
</CardFooter> </CardFooter> */}
</Card> </Card>
<PreviewInformation <PreviewInformation
+1 -1
View File
@@ -8,7 +8,7 @@ const config: z.input<typeof Schema_Config> = {
* If possible, please don't change this value * If possible, please don't change this value
* Even if you're creating a PR, just let me change it myself * 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 * 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[]> = { export const versionExpectMap: Record<"v1" | "v2" | "latest", string[]> = {
v1: ["1.0.0", "1.0.1", "1.0.2", "1.0.3"], 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"], 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 = export type PickFileResponse =
| { | {
@@ -166,7 +166,7 @@ export const initialConfiguration: z.input<typeof Schema_App_Configuration> = {
}; };
export const configurationTemplate = `import { type z } from "zod"; 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"; import { type Schema_Config } from "~/types/schema";
@@ -184,7 +184,7 @@ const config: z.input<typeof Schema_Config> = {
* @default process.env.NEXT_PUBLIC_DOMAIN * @default process.env.NEXT_PUBLIC_DOMAIN
* @fallback process.env.NEXT_PUBLIC_VERCEL_URL * @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 * Show deploy guide dropdown on navbar
+1 -1
View File
@@ -346,7 +346,7 @@ export const MediaPlayerIcons = {
Chapters: () => ( Chapters: () => (
<Icon <Icon
hideWrapper hideWrapper
name='TableOfContents' name='List'
className='vds-icon size-5' className='vds-icon size-5'
/> />
), ),
+1 -1
View File
@@ -112,7 +112,7 @@ const Schema_Config_API = z
allowDownloadProtectedFile: z.coerce.boolean(), allowDownloadProtectedFile: z.coerce.boolean(),
temporaryTokenDuration: z.coerce.number().positive(), temporaryTokenDuration: z.coerce.number().positive(),
maxFileSize: z.coerce.number().positive(), maxFileSize: z.coerce.number(),
}) })
.refine( .refine(
(data) => { (data) => {