mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
feat/playlist: Embed playlist to media player
This commit is contained in:
@@ -21,6 +21,7 @@ import "@vidstack/react/player/styles/default/layouts/audio.css";
|
||||
import "@vidstack/react/player/styles/default/layouts/video.css";
|
||||
import "@vidstack/react/player/styles/default/theme.css";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { type z } from "zod";
|
||||
|
||||
@@ -28,7 +29,8 @@ import { PageLoader } from "~/components/layout";
|
||||
import Icon from "~/components/ui/icon";
|
||||
|
||||
import useLoading from "~/hooks/useLoading";
|
||||
import { MediaPlayerIcons } from "~/lib/previewHelper";
|
||||
import { MediaPlayerIcons, getPreviewIcon } from "~/lib/previewHelper";
|
||||
import { cn, formatDate } from "~/lib/utils";
|
||||
|
||||
import { type Schema_File } from "~/types/schema";
|
||||
|
||||
@@ -37,8 +39,9 @@ import "~/styles/vidstack.css";
|
||||
type Props = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
type: "video" | "audio";
|
||||
playlist: z.infer<typeof Schema_File>[];
|
||||
};
|
||||
export default function PreviewMedia({ file, type }: Props) {
|
||||
export default function PreviewMedia({ file, type, playlist }: Props) {
|
||||
const player = useRef<MediaPlayerInstance>(null);
|
||||
const [canPlay, setCanPlay] = useState<boolean>(false);
|
||||
|
||||
@@ -85,7 +88,7 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
seekBackwardButton: null,
|
||||
seekForwardButton: null,
|
||||
playButton: (
|
||||
<div className='flex w-full items-center justify-center gap-2 md:w-fit'>
|
||||
<div className='flex w-full items-center justify-center gap-1 md:w-fit'>
|
||||
<SeekButton
|
||||
className='vds-button'
|
||||
seconds={-10}
|
||||
@@ -93,7 +96,7 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
<MediaPlayerIcons.SeekButton.Backward />
|
||||
</SeekButton>
|
||||
<PlayButton
|
||||
className='vds-button vds-play-button'
|
||||
className='vds-button vds-play-button aspect-square'
|
||||
disabled={canPlay === false}
|
||||
>
|
||||
{canPlay ? (
|
||||
@@ -144,6 +147,12 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<PlaylistMenu
|
||||
playlist={playlist}
|
||||
file={file}
|
||||
placement={"bottom end"}
|
||||
/>
|
||||
|
||||
<Menu.Root className='vds-menu'>
|
||||
<Menu.Button
|
||||
className='vds-menu-button vds-button'
|
||||
@@ -216,7 +225,7 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
icons={MediaPlayerIcons}
|
||||
colorScheme='default'
|
||||
smallLayoutWhen={smallVideoLayoutQuery}
|
||||
showTooltipDelay={150}
|
||||
showTooltipDelay={200}
|
||||
slots={{
|
||||
currentTime: (
|
||||
<Time
|
||||
@@ -230,7 +239,14 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
type='duration'
|
||||
/>
|
||||
),
|
||||
beforeSettingsMenu: (
|
||||
<PlaylistMenu
|
||||
playlist={playlist}
|
||||
file={file}
|
||||
/>
|
||||
),
|
||||
settingsMenu: (
|
||||
<>
|
||||
<Menu.Root className='vds-menu'>
|
||||
<Menu.Button
|
||||
className='vds-menu-button vds-button'
|
||||
@@ -293,6 +309,7 @@ export default function PreviewMedia({ file, type }: Props) {
|
||||
<AudioGain />
|
||||
</Menu.Items>
|
||||
</Menu.Root>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -392,3 +409,87 @@ function AudioGain() {
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
function PlaylistMenu({
|
||||
playlist,
|
||||
file,
|
||||
placement = "top end",
|
||||
}: {
|
||||
playlist: z.infer<typeof Schema_File>[];
|
||||
file: z.infer<typeof Schema_File>;
|
||||
placement?: Menu.ItemsProps["placement"];
|
||||
}) {
|
||||
return (
|
||||
<Menu.Root className='vds-menu'>
|
||||
<Menu.Button
|
||||
className='vds-menu-button vds-button'
|
||||
aria-label='Playlist'
|
||||
>
|
||||
<MediaPlayerIcons.Menu.Chapters />
|
||||
</Menu.Button>
|
||||
<Menu.Items
|
||||
className='vds-menu-items group space-y-1'
|
||||
placement={placement}
|
||||
>
|
||||
{playlist.map((item) => {
|
||||
const isCurrent = `${item.name}#${item.size}` === `${file.name}#${file.size}`;
|
||||
|
||||
const Wrapper = ({ children, className }: React.PropsWithChildren<{ className?: string }>) =>
|
||||
isCurrent ? (
|
||||
<div
|
||||
className={className}
|
||||
title={item.name}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={`${item.name}`}
|
||||
className={className}
|
||||
title={item.name}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
key={`playlist-${item.encryptedId}`}
|
||||
className={cn(
|
||||
"flex w-full max-w-96 grid-cols-4 place-items-center gap-2 rounded-lg",
|
||||
isCurrent ? "bg-primary/10" : "hover:bg-primary/5",
|
||||
)}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div
|
||||
className={cn(
|
||||
"aspect-video h-16 grow-0 overflow-hidden rounded-lg bg-black",
|
||||
isCurrent && "border border-primary",
|
||||
)}
|
||||
>
|
||||
{item.mimeType.includes("video") ? (
|
||||
<img
|
||||
src={`/api/thumb/${item.encryptedId}`}
|
||||
alt={`Thumbnail for ${item.name}`}
|
||||
className='aspect-video w-full object-contain'
|
||||
/>
|
||||
) : (
|
||||
<div className={"grid aspect-video w-full place-items-center"}>
|
||||
<Icon
|
||||
name={getPreviewIcon(item.fileExtension ?? "", item.mimeType)}
|
||||
className={"size-6"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={"col-span-3 w-full flex-1 p-2"}>
|
||||
<p className={"line-clamp-1 break-all"}>{item.name}</p>
|
||||
<span className={"text-sm text-muted-foreground"}>{formatDate(item.modifiedTime)}</span>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
})}
|
||||
</Menu.Items>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user