mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Update preview logic
This commit is contained in:
+59
-16
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AudioPlayer from "react-h5-audio-player";
|
|
||||||
import "react-h5-audio-player/lib/styles.css";
|
import "react-h5-audio-player/lib/styles.css";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Schema_File } from "~/schema";
|
import { Schema_File } from "~/schema";
|
||||||
@@ -10,7 +10,27 @@ import { cn } from "~/utils";
|
|||||||
import Icon from "~/components/Icon";
|
import Icon from "~/components/Icon";
|
||||||
|
|
||||||
import { CreateDownloadToken } from "./actions";
|
import { CreateDownloadToken } from "./actions";
|
||||||
import "./r5-style.css";
|
|
||||||
|
// import "./r5-style.css";
|
||||||
|
|
||||||
|
const Plyr = dynamic(() => import("plyr-react"), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-auto min-h-[50dvh] w-full",
|
||||||
|
"flex flex-grow flex-col items-center justify-center gap-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name='LoaderCircle'
|
||||||
|
size={32}
|
||||||
|
className='animate-spin text-foreground'
|
||||||
|
/>
|
||||||
|
<p>Loading player...</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
file: z.infer<typeof Schema_File>;
|
file: z.infer<typeof Schema_File>;
|
||||||
@@ -28,7 +48,7 @@ export default function PreviewAudio({ file }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = await CreateDownloadToken();
|
const token = await CreateDownloadToken();
|
||||||
setAudioSrc(`/api/download/${file.encryptedId}?token=${token}`);
|
setAudioSrc(`/api/stream/${file.encryptedId}?token=${token}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as Error;
|
const e = error as Error;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -66,20 +86,43 @@ export default function PreviewAudio({ file }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<AudioPlayer
|
<Plyr
|
||||||
autoPlay={false}
|
source={{
|
||||||
layout='stacked-reverse'
|
type: "audio",
|
||||||
src={audioSrc}
|
sources: [
|
||||||
showJumpControls={false}
|
{
|
||||||
showFilledVolume
|
src: audioSrc,
|
||||||
style={{
|
type: file.mimeType,
|
||||||
borderRadius: "var(--radius)",
|
size: file.size,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: file.name,
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
options={{
|
||||||
console.error(e);
|
controls: [
|
||||||
setError(
|
"play-large",
|
||||||
"Could not preview this audio, try downloading the file",
|
"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,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+45
-10
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer";
|
import DocViewer, { DocRenderer } from "@cyntler/react-doc-viewer";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Schema_File } from "~/schema";
|
import { Schema_File } from "~/schema";
|
||||||
@@ -8,15 +8,49 @@ import { cn } from "~/utils";
|
|||||||
|
|
||||||
import Icon from "~/components/Icon";
|
import Icon from "~/components/Icon";
|
||||||
|
|
||||||
|
import config from "~/config/gIndex.config";
|
||||||
|
|
||||||
import { CreateDownloadToken } from "./actions";
|
import { CreateDownloadToken } from "./actions";
|
||||||
|
|
||||||
|
const GoogleDocsViewerRenderer: DocRenderer = ({
|
||||||
|
mainState: { currentDocument },
|
||||||
|
}) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={viewerUrl.toString()}
|
||||||
|
className='m-0 h-full max-h-[70dvh] min-h-[70dvh] w-full overflow-hidden rounded-[var(--radius)] border border-border p-0 !text-black'
|
||||||
|
frameBorder={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
GoogleDocsViewerRenderer.fileTypes = [
|
||||||
|
"application/vnd.google-apps.document",
|
||||||
|
"application/vnd.google-apps.presentation",
|
||||||
|
"application/vnd.google-apps.spreadsheet",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"text/csv",
|
||||||
|
"application/pdf",
|
||||||
|
"application/vnd.oasis.opendocument.text",
|
||||||
|
];
|
||||||
|
GoogleDocsViewerRenderer.weight = 100;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
file: z.infer<typeof Schema_File>;
|
file: z.infer<typeof Schema_File>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PreviewDoc({ file }: Props) {
|
export default function PreviewDoc({ file }: Props) {
|
||||||
const [docSrc, setDocSrc] = useState<string>("");
|
const [docSrc, setDocSrc] = useState<string>("");
|
||||||
const [docBuffer, setDocBuffer] = useState<ArrayBuffer>();
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
@@ -28,11 +62,12 @@ export default function PreviewDoc({ file }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = await CreateDownloadToken();
|
const token = await CreateDownloadToken();
|
||||||
const buffer = await fetch(
|
const streamURL = new URL(
|
||||||
`/api/download/${file.encryptedId}?token=${token}`,
|
`/api/stream/${file.encryptedId}`,
|
||||||
).then((res) => res.arrayBuffer());
|
config.basePath,
|
||||||
setDocBuffer(buffer);
|
);
|
||||||
setDocSrc(`/api/download/${file.encryptedId}?token=${token}`);
|
streamURL.searchParams.set("token", token);
|
||||||
|
setDocSrc(streamURL.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as Error;
|
const e = error as Error;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -74,12 +109,11 @@ export default function PreviewDoc({ file }: Props) {
|
|||||||
documents={[
|
documents={[
|
||||||
{
|
{
|
||||||
uri: docSrc,
|
uri: docSrc,
|
||||||
fileData: docBuffer,
|
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileType: file.mimeType,
|
fileType: file.mimeType,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pluginRenderers={DocViewerRenderers}
|
pluginRenderers={[GoogleDocsViewerRenderer]}
|
||||||
config={{
|
config={{
|
||||||
header: {
|
header: {
|
||||||
disableHeader: true,
|
disableHeader: true,
|
||||||
@@ -118,7 +152,8 @@ export default function PreviewDoc({ file }: Props) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full max-h-[70dvh] min-h-[70dvh] w-full rounded-[var(--radius)] border border-border !text-black",
|
"h-full max-h-[70dvh] min-h-[70dvh] w-full overflow-hidden rounded-[var(--radius)] !text-black",
|
||||||
|
"rounded-[var(--radius)] [&>div#proxy-renderer]:overflow-hidden ",
|
||||||
)}
|
)}
|
||||||
theme={{
|
theme={{
|
||||||
disableThemeScrollbar: true,
|
disableThemeScrollbar: true,
|
||||||
|
|||||||
+64
-11
@@ -6,6 +6,9 @@ import { Schema_File } from "~/schema";
|
|||||||
import { cn } from "~/utils";
|
import { cn } from "~/utils";
|
||||||
|
|
||||||
import Icon from "~/components/Icon";
|
import Icon from "~/components/Icon";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||||
|
|
||||||
|
import config from "~/config/gIndex.config";
|
||||||
|
|
||||||
import { CreateDownloadToken } from "./actions";
|
import { CreateDownloadToken } from "./actions";
|
||||||
|
|
||||||
@@ -13,7 +16,10 @@ type Props = {
|
|||||||
file: z.infer<typeof Schema_File>;
|
file: z.infer<typeof Schema_File>;
|
||||||
};
|
};
|
||||||
export default function PreviewImage({ file }: Props) {
|
export default function PreviewImage({ file }: Props) {
|
||||||
const [imgSrc, setImgSrc] = useState<string>("");
|
const [imgSrc, setImgSrc] = useState<string>(
|
||||||
|
`/api/thumb/${file.encryptedId}?size=4`,
|
||||||
|
);
|
||||||
|
const [imgLoaded, setImgLoaded] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
@@ -25,7 +31,31 @@ export default function PreviewImage({ file }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = await CreateDownloadToken();
|
const token = await CreateDownloadToken();
|
||||||
setImgSrc(`/api/download/${file.encryptedId}?token=${token}&media=1`);
|
|
||||||
|
const streamURL = new URL(
|
||||||
|
`/api/thumb/${file.encryptedId}?size=1000`,
|
||||||
|
config.basePath,
|
||||||
|
);
|
||||||
|
streamURL.searchParams.set("token", token);
|
||||||
|
fetch(streamURL, {
|
||||||
|
headers: {
|
||||||
|
Range: `bytes=0-${(file.size || 1) - 1}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Could not load image");
|
||||||
|
}
|
||||||
|
return res.blob();
|
||||||
|
})
|
||||||
|
.then((blob) => {
|
||||||
|
const urlobject = URL.createObjectURL(blob);
|
||||||
|
setImgSrc(urlobject);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setImgLoaded(true);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 150); // Add a delay to show the image
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as Error;
|
const e = error as Error;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -62,15 +92,38 @@ export default function PreviewImage({ file }: Props) {
|
|||||||
<span className='text-center text-destructive'>{error}</span>
|
<span className='text-center text-destructive'>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<div className='h-fit w-full space-y-3 overflow-hidden rounded-[var(--radius)]'>
|
||||||
src={imgSrc}
|
<img
|
||||||
alt={file.name}
|
src={imgSrc}
|
||||||
className='max-h-[70dvh] w-full rounded-[var(--radius)] bg-muted object-contain object-center'
|
alt={file.name}
|
||||||
onError={(e) => {
|
className={cn(
|
||||||
console.error(e);
|
"h-full max-h-[70dvh] w-full bg-muted object-contain object-center transition",
|
||||||
setError("Could not preview this image, try downloading the file");
|
imgLoaded ? "blur-none" : "animate-pulse blur",
|
||||||
}}
|
)}
|
||||||
/>
|
onError={(e) => {
|
||||||
|
console.error(e);
|
||||||
|
setError(
|
||||||
|
"Could not preview this image, try downloading the file",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert className='bg-yellow-50 text-yellow-600 dark:bg-yellow-950 dark:text-yellow-500'>
|
||||||
|
<div className='flex items-start gap-3'>
|
||||||
|
<Icon
|
||||||
|
name='TriangleAlert'
|
||||||
|
className='size-5'
|
||||||
|
/>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<AlertTitle>Preview Only</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
This image is a preview and may not be the full resolution.
|
||||||
|
Please download the file for the full resolution.
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+63
-32
@@ -3,11 +3,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Schema_File } from "~/schema";
|
import { Schema_File } from "~/schema";
|
||||||
|
import { cn } from "~/utils";
|
||||||
|
|
||||||
|
import Icon from "~/components/Icon";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
|
||||||
import { getFileType } from "~/utils/previewHelper";
|
import { getFileType } from "~/utils/previewHelper";
|
||||||
|
|
||||||
|
import config from "~/config/gIndex.config";
|
||||||
|
|
||||||
import PreviewAction from "./@preview.action";
|
import PreviewAction from "./@preview.action";
|
||||||
import PreviewAudio from "./@preview.audio";
|
import PreviewAudio from "./@preview.audio";
|
||||||
import PreviewDoc from "./@preview.doc";
|
import PreviewDoc from "./@preview.doc";
|
||||||
@@ -35,39 +39,66 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
|
|||||||
fileType={fileType}
|
fileType={fileType}
|
||||||
/>
|
/>
|
||||||
<CardContent className='p-1.5 pt-0 tablet:p-3 tablet:pt-0'>
|
<CardContent className='p-1.5 pt-0 tablet:p-3 tablet:pt-0'>
|
||||||
<div className='px-3'>
|
{config.apiConfig.streamMaxSize &&
|
||||||
{fileType === "image" ? (
|
Number(data.size || 0) > config.apiConfig.streamMaxSize ? (
|
||||||
<PreviewImage file={data} />
|
<div
|
||||||
) : fileType === "audio" ? (
|
className={cn(
|
||||||
<PreviewAudio file={data} />
|
"h-auto min-h-[33dvh] w-full",
|
||||||
) : fileType === "video" ? (
|
"flex flex-grow flex-col items-center justify-center gap-3",
|
||||||
<PreviewVideo file={data} />
|
)}
|
||||||
) : fileType === "code" ? (
|
>
|
||||||
<PreviewRich
|
<Icon
|
||||||
file={data}
|
name='Frown'
|
||||||
code
|
size={32}
|
||||||
view={view}
|
className='text-muted-foreground'
|
||||||
/>
|
/>
|
||||||
) : fileType === "text" ? (
|
<h4 className='text-muted-foreground'>Preview not available</h4>
|
||||||
<PreviewRich
|
<p className='text-center text-muted-foreground tablet:text-sm'>
|
||||||
file={data}
|
Looks like this file size exceed the preview size limit
|
||||||
view={view}
|
</p>
|
||||||
/>
|
</div>
|
||||||
) : fileType === "markdown" ? (
|
) : (
|
||||||
<PreviewRich
|
/**
|
||||||
file={data}
|
* TODO: Might need a better way to handle large files preview
|
||||||
view={view}
|
* like manga, pdf, etc
|
||||||
/>
|
*
|
||||||
) : fileType === "document" ? (
|
* For now it's downloading the whole file and then previewing it
|
||||||
<PreviewDoc file={data} />
|
* which is not a good implementation
|
||||||
) : fileType === "pdf" ? (
|
*/
|
||||||
<PreviewDoc file={data} />
|
<div className='px-3'>
|
||||||
) : fileType === "manga" ? (
|
{fileType === "image" ? (
|
||||||
<PreviewManga file={data} />
|
<PreviewImage file={data} />
|
||||||
) : (
|
) : fileType === "audio" ? (
|
||||||
<PreviewUnknown />
|
<PreviewAudio file={data} />
|
||||||
)}
|
) : fileType === "video" ? (
|
||||||
</div>
|
<PreviewVideo file={data} />
|
||||||
|
) : fileType === "code" ? (
|
||||||
|
<PreviewRich
|
||||||
|
file={data}
|
||||||
|
code
|
||||||
|
view={view}
|
||||||
|
/>
|
||||||
|
) : fileType === "text" ? (
|
||||||
|
<PreviewRich
|
||||||
|
file={data}
|
||||||
|
view={view}
|
||||||
|
/>
|
||||||
|
) : fileType === "markdown" ? (
|
||||||
|
<PreviewRich
|
||||||
|
file={data}
|
||||||
|
view={view}
|
||||||
|
/>
|
||||||
|
) : fileType === "document" ? (
|
||||||
|
<PreviewDoc file={data} />
|
||||||
|
) : fileType === "pdf" ? (
|
||||||
|
<PreviewDoc file={data} />
|
||||||
|
) : fileType === "manga" ? (
|
||||||
|
<PreviewManga file={data} />
|
||||||
|
) : (
|
||||||
|
<PreviewUnknown />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<PreviewAction file={data} />
|
<PreviewAction file={data} />
|
||||||
|
|||||||
+180
-49
@@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import JSZip from "jszip";
|
import { AsyncUnzipInflate, Unzip } from "fflate";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Schema_File } from "~/schema";
|
import { Schema_File } from "~/schema";
|
||||||
import { cn } from "~/utils";
|
import { cn } from "~/utils";
|
||||||
|
|
||||||
import Icon from "~/components/Icon";
|
import Icon from "~/components/Icon";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselApi,
|
CarouselApi,
|
||||||
@@ -15,9 +17,12 @@ import {
|
|||||||
CarouselNext,
|
CarouselNext,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
} from "~/components/ui/carousel";
|
} from "~/components/ui/carousel";
|
||||||
|
import { Progress } from "~/components/ui/progress";
|
||||||
|
|
||||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||||
|
|
||||||
|
import config from "~/config/gIndex.config";
|
||||||
|
|
||||||
import { CreateDownloadToken } from "./actions";
|
import { CreateDownloadToken } from "./actions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -31,40 +36,101 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
const [currentImage, setCurrentImage] = useState<number>(1);
|
const [currentImage, setCurrentImage] = useState<number>(1);
|
||||||
const [viewSize, setViewSize] = useState<"fit" | "full">("fit");
|
const [viewSize, setViewSize] = useState<"fit" | "full">("fit");
|
||||||
const [api, setApi] = useState<CarouselApi>();
|
const [api, setApi] = useState<CarouselApi>();
|
||||||
|
|
||||||
|
const [loadedPercent, setLoadedPercent] = useState<number>(0);
|
||||||
|
const [loadFirstX, setLoadFirstX] = useState<number>(5); // 5MB
|
||||||
|
|
||||||
|
const abortController = useRef<AbortController>(new AbortController());
|
||||||
|
|
||||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!abortController.current) {
|
||||||
|
abortController.current = new AbortController();
|
||||||
|
}
|
||||||
if (!file.encryptedWebContentLink) {
|
if (!file.encryptedWebContentLink) {
|
||||||
setError("No video to preview");
|
setError("No video to preview");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = await CreateDownloadToken();
|
const token = await CreateDownloadToken();
|
||||||
const manga = await fetch(
|
const streamURL = new URL(
|
||||||
`/api/download/${file.encryptedId}?token=${token}`,
|
`/api/stream/${file.encryptedId}`,
|
||||||
|
config.basePath,
|
||||||
);
|
);
|
||||||
const archiveBlob = await manga.blob();
|
streamURL.searchParams.set("token", token);
|
||||||
const zipData = await JSZip.loadAsync(archiveBlob);
|
|
||||||
|
|
||||||
const tempArray: { name: string; blob: string }[] = [];
|
const bufferStream = await fetch(streamURL, {
|
||||||
const files = Object.values(zipData.files);
|
signal: abortController.current.signal,
|
||||||
for (const file of files) {
|
headers: {
|
||||||
const f = zipData.file(file.name);
|
Range: `bytes=0-${
|
||||||
if (!f) continue;
|
Math.min(Number(file.size || 1), loadFirstX * 1024 * 1024) - 1
|
||||||
const blob = await f.async("blob");
|
}`,
|
||||||
const reader = new FileReader();
|
},
|
||||||
reader.onload = () => {
|
});
|
||||||
setImages((prev) => {
|
|
||||||
const exist = prev.find((p) => p.name === file.name);
|
const contentLength = bufferStream.headers.get("Content-Length");
|
||||||
if (exist) return prev;
|
const totalBytes = parseInt(contentLength || "0", 10);
|
||||||
return [
|
|
||||||
...prev,
|
const reader = bufferStream.body?.getReader();
|
||||||
{ name: file.name, blob: reader.result as string },
|
if (reader) {
|
||||||
];
|
const chunks: Uint8Array[] = [];
|
||||||
});
|
let receivedLength = 0;
|
||||||
};
|
|
||||||
reader.readAsDataURL(blob);
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
chunks.push(value);
|
||||||
|
receivedLength += value.length;
|
||||||
|
|
||||||
|
const percent = (receivedLength / totalBytes) * 100;
|
||||||
|
setLoadedPercent((prev) => {
|
||||||
|
if (percent > prev) return percent;
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new Uint8Array(receivedLength);
|
||||||
|
let position = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
buffer.set(chunk, position);
|
||||||
|
position += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unzip = new Unzip((file) => {
|
||||||
|
file.ondata = (err, data, final) => {
|
||||||
|
if (err) throw err;
|
||||||
|
// Check if the file is an image
|
||||||
|
if (
|
||||||
|
!file.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(".jpg" || ".jpeg" || ".png" || ".gif" || ".webp")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (!final) return; // Skip if not fully loaded
|
||||||
|
|
||||||
|
const blob = new Blob([data]);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setImages((prev) => {
|
||||||
|
const exist = prev.find((p) => p.name === file.name);
|
||||||
|
if (exist) return prev;
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{ name: file.name, blob: reader.result as string },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
};
|
||||||
|
file.start();
|
||||||
|
});
|
||||||
|
unzip.register(AsyncUnzipInflate);
|
||||||
|
unzip.push(buffer);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as Error;
|
const e = error as Error;
|
||||||
@@ -74,7 +140,12 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [file]);
|
|
||||||
|
// return () => {
|
||||||
|
// abortController.current.abort("Cancelled by user");
|
||||||
|
// };
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
@@ -99,7 +170,31 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
size={32}
|
size={32}
|
||||||
className='animate-spin text-foreground'
|
className='animate-spin text-foreground'
|
||||||
/>
|
/>
|
||||||
<p>Loading manga content...</p>
|
<p>Please wait while we downloading the content for preview</p>
|
||||||
|
<div className='flex w-full max-w-sm flex-col gap-1'>
|
||||||
|
<Progress
|
||||||
|
className='h-2 w-full'
|
||||||
|
value={loadedPercent}
|
||||||
|
/>
|
||||||
|
<span className='text-center text-xs text-muted-foreground'>
|
||||||
|
{Math.round(loadedPercent)}% loaded
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size={"sm"}
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={async () => {
|
||||||
|
abortController.current.abort("Cancelled by user");
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setError("Preview cancelled by user");
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel Preview
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className='flex h-full flex-col items-center justify-center gap-3'>
|
<div className='flex h-full flex-col items-center justify-center gap-3'>
|
||||||
@@ -111,7 +206,7 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
<span className='text-center text-destructive'>{error}</span>
|
<span className='text-center text-destructive'>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-col gap-3'>
|
<div className='flex w-full flex-col items-center justify-center gap-3'>
|
||||||
<div className='h-full w-full tablet:px-8'>
|
<div className='h-full w-full tablet:px-8'>
|
||||||
<Carousel
|
<Carousel
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -124,24 +219,28 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
setApi={setApi}
|
setApi={setApi}
|
||||||
>
|
>
|
||||||
<CarouselContent className='h-full w-full'>
|
<CarouselContent className='h-full w-full'>
|
||||||
{images.map((image, index) => (
|
{images
|
||||||
<CarouselItem
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
key={index}
|
.map((image, index) => (
|
||||||
className='flex h-full flex-col items-center justify-center'
|
<CarouselItem
|
||||||
>
|
key={index}
|
||||||
<img
|
className='flex h-full flex-col items-center justify-center'
|
||||||
src={image.blob}
|
>
|
||||||
alt={`${image.name} - Page ${index + 1}`}
|
<img
|
||||||
className={cn(
|
src={image.blob}
|
||||||
"w-full object-contain",
|
alt={`${image.name} - Page ${index + 1}`}
|
||||||
viewSize === "fit" ? "h-full max-h-[70dvh]" : "h-full",
|
className={cn(
|
||||||
)}
|
"w-full object-contain",
|
||||||
/>
|
viewSize === "fit"
|
||||||
<span className='muted text-center text-xs'>
|
? "h-full max-h-[70dvh]"
|
||||||
{image.name}
|
: "h-full",
|
||||||
</span>
|
)}
|
||||||
</CarouselItem>
|
/>
|
||||||
))}
|
<span className='muted text-center text-xs'>
|
||||||
|
{image.name}
|
||||||
|
</span>
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
</CarouselContent>
|
</CarouselContent>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<>
|
<>
|
||||||
@@ -151,11 +250,27 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
) : null}
|
) : null}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex w-full items-center justify-end gap-1.5'>
|
<div className='flex w-full items-center justify-end gap-3'>
|
||||||
<span className='muted text-sm'>
|
<span className='text-primary-foreground'>
|
||||||
{currentImage}/{images.length}
|
{currentImage}/{images.length}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<Button
|
||||||
|
size='icon'
|
||||||
|
variant={"ghost"}
|
||||||
|
className='aspect-square size-8 p-0.5'
|
||||||
|
onClick={() =>
|
||||||
|
setViewSize((prev) => {
|
||||||
|
if (prev === "fit") return "full";
|
||||||
|
return "fit";
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={viewSize === "fit" ? "Maximize" : "Minimize"}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
{/* <div
|
||||||
className='relative z-10 cursor-pointer p-0.5 text-foreground/80 transition hover:text-foreground'
|
className='relative z-10 cursor-pointer p-0.5 text-foreground/80 transition hover:text-foreground'
|
||||||
onClick={() => setViewSize(viewSize === "fit" ? "full" : "fit")}
|
onClick={() => setViewSize(viewSize === "fit" ? "full" : "fit")}
|
||||||
>
|
>
|
||||||
@@ -163,8 +278,24 @@ export default function PreviewManga({ file }: Props) {
|
|||||||
name={viewSize === "fit" ? "Maximize" : "Minimize"}
|
name={viewSize === "fit" ? "Maximize" : "Minimize"}
|
||||||
size={14}
|
size={14}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Alert className='bg-yellow-50 text-yellow-600 dark:bg-yellow-950 dark:text-yellow-500'>
|
||||||
|
<div className='flex items-start gap-3'>
|
||||||
|
<Icon
|
||||||
|
name='TriangleAlert'
|
||||||
|
className='size-5'
|
||||||
|
/>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<AlertTitle>Preview Only</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
We only load the first {loadFirstX}MB of the file for preview.
|
||||||
|
Please download the file for full content.
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function PreviewRich({ file, code, view }: Props) {
|
|||||||
// if (code) {
|
// if (code) {
|
||||||
// setContent(`\`\`\`${file.fileExtension}\n${text}\`\`\``);
|
// setContent(`\`\`\`${file.fileExtension}\n${text}\`\`\``);
|
||||||
// } else {
|
// } else {
|
||||||
setContent(text);
|
setContent(text.trim());
|
||||||
// }
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const e = error as Error;
|
const e = error as Error;
|
||||||
@@ -91,7 +91,7 @@ export default function PreviewRich({ file, code, view }: Props) {
|
|||||||
<Markdown
|
<Markdown
|
||||||
content={
|
content={
|
||||||
code && view === "markdown"
|
code && view === "markdown"
|
||||||
? `\`\`\`${file.fileExtension}\n${content}\`\`\``
|
? `\`\`\`${file.fileExtension}\n${content}`
|
||||||
: content
|
: content
|
||||||
}
|
}
|
||||||
view={view}
|
view={view}
|
||||||
@@ -99,23 +99,26 @@ export default function PreviewRich({ file, code, view }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute bottom-0 z-10 flex w-full items-center justify-center py-3 transition",
|
"bottom-0 z-10 flex w-full items-center justify-center py-3 transition",
|
||||||
expand
|
expand ? "relative" : "absolute",
|
||||||
? "pointer-events-none opacity-0"
|
// expand
|
||||||
: "pointer-events-auto opacity-100",
|
// ? "pointer-events-none opacity-0"
|
||||||
|
// : "pointer-events-auto opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
|
variant={"secondary"}
|
||||||
|
className='gap-1.5'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setExpand(true);
|
setExpand((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name='ChevronDown'
|
name={expand ? "ChevronUp" : "ChevronDown"}
|
||||||
size={16}
|
size={16}
|
||||||
/>
|
/>
|
||||||
Expand
|
{expand ? "Collapse" : "Expand"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+24
-24
@@ -12,6 +12,21 @@ import { CreateDownloadToken } from "./actions";
|
|||||||
|
|
||||||
const Plyr = dynamic(() => import("plyr-react"), {
|
const Plyr = dynamic(() => import("plyr-react"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-auto min-h-[50dvh] w-full",
|
||||||
|
"flex flex-grow flex-col items-center justify-center gap-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name='LoaderCircle'
|
||||||
|
size={32}
|
||||||
|
className='animate-spin text-foreground'
|
||||||
|
/>
|
||||||
|
<p>Loading player...</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -78,6 +93,7 @@ export default function PreviewVideo({ file }: Props) {
|
|||||||
size: file.size,
|
size: file.size,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
poster: `/api/thumb/${file.encryptedId}?size=1000`,
|
||||||
}}
|
}}
|
||||||
// crossOrigin='anonymous'
|
// crossOrigin='anonymous'
|
||||||
options={{
|
options={{
|
||||||
@@ -97,33 +113,17 @@ export default function PreviewVideo({ file }: Props) {
|
|||||||
"download",
|
"download",
|
||||||
"fullscreen",
|
"fullscreen",
|
||||||
],
|
],
|
||||||
|
volume: 0.5,
|
||||||
|
muted: false,
|
||||||
|
loop: { active: false },
|
||||||
|
speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
|
||||||
|
keyboard: {
|
||||||
|
focused: true,
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
// <ReactPlayer
|
|
||||||
// key={file.encryptedId}
|
|
||||||
// url={videoSrc}
|
|
||||||
// controls
|
|
||||||
// pip
|
|
||||||
// wrapper={({ children }) => (
|
|
||||||
// <div className='h-[60dvh] w-full overflow-hidden rounded-[var(--radius)] bg-muted'>
|
|
||||||
// {children}
|
|
||||||
// </div>
|
|
||||||
// )}
|
|
||||||
// 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?)");
|
|
||||||
// }
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user