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";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import AudioPlayer from "react-h5-audio-player";
|
||||
import "react-h5-audio-player/lib/styles.css";
|
||||
import { z } from "zod";
|
||||
import { Schema_File } from "~/schema";
|
||||
@@ -10,7 +10,27 @@ import { cn } from "~/utils";
|
||||
import Icon from "~/components/Icon";
|
||||
|
||||
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 = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
@@ -28,7 +48,7 @@ export default function PreviewAudio({ file }: Props) {
|
||||
return;
|
||||
}
|
||||
const token = await CreateDownloadToken();
|
||||
setAudioSrc(`/api/download/${file.encryptedId}?token=${token}`);
|
||||
setAudioSrc(`/api/stream/${file.encryptedId}?token=${token}`);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
@@ -66,20 +86,43 @@ export default function PreviewAudio({ file }: Props) {
|
||||
</div>
|
||||
) : (
|
||||
<div className='w-full'>
|
||||
<AudioPlayer
|
||||
autoPlay={false}
|
||||
layout='stacked-reverse'
|
||||
src={audioSrc}
|
||||
showJumpControls={false}
|
||||
showFilledVolume
|
||||
style={{
|
||||
borderRadius: "var(--radius)",
|
||||
<Plyr
|
||||
source={{
|
||||
type: "audio",
|
||||
sources: [
|
||||
{
|
||||
src: audioSrc,
|
||||
type: file.mimeType,
|
||||
size: file.size,
|
||||
},
|
||||
],
|
||||
title: file.name,
|
||||
}}
|
||||
onError={(e) => {
|
||||
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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+45
-10
@@ -1,6 +1,6 @@
|
||||
"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";
|
||||
@@ -8,15 +8,49 @@ import { cn } from "~/utils";
|
||||
|
||||
import Icon from "~/components/Icon";
|
||||
|
||||
import config from "~/config/gIndex.config";
|
||||
|
||||
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 = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
};
|
||||
|
||||
export default function PreviewDoc({ file }: Props) {
|
||||
const [docSrc, setDocSrc] = useState<string>("");
|
||||
const [docBuffer, setDocBuffer] = useState<ArrayBuffer>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
@@ -28,11 +62,12 @@ export default function PreviewDoc({ file }: Props) {
|
||||
return;
|
||||
}
|
||||
const token = await CreateDownloadToken();
|
||||
const buffer = await fetch(
|
||||
`/api/download/${file.encryptedId}?token=${token}`,
|
||||
).then((res) => res.arrayBuffer());
|
||||
setDocBuffer(buffer);
|
||||
setDocSrc(`/api/download/${file.encryptedId}?token=${token}`);
|
||||
const streamURL = new URL(
|
||||
`/api/stream/${file.encryptedId}`,
|
||||
config.basePath,
|
||||
);
|
||||
streamURL.searchParams.set("token", token);
|
||||
setDocSrc(streamURL.toString());
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
@@ -74,12 +109,11 @@ export default function PreviewDoc({ file }: Props) {
|
||||
documents={[
|
||||
{
|
||||
uri: docSrc,
|
||||
fileData: docBuffer,
|
||||
fileName: file.name,
|
||||
fileType: file.mimeType,
|
||||
},
|
||||
]}
|
||||
pluginRenderers={DocViewerRenderers}
|
||||
pluginRenderers={[GoogleDocsViewerRenderer]}
|
||||
config={{
|
||||
header: {
|
||||
disableHeader: true,
|
||||
@@ -118,7 +152,8 @@ export default function PreviewDoc({ file }: Props) {
|
||||
},
|
||||
}}
|
||||
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={{
|
||||
disableThemeScrollbar: true,
|
||||
|
||||
+64
-11
@@ -6,6 +6,9 @@ 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";
|
||||
|
||||
@@ -13,7 +16,10 @@ type Props = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
};
|
||||
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 [error, setError] = useState<string>("");
|
||||
|
||||
@@ -25,7 +31,31 @@ export default function PreviewImage({ file }: Props) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
@@ -62,15 +92,38 @@ export default function PreviewImage({ file }: Props) {
|
||||
<span className='text-center text-destructive'>{error}</span>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
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");
|
||||
}}
|
||||
/>
|
||||
<div className='h-fit w-full space-y-3 overflow-hidden rounded-[var(--radius)]'>
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={file.name}
|
||||
className={cn(
|
||||
"h-full max-h-[70dvh] w-full bg-muted object-contain object-center transition",
|
||||
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>
|
||||
);
|
||||
|
||||
+63
-32
@@ -3,11 +3,15 @@
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { Schema_File } from "~/schema";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
import Icon from "~/components/Icon";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
|
||||
import { getFileType } from "~/utils/previewHelper";
|
||||
|
||||
import config from "~/config/gIndex.config";
|
||||
|
||||
import PreviewAction from "./@preview.action";
|
||||
import PreviewAudio from "./@preview.audio";
|
||||
import PreviewDoc from "./@preview.doc";
|
||||
@@ -35,39 +39,66 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
|
||||
fileType={fileType}
|
||||
/>
|
||||
<CardContent className='p-1.5 pt-0 tablet:p-3 tablet:pt-0'>
|
||||
<div className='px-3'>
|
||||
{fileType === "image" ? (
|
||||
<PreviewImage file={data} />
|
||||
) : fileType === "audio" ? (
|
||||
<PreviewAudio file={data} />
|
||||
) : fileType === "video" ? (
|
||||
<PreviewVideo file={data} />
|
||||
) : fileType === "code" ? (
|
||||
<PreviewRich
|
||||
file={data}
|
||||
code
|
||||
view={view}
|
||||
{config.apiConfig.streamMaxSize &&
|
||||
Number(data.size || 0) > config.apiConfig.streamMaxSize ? (
|
||||
<div
|
||||
className={cn(
|
||||
"h-auto min-h-[33dvh] w-full",
|
||||
"flex flex-grow flex-col items-center justify-center gap-3",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name='Frown'
|
||||
size={32}
|
||||
className='text-muted-foreground'
|
||||
/>
|
||||
) : 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>
|
||||
<h4 className='text-muted-foreground'>Preview not available</h4>
|
||||
<p className='text-center text-muted-foreground tablet:text-sm'>
|
||||
Looks like this file size exceed the preview size limit
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/**
|
||||
* TODO: Might need a better way to handle large files preview
|
||||
* like manga, pdf, etc
|
||||
*
|
||||
* For now it's downloading the whole file and then previewing it
|
||||
* which is not a good implementation
|
||||
*/
|
||||
<div className='px-3'>
|
||||
{fileType === "image" ? (
|
||||
<PreviewImage file={data} />
|
||||
) : fileType === "audio" ? (
|
||||
<PreviewAudio file={data} />
|
||||
) : fileType === "video" ? (
|
||||
<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>
|
||||
</Card>
|
||||
<PreviewAction file={data} />
|
||||
|
||||
+180
-49
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import JSZip from "jszip";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AsyncUnzipInflate, Unzip } from "fflate";
|
||||
import { useEffect, useRef, 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 { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselApi,
|
||||
@@ -15,9 +17,12 @@ import {
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "~/components/ui/carousel";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
|
||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||
|
||||
import config from "~/config/gIndex.config";
|
||||
|
||||
import { CreateDownloadToken } from "./actions";
|
||||
|
||||
type Props = {
|
||||
@@ -31,40 +36,101 @@ export default function PreviewManga({ file }: Props) {
|
||||
const [currentImage, setCurrentImage] = useState<number>(1);
|
||||
const [viewSize, setViewSize] = useState<"fit" | "full">("fit");
|
||||
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)");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!abortController.current) {
|
||||
abortController.current = new AbortController();
|
||||
}
|
||||
if (!file.encryptedWebContentLink) {
|
||||
setError("No video to preview");
|
||||
return;
|
||||
}
|
||||
const token = await CreateDownloadToken();
|
||||
const manga = await fetch(
|
||||
`/api/download/${file.encryptedId}?token=${token}`,
|
||||
const streamURL = new URL(
|
||||
`/api/stream/${file.encryptedId}`,
|
||||
config.basePath,
|
||||
);
|
||||
const archiveBlob = await manga.blob();
|
||||
const zipData = await JSZip.loadAsync(archiveBlob);
|
||||
streamURL.searchParams.set("token", token);
|
||||
|
||||
const tempArray: { name: string; blob: string }[] = [];
|
||||
const files = Object.values(zipData.files);
|
||||
for (const file of files) {
|
||||
const f = zipData.file(file.name);
|
||||
if (!f) continue;
|
||||
const blob = await f.async("blob");
|
||||
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);
|
||||
const bufferStream = await fetch(streamURL, {
|
||||
signal: abortController.current.signal,
|
||||
headers: {
|
||||
Range: `bytes=0-${
|
||||
Math.min(Number(file.size || 1), loadFirstX * 1024 * 1024) - 1
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
const contentLength = bufferStream.headers.get("Content-Length");
|
||||
const totalBytes = parseInt(contentLength || "0", 10);
|
||||
|
||||
const reader = bufferStream.body?.getReader();
|
||||
if (reader) {
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedLength = 0;
|
||||
|
||||
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) {
|
||||
const e = error as Error;
|
||||
@@ -74,7 +140,12 @@ export default function PreviewManga({ file }: Props) {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [file]);
|
||||
|
||||
// return () => {
|
||||
// abortController.current.abort("Cancelled by user");
|
||||
// };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
@@ -99,7 +170,31 @@ export default function PreviewManga({ file }: Props) {
|
||||
size={32}
|
||||
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>
|
||||
) : error ? (
|
||||
<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>
|
||||
</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'>
|
||||
<Carousel
|
||||
className={cn(
|
||||
@@ -124,24 +219,28 @@ export default function PreviewManga({ file }: Props) {
|
||||
setApi={setApi}
|
||||
>
|
||||
<CarouselContent className='h-full w-full'>
|
||||
{images.map((image, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className='flex h-full flex-col items-center justify-center'
|
||||
>
|
||||
<img
|
||||
src={image.blob}
|
||||
alt={`${image.name} - Page ${index + 1}`}
|
||||
className={cn(
|
||||
"w-full object-contain",
|
||||
viewSize === "fit" ? "h-full max-h-[70dvh]" : "h-full",
|
||||
)}
|
||||
/>
|
||||
<span className='muted text-center text-xs'>
|
||||
{image.name}
|
||||
</span>
|
||||
</CarouselItem>
|
||||
))}
|
||||
{images
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((image, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className='flex h-full flex-col items-center justify-center'
|
||||
>
|
||||
<img
|
||||
src={image.blob}
|
||||
alt={`${image.name} - Page ${index + 1}`}
|
||||
className={cn(
|
||||
"w-full object-contain",
|
||||
viewSize === "fit"
|
||||
? "h-full max-h-[70dvh]"
|
||||
: "h-full",
|
||||
)}
|
||||
/>
|
||||
<span className='muted text-center text-xs'>
|
||||
{image.name}
|
||||
</span>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
{isDesktop ? (
|
||||
<>
|
||||
@@ -151,11 +250,27 @@ export default function PreviewManga({ file }: Props) {
|
||||
) : null}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-end gap-1.5'>
|
||||
<span className='muted text-sm'>
|
||||
<div className='flex w-full items-center justify-end gap-3'>
|
||||
<span className='text-primary-foreground'>
|
||||
{currentImage}/{images.length}
|
||||
</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'
|
||||
onClick={() => setViewSize(viewSize === "fit" ? "full" : "fit")}
|
||||
>
|
||||
@@ -163,8 +278,24 @@ export default function PreviewManga({ file }: Props) {
|
||||
name={viewSize === "fit" ? "Maximize" : "Minimize"}
|
||||
size={14}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function PreviewRich({ file, code, view }: Props) {
|
||||
// if (code) {
|
||||
// setContent(`\`\`\`${file.fileExtension}\n${text}\`\`\``);
|
||||
// } else {
|
||||
setContent(text);
|
||||
setContent(text.trim());
|
||||
// }
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
@@ -91,7 +91,7 @@ export default function PreviewRich({ file, code, view }: Props) {
|
||||
<Markdown
|
||||
content={
|
||||
code && view === "markdown"
|
||||
? `\`\`\`${file.fileExtension}\n${content}\`\`\``
|
||||
? `\`\`\`${file.fileExtension}\n${content}`
|
||||
: content
|
||||
}
|
||||
view={view}
|
||||
@@ -99,23 +99,26 @@ export default function PreviewRich({ file, code, view }: Props) {
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 z-10 flex w-full items-center justify-center py-3 transition",
|
||||
expand
|
||||
? "pointer-events-none opacity-0"
|
||||
: "pointer-events-auto opacity-100",
|
||||
"bottom-0 z-10 flex w-full items-center justify-center py-3 transition",
|
||||
expand ? "relative" : "absolute",
|
||||
// expand
|
||||
// ? "pointer-events-none opacity-0"
|
||||
// : "pointer-events-auto opacity-100",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
className='gap-1.5'
|
||||
onClick={() => {
|
||||
setExpand(true);
|
||||
setExpand((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name='ChevronDown'
|
||||
name={expand ? "ChevronUp" : "ChevronDown"}
|
||||
size={16}
|
||||
/>
|
||||
Expand
|
||||
{expand ? "Collapse" : "Expand"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+24
-24
@@ -12,6 +12,21 @@ import { CreateDownloadToken } from "./actions";
|
||||
|
||||
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 = {
|
||||
@@ -78,6 +93,7 @@ export default function PreviewVideo({ file }: Props) {
|
||||
size: file.size,
|
||||
},
|
||||
],
|
||||
poster: `/api/thumb/${file.encryptedId}?size=1000`,
|
||||
}}
|
||||
// crossOrigin='anonymous'
|
||||
options={{
|
||||
@@ -97,33 +113,17 @@ export default function PreviewVideo({ file }: Props) {
|
||||
"download",
|
||||
"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>
|
||||
// <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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user