Update preview logic

This commit is contained in:
mbaharip
2024-05-08 08:55:40 +07:00
parent 3978928995
commit c9431dcffc
7 changed files with 447 additions and 151 deletions
+59 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+12 -9
View File
@@ -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
View File
@@ -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>
); );