mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
feat: Add Vidstack replacing Plyr
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState } from "react";
|
||||
import "react-h5-audio-player/lib/styles.css";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Loader, Status } from "~/components/global";
|
||||
|
||||
import useLoading from "~/hooks/useLoading";
|
||||
|
||||
import { Schema_File } from "~/types/schema";
|
||||
|
||||
import { CreateDownloadToken } from "actions";
|
||||
|
||||
const Plyr = dynamic(() => import("plyr-react"), {
|
||||
ssr: false,
|
||||
loading: () => <Loader message='Loading player...' />,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
};
|
||||
export default function PreviewAudio({ file }: Props) {
|
||||
const [audioSrc, setAudioSrc] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const loading = useLoading(async () => {
|
||||
try {
|
||||
if (!file.encryptedWebContentLink) {
|
||||
setError("No audio to preview");
|
||||
return;
|
||||
}
|
||||
const token = await CreateDownloadToken();
|
||||
setAudioSrc(`/api/stream/${file.encryptedId}?token=${token}`);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
setError(e.message);
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div className='flex h-full min-h-[33dvh] w-full items-center justify-center py-3'>
|
||||
{loading ? (
|
||||
<Loader message='Loading audio...' />
|
||||
) : error ? (
|
||||
<Status
|
||||
icon='TriangleAlert'
|
||||
message={error}
|
||||
destructive
|
||||
/>
|
||||
) : (
|
||||
<div className='w-full'>
|
||||
<Plyr
|
||||
source={{
|
||||
type: "audio",
|
||||
sources: [
|
||||
{
|
||||
src: audioSrc,
|
||||
type: file.mimeType,
|
||||
size: file.size,
|
||||
},
|
||||
],
|
||||
title: file.name,
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type AudioMimeType,
|
||||
MediaPlayer,
|
||||
type MediaPlayerInstance,
|
||||
type MediaPlayerQuery,
|
||||
MediaProvider,
|
||||
Menu,
|
||||
PlayButton,
|
||||
SeekButton,
|
||||
Time,
|
||||
TimeSlider,
|
||||
type VideoMimeType,
|
||||
useAudioGainOptions,
|
||||
usePlaybackRateOptions,
|
||||
} from "@vidstack/react";
|
||||
import { DefaultAudioLayout, DefaultVideoLayout, defaultLayoutIcons } from "@vidstack/react/player/layouts/default";
|
||||
import "@vidstack/react/player/styles/base.css";
|
||||
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 { useCallback, useRef, useState } from "react";
|
||||
import { type z } from "zod";
|
||||
|
||||
import Icon from "~/components/ui/icon";
|
||||
|
||||
import useLoading from "~/hooks/useLoading";
|
||||
|
||||
import { type Schema_File } from "~/types/schema";
|
||||
|
||||
import "~/styles/vidstack.css";
|
||||
|
||||
import { PageLoader } from "../layout";
|
||||
|
||||
export const MediaPlayerIcons = {
|
||||
...defaultLayoutIcons,
|
||||
AirPlayButton: {
|
||||
Default: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Airplay'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Connecting: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='LoaderCircle'
|
||||
className='vds-icon size-5 animate-spin'
|
||||
/>
|
||||
),
|
||||
Connected: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Airplay'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
GoogleCastButton: {
|
||||
Default: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Cast'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Connecting: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='LoaderCircle'
|
||||
className='vds-icon size-5 animate-spin'
|
||||
/>
|
||||
),
|
||||
Connected: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Cast'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
PlayButton: {
|
||||
Play: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Play'
|
||||
className='vds-icon size-5 fill-current media-ended:hidden media-playing:hidden'
|
||||
/>
|
||||
),
|
||||
Pause: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Pause'
|
||||
className='vds-icon size-5 fill-current media-paused:hidden'
|
||||
/>
|
||||
),
|
||||
Replay: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='RotateCcw'
|
||||
className='vds-icon hidden size-5 media-ended:block'
|
||||
/>
|
||||
),
|
||||
},
|
||||
MuteButton: {
|
||||
Mute: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='VolumeX'
|
||||
className='vds-icon mute-icon size-5'
|
||||
/>
|
||||
),
|
||||
VolumeLow: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume1'
|
||||
className='vds-icon volume-low-icon size-5'
|
||||
/>
|
||||
),
|
||||
VolumeHigh: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume2'
|
||||
className='vds-icon volume-high-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
CaptionButton: {
|
||||
On: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Captions'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Off: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='CaptionsOff'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
PIPButton: {
|
||||
Enter: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='PictureInPicture'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Exit: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='X'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
FullscreenButton: {
|
||||
Enter: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Maximize2'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Exit: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Minimize2'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
SeekButton: {
|
||||
Backward: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsLeft'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Forward: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsRight'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
DownloadButton: {
|
||||
Default: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Download'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
},
|
||||
Menu: {
|
||||
Accessibility: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='PersonStanding'
|
||||
className='vds-icon mr-2 size-5'
|
||||
/>
|
||||
),
|
||||
ArrowLeft: () => null,
|
||||
ArrowRight: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronRight'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Audio: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume2'
|
||||
className='vds-icon mr-2 size-5'
|
||||
/>
|
||||
),
|
||||
AudioBoostUp: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume2'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
AudioBoostDown: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Chapters: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='TableOfContents'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Captions: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Captions'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
Playback: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ListVideo'
|
||||
className='size5 mr-2 '
|
||||
/>
|
||||
),
|
||||
Settings: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Settings'
|
||||
className='vds-icon vds-rotate-icon size-5'
|
||||
/>
|
||||
),
|
||||
SpeedUp: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsUp'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
SpeedDown: () => (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsDown'
|
||||
className='vds-icon size-5'
|
||||
/>
|
||||
),
|
||||
QualityUp: () => null,
|
||||
QualityDown: () => null,
|
||||
FontSizeUp: () => null,
|
||||
FontSizeDown: () => null,
|
||||
OpacityUp: () => null,
|
||||
OpacityDown: () => null,
|
||||
RadioCheck: () => null,
|
||||
},
|
||||
KeyboardDisplay: {
|
||||
Play: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Play'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
Pause: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Pause'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
Mute: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='VolumeX'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
VolumeUp: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume2'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
VolumeDown: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Volume1'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
EnterFullscreen: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Maximize2'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
ExitFullscreen: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Minimize2'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
// EnterPiP: () => null,
|
||||
// ExitPiP: () => null,
|
||||
CaptionsOn: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='Captions'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
CaptionsOff: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='CaptionsOff'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
SeekForward: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsRight'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
SeekBackward: () => (
|
||||
<div className='grid h-full w-full place-items-center'>
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='ChevronsLeft'
|
||||
className='size-6'
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
type: "video" | "audio";
|
||||
};
|
||||
export default function PreviewMedia({ file, type }: Props) {
|
||||
const loading = useLoading();
|
||||
const player = useRef<MediaPlayerInstance>(null);
|
||||
const [canPlay, setCanPlay] = useState<boolean>(false);
|
||||
|
||||
const [isLoop, setIsLoop] = useState<boolean>(false);
|
||||
|
||||
const smallAudioLayoutQuery = useCallback<MediaPlayerQuery>(({ width }) => {
|
||||
return width < 576;
|
||||
}, []);
|
||||
const smallVideoLayoutQuery = useCallback<MediaPlayerQuery>(({ width, height }) => {
|
||||
return width < 576 || height < 380;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-full items-center justify-center py-3'>
|
||||
{loading ? (
|
||||
<PageLoader message='Loading media...' />
|
||||
) : (
|
||||
<MediaPlayer
|
||||
ref={player}
|
||||
key={file.encryptedId}
|
||||
src={{
|
||||
src: `/api/preview/${file.encryptedId}`,
|
||||
type: file.mimeType as AudioMimeType | VideoMimeType,
|
||||
}}
|
||||
loop={isLoop}
|
||||
autoPlay
|
||||
playsInline
|
||||
crossOrigin
|
||||
viewType={type === "audio" ? "audio" : "video"}
|
||||
className='media-player h-full w-full rounded-xl'
|
||||
preload='auto'
|
||||
onLoadedMetadata={() => {
|
||||
setCanPlay(true);
|
||||
}}
|
||||
>
|
||||
<MediaProvider />
|
||||
<DefaultAudioLayout
|
||||
icons={MediaPlayerIcons}
|
||||
colorScheme='default'
|
||||
smallLayoutWhen={smallAudioLayoutQuery}
|
||||
showTooltipDelay={150}
|
||||
className='h-fit !px-2 !py-4 shadow-md [&.vds-button>svg]:stroke-[--media-controls-color] [&.vds-play-button>svg]:fill-[--audio-play-button-color] [&.vds-play-button>svg]:stroke-[--audio-play-button-color] [&>.vds-captions]:hidden [&>.vds-controls>.vds-controls-group]:inline-block [&>.vds-controls>.vds-controls-group]:md:inline-flex [&>.vds-controls>.vds-controls-group]:md:flex-row [&>.vds-controls>.vds-controls-group]:md:gap-1'
|
||||
slots={{
|
||||
seekBackwardButton: null,
|
||||
seekForwardButton: null,
|
||||
playButton: (
|
||||
<div className='flex w-full items-center justify-center gap-2 md:w-fit'>
|
||||
<SeekButton
|
||||
className='vds-button'
|
||||
seconds={-10}
|
||||
>
|
||||
<MediaPlayerIcons.SeekButton.Backward />
|
||||
</SeekButton>
|
||||
<PlayButton
|
||||
className='vds-button vds-play-button'
|
||||
disabled={canPlay === false}
|
||||
>
|
||||
{canPlay ? (
|
||||
<>
|
||||
<MediaPlayerIcons.PlayButton.Play />
|
||||
<MediaPlayerIcons.PlayButton.Pause />
|
||||
<MediaPlayerIcons.PlayButton.Replay />
|
||||
</>
|
||||
) : (
|
||||
<Icon
|
||||
hideWrapper
|
||||
name='LoaderCircle'
|
||||
className='vds-icon size-5 animate-spin'
|
||||
/>
|
||||
)}
|
||||
</PlayButton>
|
||||
<SeekButton
|
||||
className='vds-button'
|
||||
seconds={10}
|
||||
>
|
||||
<MediaPlayerIcons.SeekButton.Forward />
|
||||
</SeekButton>
|
||||
</div>
|
||||
),
|
||||
timeSlider: (
|
||||
<div className='grid w-auto grow place-items-center md:w-full'>
|
||||
<TimeSlider.Root className='vds-time-slider vds-slider !opacity-100'>
|
||||
<TimeSlider.Track className='vds-slider-track'>
|
||||
<TimeSlider.Progress className='vds-slider-progress' />
|
||||
<TimeSlider.TrackFill className='vds-slider-track-fill vds-slider-track' />
|
||||
</TimeSlider.Track>
|
||||
</TimeSlider.Root>
|
||||
</div>
|
||||
),
|
||||
beforeEndTime: <div className='vds-controls-spacer' />,
|
||||
endTime: (
|
||||
<div className='inline-flex w-full items-center justify-between gap-2 md:w-fit'>
|
||||
<div className='inline-flex items-center text-sm'>
|
||||
<Time
|
||||
className='time'
|
||||
type='current'
|
||||
/>
|
||||
<span className='mx-1'>/</span>
|
||||
<Time
|
||||
className='time'
|
||||
type='duration'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<Menu.Root className='vds-menu'>
|
||||
<Menu.Button
|
||||
className='vds-menu-button vds-button'
|
||||
aria-label='Settings'
|
||||
>
|
||||
<MediaPlayerIcons.Menu.Settings />
|
||||
</Menu.Button>
|
||||
<Menu.Items
|
||||
className='vds-menu-items'
|
||||
placement={"bottom"}
|
||||
offset={0}
|
||||
>
|
||||
{/* Loop */}
|
||||
<Menu.Root>
|
||||
<Menu.Button className='vds-menu-item'>
|
||||
<ChevronLeft className='vds-menu-close-icon' />
|
||||
<Icon
|
||||
name='Repeat'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-menu-item-label'>Loop</span>
|
||||
<span className='vds-menu-item-hint'>{isLoop ? "On" : "Off"}</span>
|
||||
<ChevronRight className='vds-menu-open-icon' />
|
||||
</Menu.Button>
|
||||
<Menu.Content className='vds-menu-items'>
|
||||
<Menu.RadioGroup
|
||||
className='vds-radio-group'
|
||||
value={String(isLoop)}
|
||||
>
|
||||
<Menu.Radio
|
||||
className='vds-radio'
|
||||
value='true'
|
||||
onSelect={() => setIsLoop(true)}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label'>On</span>
|
||||
</Menu.Radio>
|
||||
<Menu.Radio
|
||||
className='vds-radio'
|
||||
value='false'
|
||||
onSelect={() => setIsLoop(false)}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label'>Off</span>
|
||||
</Menu.Radio>
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
|
||||
{/* Playback Speed */}
|
||||
<PlaybackMenu />
|
||||
|
||||
{/* Audio Gain */}
|
||||
<AudioGain />
|
||||
</Menu.Items>
|
||||
</Menu.Root>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
settingsMenu: null,
|
||||
}}
|
||||
/>
|
||||
<DefaultVideoLayout
|
||||
icons={MediaPlayerIcons}
|
||||
colorScheme='default'
|
||||
smallLayoutWhen={smallVideoLayoutQuery}
|
||||
showTooltipDelay={150}
|
||||
slots={{
|
||||
currentTime: (
|
||||
<Time
|
||||
className='text-sm'
|
||||
type='current'
|
||||
/>
|
||||
),
|
||||
endTime: (
|
||||
<Time
|
||||
className='text-sm'
|
||||
type='duration'
|
||||
/>
|
||||
),
|
||||
settingsMenu: (
|
||||
<Menu.Root className='vds-menu'>
|
||||
<Menu.Button
|
||||
className='vds-menu-button vds-button'
|
||||
aria-label='Settings'
|
||||
>
|
||||
<MediaPlayerIcons.Menu.Settings />
|
||||
</Menu.Button>
|
||||
<Menu.Items
|
||||
className='vds-menu-items'
|
||||
placement={"top"}
|
||||
offset={0}
|
||||
>
|
||||
{/* Loop */}
|
||||
<Menu.Root>
|
||||
<Menu.Button className='vds-menu-item'>
|
||||
<ChevronLeft className='vds-menu-close-icon' />
|
||||
<Icon
|
||||
name='Repeat'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-menu-item-label'>Loop</span>
|
||||
<span className='vds-menu-item-hint'>{isLoop ? "On" : "Off"}</span>
|
||||
<ChevronRight className='vds-menu-open-icon' />
|
||||
</Menu.Button>
|
||||
<Menu.Content className='vds-menu-items'>
|
||||
<Menu.RadioGroup
|
||||
className='vds-radio-group'
|
||||
value={String(isLoop)}
|
||||
>
|
||||
<Menu.Radio
|
||||
className='vds-radio'
|
||||
value='true'
|
||||
onSelect={() => setIsLoop(true)}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label'>On</span>
|
||||
</Menu.Radio>
|
||||
<Menu.Radio
|
||||
className='vds-radio'
|
||||
value='false'
|
||||
onSelect={() => setIsLoop(false)}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label'>Off</span>
|
||||
</Menu.Radio>
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
|
||||
{/* Playback Speed */}
|
||||
<PlaybackMenu />
|
||||
|
||||
{/* Audio Gain */}
|
||||
<AudioGain />
|
||||
</Menu.Items>
|
||||
</Menu.Root>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</MediaPlayer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaybackMenu() {
|
||||
const options = usePlaybackRateOptions();
|
||||
const hint = options.selectedValue === "1" ? "Normal" : `${options.selectedValue}x`;
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Button
|
||||
className='vds-menu-item'
|
||||
disabled={options.disabled}
|
||||
>
|
||||
<ChevronLeft className='vds-menu-close-icon' />
|
||||
<Icon
|
||||
name='Gauge'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-menu-item-label'>Playback Speed</span>
|
||||
<span className='vds-menu-item-hint'>{hint}</span>
|
||||
<ChevronRight className='vds-menu-open-icon' />
|
||||
</Menu.Button>
|
||||
<Menu.Content className='vds-menu-items'>
|
||||
<Menu.RadioGroup
|
||||
className='vds-radio-group'
|
||||
value={options.selectedValue}
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/unbound-method */}
|
||||
{options.map(({ value, select, label }) => (
|
||||
<Menu.Radio
|
||||
key={value}
|
||||
className='vds-radio'
|
||||
value={value}
|
||||
onSelect={select}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label'>{label}</span>
|
||||
</Menu.Radio>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
function AudioGain() {
|
||||
const options = useAudioGainOptions({
|
||||
disabledLabel: "100%",
|
||||
});
|
||||
const hint = options.selectedValue ? `${Number(options.selectedValue) * 100}%` : "100%";
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Button
|
||||
className='vds-menu-item'
|
||||
disabled={options.disabled}
|
||||
>
|
||||
<ChevronLeft className='vds-menu-close-icon' />
|
||||
<Icon
|
||||
name='Volume2'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-menu-item-label'>Audio Boost</span>
|
||||
<span className='vds-menu-item-hint'>{hint}</span>
|
||||
<ChevronRight className='vds-menu-open-icon' />
|
||||
</Menu.Button>
|
||||
<Menu.Content className='vds-menu-items'>
|
||||
<Menu.RadioGroup
|
||||
className='vds-radio-group'
|
||||
value={options.selectedValue}
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/unbound-method */}
|
||||
{options.map(({ value, select, label }) => (
|
||||
<Menu.Radio
|
||||
key={value}
|
||||
className='vds-radio'
|
||||
value={value}
|
||||
onSelect={select}
|
||||
>
|
||||
<Icon
|
||||
name='Check'
|
||||
className='vds-icon'
|
||||
/>
|
||||
<span className='vds-radio-label capitalize'>{label}</span>
|
||||
</Menu.Radio>
|
||||
))}
|
||||
</Menu.RadioGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Loader, Status } from "~/components/global";
|
||||
|
||||
import useLoading from "~/hooks/useLoading";
|
||||
|
||||
import { Schema_File } from "~/types/schema";
|
||||
|
||||
import { CreateDownloadToken } from "actions";
|
||||
|
||||
const Plyr = dynamic(() => import("plyr-react"), {
|
||||
ssr: false,
|
||||
loading: () => <Loader message='Loading player...' />,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
file: z.infer<typeof Schema_File>;
|
||||
};
|
||||
export default function PreviewVideo({ file }: Props) {
|
||||
const [videoSrc, setVideoSrc] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const loading = useLoading(async () => {
|
||||
try {
|
||||
if (!file.encryptedWebContentLink) {
|
||||
setError("No video to preview");
|
||||
return;
|
||||
}
|
||||
const token = await CreateDownloadToken();
|
||||
setVideoSrc(`/api/stream/${file.encryptedId}?token=${token}`);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
setError(e.message);
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div className='flex h-full min-h-[33dvh] w-full items-center justify-center py-3'>
|
||||
{loading ? (
|
||||
<Loader message='Loading video...' />
|
||||
) : error ? (
|
||||
<Status
|
||||
icon='TriangleAlert'
|
||||
message={error}
|
||||
destructive
|
||||
/>
|
||||
) : (
|
||||
<div className='h-full w-full'>
|
||||
<Plyr
|
||||
source={{
|
||||
type: "video",
|
||||
sources: [
|
||||
{
|
||||
src: videoSrc,
|
||||
type: file.mimeType,
|
||||
size: file.size,
|
||||
},
|
||||
],
|
||||
poster: `/api/thumb/${file.encryptedId}?size=1000`,
|
||||
}}
|
||||
options={{
|
||||
toggleInvert: true,
|
||||
settings: ["quality", "speed"],
|
||||
ratio: "16:9",
|
||||
controls: [
|
||||
"play-large",
|
||||
"play",
|
||||
"progress",
|
||||
"current-time",
|
||||
"duration",
|
||||
"mute",
|
||||
"volume",
|
||||
"settings",
|
||||
"pip",
|
||||
"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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+9
-1
@@ -1,4 +1,5 @@
|
||||
import twTypography from "@tailwindcss/typography";
|
||||
import twVidstack from "@vidstack/react/tailwind.cjs";
|
||||
import type { Config } from "tailwindcss";
|
||||
import twAnimate from "tailwindcss-animate";
|
||||
import tw from "tailwindcss/defaultTheme";
|
||||
@@ -103,7 +104,14 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [twAnimate, twTypography],
|
||||
plugins: [
|
||||
twAnimate,
|
||||
twTypography,
|
||||
twVidstack({
|
||||
selector: ".media-player",
|
||||
prefix: "media",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user