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/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,69 +239,77 @@ 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.Button
|
<Menu.Root className='vds-menu'>
|
||||||
className='vds-menu-button vds-button'
|
<Menu.Button
|
||||||
aria-label='Settings'
|
className='vds-menu-button vds-button'
|
||||||
>
|
aria-label='Settings'
|
||||||
<MediaPlayerIcons.Menu.Settings />
|
>
|
||||||
</Menu.Button>
|
<MediaPlayerIcons.Menu.Settings />
|
||||||
<Menu.Items
|
</Menu.Button>
|
||||||
className='vds-menu-items'
|
<Menu.Items
|
||||||
placement={"top"}
|
className='vds-menu-items'
|
||||||
offset={0}
|
placement={"top"}
|
||||||
>
|
offset={0}
|
||||||
{/* Loop */}
|
>
|
||||||
<Menu.Root>
|
{/* Loop */}
|
||||||
<Menu.Button className='vds-menu-item'>
|
<Menu.Root>
|
||||||
<ChevronLeft className='vds-menu-close-icon' />
|
<Menu.Button className='vds-menu-item'>
|
||||||
<Icon
|
<ChevronLeft className='vds-menu-close-icon' />
|
||||||
name='Repeat'
|
<Icon
|
||||||
className='vds-icon'
|
name='Repeat'
|
||||||
/>
|
className='vds-icon'
|
||||||
<span className='vds-menu-item-label'>Loop</span>
|
/>
|
||||||
<span className='vds-menu-item-hint'>{isLoop ? "On" : "Off"}</span>
|
<span className='vds-menu-item-label'>Loop</span>
|
||||||
<ChevronRight className='vds-menu-open-icon' />
|
<span className='vds-menu-item-hint'>{isLoop ? "On" : "Off"}</span>
|
||||||
</Menu.Button>
|
<ChevronRight className='vds-menu-open-icon' />
|
||||||
<Menu.Content className='vds-menu-items'>
|
</Menu.Button>
|
||||||
<Menu.RadioGroup
|
<Menu.Content className='vds-menu-items'>
|
||||||
className='vds-radio-group'
|
<Menu.RadioGroup
|
||||||
value={String(isLoop)}
|
className='vds-radio-group'
|
||||||
>
|
value={String(isLoop)}
|
||||||
<Menu.Radio
|
|
||||||
className='vds-radio'
|
|
||||||
value='true'
|
|
||||||
onSelect={() => setIsLoop(true)}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Menu.Radio
|
||||||
name='Check'
|
className='vds-radio'
|
||||||
className='vds-icon'
|
value='true'
|
||||||
/>
|
onSelect={() => setIsLoop(true)}
|
||||||
<span className='vds-radio-label'>On</span>
|
>
|
||||||
</Menu.Radio>
|
<Icon
|
||||||
<Menu.Radio
|
name='Check'
|
||||||
className='vds-radio'
|
className='vds-icon'
|
||||||
value='false'
|
/>
|
||||||
onSelect={() => setIsLoop(false)}
|
<span className='vds-radio-label'>On</span>
|
||||||
>
|
</Menu.Radio>
|
||||||
<Icon
|
<Menu.Radio
|
||||||
name='Check'
|
className='vds-radio'
|
||||||
className='vds-icon'
|
value='false'
|
||||||
/>
|
onSelect={() => setIsLoop(false)}
|
||||||
<span className='vds-radio-label'>Off</span>
|
>
|
||||||
</Menu.Radio>
|
<Icon
|
||||||
</Menu.RadioGroup>
|
name='Check'
|
||||||
</Menu.Content>
|
className='vds-icon'
|
||||||
</Menu.Root>
|
/>
|
||||||
|
<span className='vds-radio-label'>Off</span>
|
||||||
|
</Menu.Radio>
|
||||||
|
</Menu.RadioGroup>
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Root>
|
||||||
|
|
||||||
{/* Playback Speed */}
|
{/* Playback Speed */}
|
||||||
<PlaybackMenu />
|
<PlaybackMenu />
|
||||||
|
|
||||||
{/* Audio Gain */}
|
{/* Audio Gain */}
|
||||||
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user