aboutsummaryrefslogtreecommitdiff
path: root/components/watch/new-player
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-12-24 13:03:54 +0700
committerFactiven <[email protected]>2023-12-24 13:03:54 +0700
commit50a0f0240d7fef133eb5acc1bea2b1168b08e9db (patch)
tree307e09e505580415a58d64b5fc3580e9235869f1 /components/watch/new-player
parentUpdate README.md (#104) (diff)
downloadmoopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.tar.xz
moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.zip
migrate to typescript
Diffstat (limited to 'components/watch/new-player')
-rw-r--r--components/watch/new-player/components/bufferingIndicator.tsx15
-rw-r--r--components/watch/new-player/components/buttons.tsx277
-rw-r--r--components/watch/new-player/components/chapter-title.tsx11
-rw-r--r--components/watch/new-player/components/layouts/captions.module.css80
-rw-r--r--components/watch/new-player/components/layouts/video-layout.module.css13
-rw-r--r--components/watch/new-player/components/layouts/video-layout.tsx173
-rw-r--r--components/watch/new-player/components/menus.tsx387
-rw-r--r--components/watch/new-player/components/sliders.tsx73
-rw-r--r--components/watch/new-player/components/time-group.tsx11
-rw-r--r--components/watch/new-player/components/title.tsx35
-rw-r--r--components/watch/new-player/player.module.css50
-rw-r--r--components/watch/new-player/player.tsx471
-rw-r--r--components/watch/new-player/tracks.tsx184
13 files changed, 1780 insertions, 0 deletions
diff --git a/components/watch/new-player/components/bufferingIndicator.tsx b/components/watch/new-player/components/bufferingIndicator.tsx
new file mode 100644
index 0000000..4793d55
--- /dev/null
+++ b/components/watch/new-player/components/bufferingIndicator.tsx
@@ -0,0 +1,15 @@
+import { Spinner } from "@vidstack/react";
+
+export default function BufferingIndicator() {
+ return (
+ <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center">
+ <Spinner.Root
+ className="text-white opacity-0 transition-opacity duration-200 ease-linear media-buffering:animate-spin media-buffering:opacity-100"
+ size={84}
+ >
+ <Spinner.Track className="opacity-25" width={8} />
+ <Spinner.TrackFill className="opacity-75" width={8} />
+ </Spinner.Root>
+ </div>
+ );
+}
diff --git a/components/watch/new-player/components/buttons.tsx b/components/watch/new-player/components/buttons.tsx
new file mode 100644
index 0000000..18c2b42
--- /dev/null
+++ b/components/watch/new-player/components/buttons.tsx
@@ -0,0 +1,277 @@
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import {
+ CaptionButton,
+ FullscreenButton,
+ isTrackCaptionKind,
+ MuteButton,
+ PIPButton,
+ PlayButton,
+ Tooltip,
+ useMediaState,
+ type TooltipPlacement,
+ useMediaRemote,
+ useMediaStore,
+} from "@vidstack/react";
+import {
+ ClosedCaptionsIcon,
+ ClosedCaptionsOnIcon,
+ FullscreenExitIcon,
+ FullscreenIcon,
+ MuteIcon,
+ PauseIcon,
+ PictureInPictureExitIcon,
+ PictureInPictureIcon,
+ PlayIcon,
+ ReplayIcon,
+ TheatreModeExitIcon,
+ TheatreModeIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+} from "@vidstack/react/icons";
+import { useRouter } from "next/router";
+import { Navigation } from "../player";
+
+export interface MediaButtonProps {
+ tooltipPlacement: TooltipPlacement;
+ navigation?: Navigation;
+ host?: boolean;
+}
+
+export const buttonClass =
+ "group ring-media-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 data-[focus]:ring-4";
+
+export const tooltipClass =
+ "animate-out fade-out slide-out-to-bottom-2 data-[visible]:animate-in data-[visible]:fade-in data-[visible]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden";
+
+export function Play({ tooltipPlacement }: MediaButtonProps) {
+ const isPaused = useMediaState("paused"),
+ ended = useMediaState("ended"),
+ tooltipText = isPaused ? "Play" : "Pause",
+ Icon = ended ? ReplayIcon : isPaused ? PlayIcon : PauseIcon;
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PlayButton className={buttonClass}>
+ <Icon className="w-8 h-8" />
+ </PlayButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {tooltipText}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function MobilePlayButton({ tooltipPlacement, host }: MediaButtonProps) {
+ const isPaused = useMediaState("paused"),
+ ended = useMediaState("ended"),
+ Icon = ended ? ReplayIcon : isPaused ? PlayIcon : PauseIcon;
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PlayButton
+ className={`${
+ host ? "" : "pointer-events-none"
+ } group ring-media-focus relative inline-flex h-16 w-16 media-paused:cursor-pointer cursor-default items-center justify-center rounded-full outline-none`}
+ >
+ <Icon className="w-10 h-10" />
+ </PlayButton>
+ </Tooltip.Trigger>
+ {/* <Tooltip.Content
+ className="animate-out fade-out slide-out-to-bottom-2 data-[visible]:animate-in data-[visible]:fade-in data-[visible]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden"
+ placement={tooltipPlacement}
+ >
+ {tooltipText}
+ </Tooltip.Content> */}
+ </Tooltip.Root>
+ );
+}
+
+export function Mute({ tooltipPlacement }: MediaButtonProps) {
+ const volume = useMediaState("volume"),
+ isMuted = useMediaState("muted");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <MuteButton className={buttonClass}>
+ {isMuted || volume == 0 ? (
+ <MuteIcon className="w-8 h-8" />
+ ) : volume < 0.5 ? (
+ <VolumeLowIcon className="w-8 h-8" />
+ ) : (
+ <VolumeHighIcon className="w-8 h-8" />
+ )}
+ </MuteButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isMuted ? "Unmute" : "Mute"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function Caption({ tooltipPlacement }: MediaButtonProps) {
+ const track = useMediaState("textTrack"),
+ isOn = track && isTrackCaptionKind(track);
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <CaptionButton className={buttonClass}>
+ {isOn ? (
+ <ClosedCaptionsOnIcon className="w-8 h-8" />
+ ) : (
+ <ClosedCaptionsIcon className="w-8 h-8" />
+ )}
+ </CaptionButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isOn ? "Closed-Captions On" : "Closed-Captions Off"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function TheaterButton({ tooltipPlacement }: MediaButtonProps) {
+ const playerState = useMediaState("currentTime"),
+ isPlaying = useMediaState("playing");
+
+ const { setPlayerState, setTheaterMode, theaterMode } = useWatchProvider();
+
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <button
+ type="button"
+ className={buttonClass}
+ onClick={() => {
+ setPlayerState((prev: any) => ({
+ ...prev,
+ currentTime: playerState,
+ isPlaying: isPlaying,
+ }));
+ setTheaterMode((prev: any) => !prev);
+ }}
+ >
+ {!theaterMode ? (
+ <TheatreModeIcon className="w-8 h-8" />
+ ) : (
+ <TheatreModeExitIcon className="w-8 h-8" />
+ )}
+ </button>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ Theatre Mode
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function PIP({ tooltipPlacement }: MediaButtonProps) {
+ const isActive = useMediaState("pictureInPicture");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PIPButton className={buttonClass}>
+ {isActive ? (
+ <PictureInPictureExitIcon className="w-8 h-8" />
+ ) : (
+ <PictureInPictureIcon className="w-8 h-8" />
+ )}
+ </PIPButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isActive ? "Exit PIP" : "Enter PIP"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function PlayNextButton({
+ tooltipPlacement,
+ navigation,
+}: MediaButtonProps) {
+ // const remote = useMediaRemote();
+ const router = useRouter();
+ const { dataMedia, track } = useWatchProvider();
+ return (
+ <button
+ title="next-button"
+ type="button"
+ onClick={() => {
+ if (navigation?.next) {
+ router.push(
+ `/en/anime/watch/${dataMedia.id}/${track.provider}?id=${
+ navigation?.next?.id
+ }&num=${navigation?.next?.number}${
+ track?.isDub ? `&dub=${track?.isDub}` : ""
+ }`
+ );
+ }
+ }}
+ className="next-button hidden"
+ >
+ Next Episode
+ </button>
+ );
+}
+
+export function SkipOpButton({ tooltipPlacement }: MediaButtonProps) {
+ const remote = useMediaRemote();
+ const { track } = useWatchProvider();
+ const op = track?.skip?.find((item: any) => item.text === "Opening");
+
+ return (
+ <button
+ type="button"
+ onClick={() => {
+ remote.seek(op?.endTime);
+ }}
+ className="op-button hidden hover:bg-white/80 bg-white px-4 py-2 text-primary font-karla font-semibold rounded-md"
+ >
+ Skip Opening
+ </button>
+ );
+}
+
+export function SkipEdButton({ tooltipPlacement }: MediaButtonProps) {
+ const remote = useMediaRemote();
+ const { duration } = useMediaStore();
+ const { track } = useWatchProvider();
+ const ed = track?.skip?.find((item: any) => item.text === "Ending");
+
+ const endTime =
+ Math.round(duration) === ed?.endTime ? ed?.endTime - 1 : ed?.endTime;
+
+ // console.log(endTime);
+
+ return (
+ <button
+ title="ed-button"
+ type="button"
+ onClick={() => remote.seek(endTime)}
+ className="ed-button hidden cursor-pointer hover:bg-white/80 bg-white px-4 py-2 text-primary font-karla font-semibold rounded-md"
+ >
+ Skip Ending
+ </button>
+ );
+}
+
+export function Fullscreen({ tooltipPlacement }: MediaButtonProps) {
+ const isActive = useMediaState("fullscreen");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <FullscreenButton className={buttonClass}>
+ {isActive ? (
+ <FullscreenExitIcon className="w-8 h-8" />
+ ) : (
+ <FullscreenIcon className="w-8 h-8" />
+ )}
+ </FullscreenButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isActive ? "Exit Fullscreen" : "Enter Fullscreen"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
diff --git a/components/watch/new-player/components/chapter-title.tsx b/components/watch/new-player/components/chapter-title.tsx
new file mode 100644
index 0000000..779f826
--- /dev/null
+++ b/components/watch/new-player/components/chapter-title.tsx
@@ -0,0 +1,11 @@
+import { ChapterTitle, type ChapterTitleProps } from "@vidstack/react";
+import { ChevronLeftIcon, ChevronRightIcon } from "@vidstack/react/icons";
+
+export function ChapterTitleComponent() {
+ return (
+ <span className="inline-block flex-1 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-sm font-medium text-white">
+ <span className="mr-1 text-txt">&#8226;</span>
+ <ChapterTitle className="ml-1" />
+ </span>
+ );
+}
diff --git a/components/watch/new-player/components/layouts/captions.module.css b/components/watch/new-player/components/layouts/captions.module.css
new file mode 100644
index 0000000..338b96e
--- /dev/null
+++ b/components/watch/new-player/components/layouts/captions.module.css
@@ -0,0 +1,80 @@
+.captions {
+ @apply font-roboto font-medium;
+ /* Recommended settings in the WebVTT spec (https://www.w3.org/TR/webvtt1). */
+ /* --cue-color: var(--media-cue-color, white); */
+ /* --cue-color: white; */
+ /* z-index: 20; */
+ /* --cue-bg-color: var(--media-cue-bg, rgba(0, 0, 0, 0.7)); */
+
+ /* bg color white */
+ --cue-bg-color: rgba(255, 255, 255, 0.9);
+ --cue-font-size: calc(var(--overlay-height) / 100 * 5);
+ --cue-line-height: calc(var(--cue-font-size) * 1.2);
+ --cue-padding-x: 0.5em;
+ --cue-padding-y: 0.1em;
+
+ /* remove background blur */
+
+ /* --cue-text-shadow: 0 0 5px black; */
+
+ font-size: var(--cue-font-size);
+ word-spacing: normal;
+ text-shadow: 0px 2px 8px rgba(0, 0, 0, 1);
+ /* contain: layout style; */
+}
+
+.captions[data-dir="rtl"] :global([data-part="cue-display"]) {
+ direction: rtl;
+}
+
+.captions[aria-hidden="true"] {
+ display: none;
+}
+
+/*************************************************************************************************
+ * Cue Display
+ *************************************************************************************************/
+
+/*
+* Most of the cue styles are set automatically by our [media-captions](https://github.com/vidstack/media-captions)
+* library via CSS variables. They are inferred from the VTT, SRT, or SSA file cue settings. You're
+* free to ignore them and style the captions as desired, but we don't recommend it unless the
+* captions file contains no cue settings. Otherwise, you might be breaking accessibility.
+*/
+.captions :global([data-part="cue-display"]) {
+ position: absolute;
+ direction: ltr;
+ overflow: visible;
+ contain: content;
+ top: var(--cue-top);
+ left: var(--cue-left);
+ right: var(--cue-right);
+ bottom: var(--cue-bottom);
+ width: var(--cue-width, auto);
+ height: var(--cue-height, auto);
+ transform: var(--cue-transform);
+ text-align: var(--cue-text-align);
+ writing-mode: var(--cue-writing-mode, unset);
+ white-space: pre-line;
+ unicode-bidi: plaintext;
+ min-width: min-content;
+ min-height: min-content;
+}
+
+.captions :global([data-part="cue"]) {
+ display: inline-block;
+ contain: content;
+ /* border-radius: 2px; */
+ /* backdrop-filter: unset; */
+ padding: var(--cue-padding-y) var(--cue-padding-x);
+ line-height: var(--cue-line-height);
+ /* background-color: var(--cue-bg-color); */
+ color: var(--cue-color);
+ white-space: pre-wrap;
+ outline: var(--cue-outline);
+ text-shadow: var(--cue-text-shadow);
+}
+
+.captions :global([data-part="cue-display"][data-vertical] [data-part="cue"]) {
+ padding: var(--cue-padding-x) var(--cue-padding-y);
+}
diff --git a/components/watch/new-player/components/layouts/video-layout.module.css b/components/watch/new-player/components/layouts/video-layout.module.css
new file mode 100644
index 0000000..14540f6
--- /dev/null
+++ b/components/watch/new-player/components/layouts/video-layout.module.css
@@ -0,0 +1,13 @@
+.controls {
+ /*
+ * These CSS variables are supported out of the box to easily apply offsets to all popups.
+ * You can also offset via props on `Tooltip.Content`, `Menu.Content`, and slider previews.
+ */
+ --media-tooltip-y-offset: 30px;
+ --media-menu-y-offset: 30px;
+}
+
+.controls :global(.volume-slider) {
+ --media-slider-preview-offset: 30px;
+ margin-left: 1.5px;
+}
diff --git a/components/watch/new-player/components/layouts/video-layout.tsx b/components/watch/new-player/components/layouts/video-layout.tsx
new file mode 100644
index 0000000..fa1f6c3
--- /dev/null
+++ b/components/watch/new-player/components/layouts/video-layout.tsx
@@ -0,0 +1,173 @@
+import captionStyles from "./captions.module.css";
+import styles from "./video-layout.module.css";
+
+import {
+ Captions,
+ Controls,
+ Gesture,
+ Spinner,
+ useMediaState,
+} from "@vidstack/react";
+
+import * as Buttons from "../buttons";
+import * as Menus from "../menus";
+import * as Sliders from "../sliders";
+import { TimeGroup } from "../time-group";
+import { Title } from "../title";
+import { ChapterTitleComponent } from "../chapter-title";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { Navigation } from "../../player";
+import BufferingIndicator from "../bufferingIndicator";
+import { useEffect, useState } from "react";
+
+export interface VideoLayoutProps {
+ thumbnails?: string;
+ navigation?: Navigation;
+ host?: boolean;
+}
+
+function isMobileDevice() {
+ if (typeof window !== "undefined") {
+ return (
+ typeof window.orientation !== "undefined" ||
+ navigator.userAgent.indexOf("IEMobile") !== -1
+ );
+ }
+ return false;
+}
+
+export function VideoLayout({
+ thumbnails,
+ navigation,
+ host = true,
+}: VideoLayoutProps) {
+ const [isMobile, setIsMobile] = useState(false);
+
+ const { track } = useWatchProvider();
+ const isFullscreen = useMediaState("fullscreen");
+
+ useEffect(() => {
+ setIsMobile(isMobileDevice());
+ }, []);
+
+ return (
+ <>
+ <Gestures host={host} />
+ <Captions
+ className={`${captionStyles.captions} media-preview:opacity-0 media-controls:bottom-[85px] media-captions:opacity-100 absolute inset-0 bottom-2 z-10 select-none break-words opacity-0 transition-[opacity,bottom] duration-300`}
+ />
+ <Controls.Root
+ className={`${styles.controls} media-paused:bg-black/10 duration-200 media-controls:opacity-100 absolute inset-0 z-10 flex h-full w-full flex-col bg-gradient-to-t from-black/30 via-transparent to-black/30 opacity-0 transition-opacity`}
+ >
+ <Controls.Group className="flex justify-between items-center w-full px-2 pt-2">
+ <Title navigation={navigation} />
+ <div className="flex-1" />
+ {/* <Menus.Episodes placement="left start" /> */}
+ </Controls.Group>
+ <div className="flex-1" />
+
+ {/* {isPaused && ( */}
+ <Controls.Group
+ className={`media-paused:opacity-100 media-paused:scale-100 backdrop-blur-sm scale-[160%] opacity-0 duration-200 ease-out flex shadow bg-white/10 rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2`}
+ >
+ <Buttons.MobilePlayButton tooltipPlacement="top center" host={host} />
+ </Controls.Group>
+ {/* )} */}
+
+ <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center">
+ <Spinner.Root
+ className="text-white opacity-0 transition-opacity duration-200 ease-linear media-buffering:animate-spin media-buffering:opacity-100"
+ size={84}
+ >
+ <Spinner.Track className="opacity-25" width={8} />
+ <Spinner.TrackFill className="opacity-75" width={8} />
+ </Spinner.Root>
+ </div>
+ {/* </Controls.Group> */}
+
+ <Controls.Group className="flex px-4">
+ <div className="flex-1" />
+ {host && (
+ <>
+ <Buttons.SkipOpButton tooltipPlacement="top end" />
+ <Buttons.SkipEdButton tooltipPlacement="top end" />
+ <Buttons.PlayNextButton
+ navigation={navigation}
+ tooltipPlacement="top end"
+ />
+ </>
+ )}
+ </Controls.Group>
+
+ <Controls.Group className="flex w-full items-center px-2">
+ <Sliders.Time thumbnails={thumbnails} host={host} />
+ </Controls.Group>
+ <Controls.Group className="-mt-0.5 flex w-full items-center px-2 pb-2">
+ <Buttons.Play tooltipPlacement="top start" />
+ <Buttons.Mute tooltipPlacement="top" />
+ <Sliders.Volume />
+ <TimeGroup />
+ <ChapterTitleComponent />
+ <div className="flex-1" />
+ {track?.subtitles && <Buttons.Caption tooltipPlacement="top" />}
+ <Menus.Settings placement="top end" tooltipPlacement="top" />
+ {!isMobile && !isFullscreen && (
+ <Buttons.TheaterButton tooltipPlacement="top" />
+ )}
+ <Buttons.PIP tooltipPlacement="top" />
+ <Buttons.Fullscreen tooltipPlacement="top end" />
+ </Controls.Group>
+ </Controls.Root>
+ </>
+ );
+}
+
+function Gestures({ host }: { host?: boolean }) {
+ const isMobile = isMobileDevice();
+ return (
+ <>
+ {isMobile ? (
+ <>
+ {host && (
+ <Gesture
+ className="absolute inset-0 z-10"
+ event="dblpointerup"
+ action="toggle:paused"
+ />
+ )}
+ <Gesture
+ className="absolute inset-0"
+ event="pointerup"
+ action="toggle:controls"
+ />
+ </>
+ ) : (
+ <>
+ {host && (
+ <Gesture
+ className="absolute inset-0"
+ event="pointerup"
+ action="toggle:paused"
+ />
+ )}
+ <Gesture
+ className="absolute inset-0 z-10"
+ event="dblpointerup"
+ action="toggle:fullscreen"
+ />
+ </>
+ )}
+
+ <Gesture
+ className="absolute top-0 left-0 w-1/5 h-full z-20"
+ event="dblpointerup"
+ action="seek:-10"
+ />
+ <Gesture
+ className="absolute top-0 right-0 w-1/5 h-full z-20"
+ event="dblpointerup"
+ action="seek:10"
+ />
+ </>
+ );
+}
diff --git a/components/watch/new-player/components/menus.tsx b/components/watch/new-player/components/menus.tsx
new file mode 100644
index 0000000..de2b302
--- /dev/null
+++ b/components/watch/new-player/components/menus.tsx
@@ -0,0 +1,387 @@
+// @ts-nocheck
+
+import type { ReactElement } from "react";
+
+// import EpiDataDummy from "@/components/test/episodeDummy.json";
+
+import {
+ Menu,
+ Tooltip,
+ useCaptionOptions,
+ type MenuPlacement,
+ type TooltipPlacement,
+ useVideoQualityOptions,
+ useMediaState,
+ usePlaybackRateOptions,
+} from "@vidstack/react";
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ ClosedCaptionsIcon,
+ SettingsMenuIcon,
+ RadioButtonIcon,
+ RadioButtonSelectedIcon,
+ SettingsIcon,
+ // EpisodesIcon,
+ SettingsSwitchIcon,
+ // PlaybackSpeedCircleIcon,
+ OdometerIcon,
+} from "@vidstack/react/icons";
+
+import { buttonClass, tooltipClass } from "./buttons";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import React from "react";
+
+export interface SettingsProps {
+ placement: MenuPlacement;
+ tooltipPlacement: TooltipPlacement;
+}
+
+export const menuClass =
+ "fixed bottom-0 animate-out fade-out slide-out-to-bottom-2 data-[open]:animate-in data-[open]:fade-in data-[open]:slide-in-from-bottom-4 flex h-[var(--menu-height)] max-h-[200px] lg:max-h-[400px] min-w-[260px] flex-col overflow-y-auto overscroll-y-contain rounded-md border border-white/10 bg-black/95 p-2.5 font-sans text-[15px] font-medium outline-none backdrop-blur-sm transition-[height] duration-300 will-change-[height] data-[resizing]:overflow-hidden";
+
+export const submenuClass =
+ "hidden w-full flex-col items-start justify-center outline-none data-[keyboard]:mt-[3px] data-[open]:inline-block";
+
+export const contentMenuClass =
+ "flex cust-scroll h-[var(--menu-height)] max-h-[180px] lg:max-h-[400px] min-w-[260px] flex-col overflow-y-auto overscroll-y-contain rounded-md border border-white/10 bg-secondary p-2 font-sans text-[15px] font-medium outline-none backdrop-blur-sm transition-[height] duration-300 will-change-[height] data-[resizing]:overflow-hidden";
+
+export function Settings({ placement, tooltipPlacement }: SettingsProps) {
+ const { track } = useWatchProvider();
+ const isSubtitleAvailable = track?.epiData?.subtitles?.length > 0;
+
+ return (
+ <Menu.Root className="parent">
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <Menu.Button className={buttonClass}>
+ <SettingsIcon className="h-8 w-8 transform transition-transform duration-200 ease-out group-data-[open]:rotate-90" />
+ </Menu.Button>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ Settings
+ </Tooltip.Content>
+ </Tooltip.Root>
+ {/* <Menu.Content className={menuClass} placement={placement}>
+ {isSubtitleAvailable && <CaptionSubmenu />}
+ <QualitySubmenu />
+ </Menu.Content> */}
+ <Menu.Content className={contentMenuClass} placement={placement}>
+ <AutoPlay />
+ <AutoNext />
+ <SpeedSubmenu />
+ {isSubtitleAvailable && <CaptionSubmenu />}
+ <QualitySubmenu />
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+// export function Episodes({ placement }: { placement: MenuPlacement }) {
+// return (
+// <Menu.Root className="parent">
+// <Tooltip.Root>
+// <Tooltip.Trigger asChild>
+// <Menu.Button className={buttonClass}>
+// <EpisodesIcon className="w-10 h-10" />
+// </Menu.Button>
+// </Tooltip.Trigger>
+// </Tooltip.Root>
+// <Menu.Content
+// className={`bg-secondary/95 border border-white/10 max-h-[240px] overflow-y-scroll cust-scroll rounded overflow-hidden z-30 -translate-y-5 -translate-x-2`}
+// placement={placement}
+// >
+// <EpisodeSubmenu />
+// </Menu.Content>
+// </Menu.Root>
+// );
+// }
+
+function SpeedSubmenu() {
+ const options = usePlaybackRateOptions(),
+ hint =
+ options.selectedValue === "1" ? "Normal" : options.selectedValue + "x";
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Playback Rate"
+ hint={hint}
+ icon={OdometerIcon}
+ disabled={options.disabled}
+ >
+ Speed ({hint})
+ </SubmenuButton>
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function CaptionSubmenu() {
+ const options = useCaptionOptions(),
+ hint = options.selectedTrack?.label ?? "Off";
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Captions"
+ hint={hint}
+ disabled={options.disabled}
+ icon={ClosedCaptionsIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+// function EpisodeSubmenu() {
+// return (
+// // <div className="h-full w-[320px]">
+// <div className="flex flex-col h-full w-[360px] font-karla">
+// {/* {EpiDataDummy.map((epi, index) => ( */}
+// <div
+// key={index}
+// className={`flex gap-1 hover:bg-secondary px-3 py-2 ${
+// index === 0
+// ? "pt-4"
+// // : index === EpiDataDummy.length - 1
+// ? "pb-4"
+// : ""
+// }`}
+// >
+// <Image
+// src={epi.img}
+// alt="thumbnail"
+// width={100}
+// height={100}
+// className="object-cover w-[120px] h-[64px] rounded-md"
+// />
+// <div className="flex flex-col pl-2">
+// <h1 className="font-semibold">{epi.title}</h1>
+// <p className="line-clamp-2 text-sm font-light">
+// {epi?.description}
+// </p>
+// </div>
+// </div>
+// ))}
+// </div>
+// // </div>
+// );
+// }
+
+function AutoPlay() {
+ const [options, setOptions] = React.useState([
+ {
+ label: "On",
+ value: "on",
+ selected: false,
+ },
+ {
+ label: "Off",
+ value: "off",
+ selected: true,
+ },
+ ]);
+
+ const { autoplay, setAutoPlay } = useWatchProvider();
+
+ // console.log({ autoplay });
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Autoplay Video"
+ hint={
+ autoplay
+ ? options.find((option) => option.value === autoplay)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ icon={SettingsSwitchIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={
+ autoplay
+ ? options.find((option) => option.value === autoplay)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ onChange={(value) => {
+ setOptions((options) =>
+ options.map((option) =>
+ option.value === value
+ ? { ...option, selected: true }
+ : { ...option, selected: false }
+ )
+ );
+ setAutoPlay(value);
+ localStorage.setItem("autoplay", value);
+ }}
+ >
+ {options.map((option) => (
+ <Radio key={option.value} value={option.value}>
+ {option.label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function AutoNext() {
+ const [options, setOptions] = React.useState([
+ {
+ label: "On",
+ value: "on",
+ selected: false,
+ },
+ {
+ label: "Off",
+ value: "off",
+ selected: true,
+ },
+ ]);
+
+ const { autoNext, setAutoNext } = useWatchProvider();
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Autoplay Next"
+ hint={
+ autoNext
+ ? options.find((option) => option.value === autoNext)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ icon={SettingsSwitchIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={
+ autoNext
+ ? options.find((option) => option.value === autoNext)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ onChange={(value) => {
+ setOptions((options) =>
+ options.map((option) =>
+ option.value === value
+ ? { ...option, selected: true }
+ : { ...option, selected: false }
+ )
+ );
+ setAutoNext(value);
+ localStorage.setItem("autoNext", value);
+ }}
+ >
+ {options.map((option) => (
+ <Radio key={option.value} value={option.value}>
+ {option.label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function QualitySubmenu() {
+ const options = useVideoQualityOptions({ sort: "descending" }),
+ autoQuality = useMediaState("autoQuality"),
+ currentQualityText = options.selectedQuality?.height + "p" ?? "",
+ hint = !autoQuality ? currentQualityText : `Auto (${currentQualityText})`;
+
+ // console.log({ options });
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Quality"
+ hint={hint}
+ disabled={options.disabled}
+ icon={SettingsMenuIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, bitrateText, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+export interface RadioProps extends Menu.RadioProps {}
+
+function Radio({ children, ...props }: RadioProps) {
+ return (
+ <Menu.Radio
+ className="ring-media-focus group relative flex w-full cursor-pointer select-none items-center justify-start rounded-sm p-2.5 outline-none data-[hocus]:bg-white/10 data-[focus]:ring-[3px]"
+ {...props}
+ >
+ <RadioButtonIcon className="h-4 w-4 text-white group-data-[checked]:hidden" />
+ <RadioButtonSelectedIcon
+ className="text-media-brand hidden h-4 w-4 group-data-[checked]:block"
+ type="radio-button-selected"
+ />
+ <span className="ml-2">{children}</span>
+ </Menu.Radio>
+ );
+}
+
+export interface SubmenuButtonProps {
+ label: string;
+ hint: string;
+ disabled?: boolean;
+ icon: ReactElement;
+}
+
+function SubmenuButton({
+ label,
+ hint,
+ icon: Icon,
+ disabled,
+}: SubmenuButtonProps) {
+ return (
+ <Menu.Button
+ className="ring-media-focus data-[open]:bg-secondary parent left-0 z-10 flex w-full cursor-pointer select-none items-center justify-start rounded-sm p-2.5 outline-none ring-inset data-[open]:sticky data-[open]:-top-2.5 data-[hocus]:bg-white/10 data-[focus]:ring-[3px]"
+ disabled={disabled}
+ >
+ <ChevronLeftIcon className="parent-data-[open]:block -ml-0.5 mr-1.5 hidden h-[18px] w-[18px]" />
+ <div className="contents parent-data-[open]:hidden">
+ <Icon className="w-5 h-5" />
+ </div>
+ <span className="ml-1.5 parent-data-[open]:ml-0">{label}</span>
+ <span className="ml-auto text-sm text-white/50">{hint}</span>
+ <ChevronRightIcon className="parent-data-[open]:hidden ml-0.5 h-[18px] w-[18px] text-sm text-white/50" />
+ </Menu.Button>
+ );
+}
diff --git a/components/watch/new-player/components/sliders.tsx b/components/watch/new-player/components/sliders.tsx
new file mode 100644
index 0000000..f31e28a
--- /dev/null
+++ b/components/watch/new-player/components/sliders.tsx
@@ -0,0 +1,73 @@
+import { TimeSlider, VolumeSlider } from "@vidstack/react";
+
+export function Volume() {
+ return (
+ <VolumeSlider.Root className="volume-slider group relative mx-[7.5px] inline-flex h-10 w-full max-w-[80px] cursor-pointer touch-none select-none items-center outline-none aria-hidden:hidden">
+ <VolumeSlider.Track className="relative ring-media-focus z-0 h-[5px] w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <VolumeSlider.TrackFill className="bg-white absolute h-full w-[var(--slider-fill)] rounded-sm will-change-[width]" />
+ </VolumeSlider.Track>
+
+ <VolumeSlider.Preview
+ className="flex flex-col items-center opacity-0 transition-opacity duration-200 data-[visible]:opacity-100"
+ noClamp
+ >
+ <VolumeSlider.Value className="rounded-sm bg-black px-2 py-px text-[13px] font-medium" />
+ </VolumeSlider.Preview>
+ <VolumeSlider.Thumb className="absolute left-[var(--slider-fill)] top-1/2 z-20 h-[15px] w-[15px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-[#cacaca] bg-white opacity-0 ring-white/40 transition-opacity group-data-[active]:opacity-100 group-data-[dragging]:ring-4 will-change-[left]" />
+ </VolumeSlider.Root>
+ );
+}
+
+export interface TimeSliderProps {
+ thumbnails?: string;
+ host?: boolean;
+}
+
+export function Time({ thumbnails, host }: TimeSliderProps) {
+ return (
+ <TimeSlider.Root
+ className={`${
+ host ? "" : "pointer-events-none"
+ } time-slider group relative mx-[7.5px] inline-flex h-10 w-full cursor-pointer touch-none select-none items-center outline-none`}
+ >
+ <TimeSlider.Chapters className="relative flex h-full w-full items-center rounded-[1px]">
+ {(cues, forwardRef) =>
+ cues.map((cue) => (
+ <div
+ className="last-child:mr-0 group/slider relative mr-0.5 flex h-full w-full items-center rounded-[1px]"
+ style={{ contain: "layout style" }}
+ key={cue.startTime}
+ ref={forwardRef}
+ >
+ <TimeSlider.Track className="relative ring-media-focus z-0 h-[5px] group-hover/slider:h-[10px] transition-all duration-100 w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <TimeSlider.TrackFill className="bg-white absolute h-full w-[var(--chapter-fill)] rounded-sm will-change-[width]" />
+ <TimeSlider.Progress className="absolute z-10 h-full w-[var(--chapter-progress)] rounded-sm bg-white/50 will-change-[width]" />
+ </TimeSlider.Track>
+ </div>
+ ))
+ }
+ </TimeSlider.Chapters>
+ {/* <TimeSlider.Track className="relative ring-media-focus z-0 h-[5px] w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <TimeSlider.TrackFill className="bg-white absolute h-full w-[var(--slider-fill)] rounded-sm will-change-[width]" />
+ <TimeSlider.Progress className="absolute z-10 h-full w-[var(--slider-progress)] rounded-sm bg-white/40 will-change-[width]" />
+ </TimeSlider.Track> */}
+
+ <TimeSlider.Thumb className="absolute left-[var(--slider-fill)] top-1/2 z-20 h-[15px] w-[15px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-[#cacaca] bg-white opacity-0 ring-white/40 transition-opacity group-data-[active]:opacity-100 group-data-[dragging]:ring-4 will-change-[left]" />
+
+ <TimeSlider.Preview className="flex flex-col items-center opacity-0 transition-opacity duration-200 data-[visible]:opacity-100 pointer-events-none">
+ {thumbnails ? (
+ <TimeSlider.Thumbnail.Root
+ src={thumbnails}
+ className="block h-[var(--thumbnail-height)] max-h-[160px] min-h-[80px] w-[var(--thumbnail-width)] min-w-[120px] max-w-[180px] overflow-hidden border border-white bg-black"
+ >
+ <TimeSlider.Thumbnail.Img />
+ </TimeSlider.Thumbnail.Root>
+ ) : null}
+
+ <TimeSlider.ChapterTitle className="mt-2 text-sm" />
+
+ <TimeSlider.Value className="text-[13px]" />
+ </TimeSlider.Preview>
+ </TimeSlider.Root>
+ );
+}
diff --git a/components/watch/new-player/components/time-group.tsx b/components/watch/new-player/components/time-group.tsx
new file mode 100644
index 0000000..45fc795
--- /dev/null
+++ b/components/watch/new-player/components/time-group.tsx
@@ -0,0 +1,11 @@
+import { Time } from "@vidstack/react";
+
+export function TimeGroup() {
+ return (
+ <div className="ml-1.5 flex items-center text-sm font-medium">
+ <Time className="time" type="current" />
+ <div className="mx-1 text-white/80">/</div>
+ <Time className="time" type="duration" />
+ </div>
+ );
+}
diff --git a/components/watch/new-player/components/title.tsx b/components/watch/new-player/components/title.tsx
new file mode 100644
index 0000000..6233061
--- /dev/null
+++ b/components/watch/new-player/components/title.tsx
@@ -0,0 +1,35 @@
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { useMediaRemote } from "@vidstack/react";
+import { ChevronLeftIcon } from "@vidstack/react/icons";
+import { Navigation } from "../player";
+
+type TitleProps = {
+ navigation?: Navigation;
+};
+
+export function Title({ navigation }: TitleProps) {
+ const { dataMedia } = useWatchProvider();
+ const remote = useMediaRemote();
+
+ return (
+ <div className="media-fullscreen:flex hidden text-start flex-1 text-sm font-medium text-white">
+ {/* <p className="pt-4 h-full">
+ </p> */}
+ <button
+ type="button"
+ className="flex items-center gap-2 text-sm font-karla w-full"
+ onClick={() => remote.toggleFullscreen()}
+ >
+ <ChevronLeftIcon className="font-extrabold w-7 h-7" />
+ <span className="max-w-[75%] text-base xl:text-2xl font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
+ {dataMedia?.title?.romaji}
+ </span>
+ <span className="text-base xl:text-2xl font-normal">/</span>
+ <span className="text-base xl:text-2xl font-normal">
+ Episode {navigation?.playing.number}
+ </span>
+ {/* <span className="absolute top-5 left-[1s0%] w-[24%] h-[1px] bg-white" /> */}
+ </button>
+ </div>
+ );
+}
diff --git a/components/watch/new-player/player.module.css b/components/watch/new-player/player.module.css
new file mode 100644
index 0000000..f2f5b39
--- /dev/null
+++ b/components/watch/new-player/player.module.css
@@ -0,0 +1,50 @@
+.player {
+ --media-brand: #f5f5f5;
+ --media-focus-ring-color: #4e9cf6;
+ --media-focus-ring: 0 0 0 3px var(--media-focus-ring-color);
+
+ --media-tooltip-y-offset: 30px;
+ --media-menu-y-offset: 30px;
+
+ background-color: black;
+ border-radius: var(--media-border-radius);
+ color: #f5f5f5;
+ contain: layout;
+ font-family: sans-serif;
+ overflow: hidden;
+}
+
+.player[data-focus]:not([data-playing]) {
+ box-shadow: var(--media-focus-ring);
+}
+
+.player video {
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+.player video,
+.poster {
+ border-radius: var(--media-border-radius);
+}
+
+.poster {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.poster[data-visible] {
+ opacity: 1;
+}
+
+.poster img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
diff --git a/components/watch/new-player/player.tsx b/components/watch/new-player/player.tsx
new file mode 100644
index 0000000..b98ff79
--- /dev/null
+++ b/components/watch/new-player/player.tsx
@@ -0,0 +1,471 @@
+import "@vidstack/react/player/styles/base.css";
+
+import { useEffect, useRef, useState } from "react";
+
+import style from "./player.module.css";
+
+import {
+ MediaPlayer,
+ MediaProvider,
+ useMediaStore,
+ useMediaRemote,
+ type MediaPlayerInstance,
+ Track,
+ MediaTimeUpdateEventDetail,
+ MediaTimeUpdateEvent,
+} from "@vidstack/react";
+import { VideoLayout } from "./components/layouts/video-layout";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { useRouter } from "next/router";
+import { Subtitle } from "types/episodes/TrackData";
+import useWatchStorage from "@/lib/hooks/useWatchStorage";
+import { Sessions } from "types/episodes/Sessions";
+import { useAniList } from "@/lib/anilist/useAnilist";
+
+export interface Navigation {
+ prev: Prev;
+ playing: Playing;
+ next: Next;
+}
+
+export interface Prev {
+ id: string;
+ title: string;
+ img: string;
+ number: number;
+ description: string;
+}
+
+export interface Playing {
+ id: string;
+ title: string;
+ description: string;
+ img: string;
+ number: number;
+}
+
+export interface Next {
+ id: string;
+ title: string;
+ description: string;
+ img: string;
+ number: number;
+}
+
+type VidStackProps = {
+ id: string;
+ navigation: Navigation;
+ userData: UserData;
+ sessions: Sessions;
+};
+
+export type UserData = {
+ id?: string;
+ userProfileId?: string;
+ aniId: string;
+ watchId: string;
+ title: string;
+ aniTitle: string;
+ image: string;
+ episode: number;
+ duration: number;
+ timeWatched: number;
+ provider: string;
+ nextId: string;
+ nextNumber: number;
+ dub: boolean;
+ createdAt: string;
+};
+
+type SkipData = {
+ startTime: number;
+ endTime: number;
+ text: string;
+};
+
+export default function VidStack({
+ id,
+ navigation,
+ userData,
+ sessions,
+}: VidStackProps) {
+ let player = useRef<MediaPlayerInstance>(null);
+
+ const {
+ aspectRatio,
+ setAspectRatio,
+ track,
+ playerState,
+ dataMedia,
+ autoNext,
+ } = useWatchProvider();
+
+ const { qualities, duration } = useMediaStore(player);
+
+ const [getSettings, updateSettings] = useWatchStorage();
+ const { marked, setMarked } = useWatchProvider();
+
+ const { markProgress } = useAniList(sessions);
+
+ const remote = useMediaRemote(player);
+
+ const { defaultQuality = null } = track ?? {};
+
+ const [chapters, setChapters] = useState<string>("");
+
+ const router = useRouter();
+
+ useEffect(() => {
+ if (qualities.length > 0) {
+ const sourceQuality = qualities.reduce(
+ (max, obj) => (obj.height > max.height ? obj : max),
+ qualities[0]
+ );
+ const aspectRatio = calculateAspectRatio(
+ sourceQuality.width,
+ sourceQuality.height
+ );
+
+ setAspectRatio(aspectRatio);
+ }
+ }, [qualities]);
+
+ const [isPlaying, setIsPlaying] = useState(false);
+ let interval: any;
+
+ useEffect(() => {
+ const plyr = player.current;
+
+ function handlePlay() {
+ // console.log("Player is playing");
+ setIsPlaying(true);
+ }
+
+ function handlePause() {
+ // console.log("Player is paused");
+ setIsPlaying(false);
+ }
+
+ function handleEnd() {
+ // console.log("Player ended");
+ setIsPlaying(false);
+ }
+
+ plyr?.addEventListener("play", handlePlay);
+ plyr?.addEventListener("pause", handlePause);
+ plyr?.addEventListener("ended", handleEnd);
+
+ return () => {
+ plyr?.removeEventListener("play", handlePlay);
+ plyr?.removeEventListener("pause", handlePause);
+ plyr?.removeEventListener("ended", handleEnd);
+ };
+ }, [id, duration]);
+
+ useEffect(() => {
+ if (isPlaying) {
+ interval = setInterval(async () => {
+ const currentTime = player.current?.currentTime
+ ? Math.round(player.current?.currentTime)
+ : 0;
+
+ const parsedImage = navigation?.playing?.img?.includes("null")
+ ? dataMedia?.coverImage?.extraLarge
+ : navigation?.playing?.img;
+
+ if (sessions?.user?.name) {
+ // console.log("updating user data");
+ await fetch("/api/user/update/episode", {
+ method: "PUT",
+ body: JSON.stringify({
+ name: sessions?.user?.name,
+ id: String(dataMedia?.id),
+ watchId: navigation?.playing?.id,
+ title:
+ navigation.playing?.title ||
+ dataMedia.title?.romaji ||
+ dataMedia.title?.english,
+ aniTitle: dataMedia.title?.romaji || dataMedia.title?.english,
+ image: parsedImage,
+ number: Number(navigation.playing?.number),
+ duration: duration,
+ timeWatched: currentTime,
+ provider: track?.provider,
+ nextId: navigation?.next?.id,
+ nextNumber: Number(navigation?.next?.number),
+ dub: track?.isDub ? true : false,
+ }),
+ });
+ }
+
+ updateSettings(navigation?.playing?.id, {
+ aniId: String(dataMedia.id),
+ watchId: navigation?.playing?.id,
+ title:
+ navigation.playing?.title ||
+ dataMedia.title?.romaji ||
+ dataMedia.title?.english,
+ aniTitle: dataMedia.title?.romaji || dataMedia.title?.english,
+ image: parsedImage,
+ episode: Number(navigation.playing?.number),
+ duration: duration,
+ timeWatched: currentTime, // update timeWatched with currentTime
+ provider: track?.provider,
+ nextId: navigation?.next?.id,
+ nextNumber: navigation?.next?.number,
+ dub: track?.isDub ? true : false,
+ createdAt: new Date().toISOString(),
+ });
+ // console.log("update");
+ }, 5000);
+ } else {
+ clearInterval(interval);
+ }
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [isPlaying, sessions?.user?.name, track?.isDub, duration]);
+
+ useEffect(() => {
+ const autoplay = localStorage.getItem("autoplay") || "off";
+
+ return player.current!.subscribe(({ canPlay }) => {
+ // console.log("can play?", "->", canPlay);
+ if (canPlay) {
+ if (autoplay === "on") {
+ if (playerState?.currentTime === 0) {
+ remote.play();
+ } else {
+ if (playerState?.isPlaying) {
+ remote.play();
+ } else {
+ remote.pause();
+ }
+ }
+ } else {
+ if (playerState?.isPlaying) {
+ remote.play();
+ } else {
+ remote.pause();
+ }
+ }
+ remote.seek(playerState?.currentTime);
+ }
+ });
+ }, [playerState?.currentTime, playerState?.isPlaying]);
+
+ useEffect(() => {
+ const chapter = track?.skip,
+ videoDuration = Math.round(duration);
+
+ let vtt = "WEBVTT\n\n";
+
+ let lastEndTime = 0;
+
+ if (chapter && chapter?.length > 0) {
+ chapter.forEach((item: SkipData) => {
+ let startMinutes = Math.floor(item.startTime / 60);
+ let startSeconds = item.startTime % 60;
+ let endMinutes = Math.floor(item.endTime / 60);
+ let endSeconds = item.endTime % 60;
+
+ let start = `${startMinutes.toString().padStart(2, "0")}:${startSeconds
+ .toString()
+ .padStart(2, "0")}`;
+ let end = `${endMinutes.toString().padStart(2, "0")}:${endSeconds
+ .toString()
+ .padStart(2, "0")}`;
+
+ vtt += `${start} --> ${end}\n${item.text}\n\n`;
+ if (item.endTime > lastEndTime) {
+ lastEndTime = item.endTime;
+ }
+ });
+
+ if (lastEndTime < videoDuration) {
+ let startMinutes = Math.floor(lastEndTime / 60);
+ let startSeconds = lastEndTime % 60;
+ let endMinutes = Math.floor(videoDuration / 60);
+ let endSeconds = videoDuration % 60;
+
+ let start = `${startMinutes.toString().padStart(2, "0")}:${startSeconds
+ .toString()
+ .padStart(2, "0")}`;
+ let end = `${endMinutes.toString().padStart(2, "0")}:${endSeconds
+ .toString()
+ .padStart(2, "0")}`;
+
+ vtt += `${start} --> ${end}\n\n\n`;
+ }
+
+ const vttBlob = new Blob([vtt], { type: "text/vtt" });
+ const vttUrl = URL.createObjectURL(vttBlob);
+
+ setChapters(vttUrl);
+ }
+ return () => {
+ setChapters("");
+ };
+ }, [track?.skip, duration]);
+
+ useEffect(() => {
+ return () => {
+ if (player.current) {
+ player.current.destroy();
+ }
+ };
+ }, []);
+
+ function onEnded() {
+ if (!navigation?.next?.id) return;
+ if (autoNext === "on") {
+ const nextButton = document.querySelector(".next-button");
+
+ let timeoutId: ReturnType<typeof setTimeout>;
+
+ const stopTimeout = () => {
+ clearTimeout(timeoutId);
+ nextButton?.classList.remove("progress");
+ };
+
+ nextButton?.classList.remove("hidden");
+ nextButton?.classList.add("progress");
+
+ timeoutId = setTimeout(() => {
+ console.log("time is up!");
+ if (navigation?.next) {
+ router.push(
+ `/en/anime/watch/${dataMedia.id}/${track.provider}?id=${
+ navigation?.next?.id
+ }&num=${navigation?.next?.number}${
+ track?.isDub ? `&dub=${track?.isDub}` : ""
+ }`
+ );
+ }
+ }, 7000);
+
+ nextButton?.addEventListener("mouseover", stopTimeout);
+ }
+ }
+
+ function onLoadedMetadata() {
+ const seek: any = getSettings(navigation?.playing?.id);
+ if (playerState?.currentTime !== 0) return;
+ const seekTime = seek?.timeWatched;
+ const percentage = duration !== 0 ? seekTime / Math.round(duration) : 0;
+ const percentagedb =
+ duration !== 0 ? userData?.timeWatched / Math.round(duration) : 0;
+
+ if (percentage >= 0.9 || percentagedb >= 0.9) {
+ remote.seek(0);
+ console.log("Video started from the beginning");
+ } else if (userData?.timeWatched) {
+ remote.seek(userData?.timeWatched);
+ } else {
+ remote.seek(seekTime);
+ }
+ }
+
+ let mark = 0;
+ function onTimeUpdate(detail: MediaTimeUpdateEventDetail) {
+ if (sessions) {
+ let currentTime = detail.currentTime;
+ const percentage = currentTime / duration;
+
+ if (percentage >= 0.9) {
+ // use >= instead of >
+ if (mark < 1 && marked < 1) {
+ mark = 1;
+ setMarked(1);
+ console.log("marking progress");
+ markProgress(dataMedia.id, navigation.playing.number);
+ }
+ }
+ }
+
+ const opButton = document.querySelector(".op-button");
+ const edButton = document.querySelector(".ed-button");
+
+ const op: SkipData = track?.skip.find(
+ (item: SkipData) => item.text === "Opening"
+ ),
+ ed = track?.skip.find((item: SkipData) => item.text === "Ending");
+
+ if (
+ op &&
+ detail.currentTime > op.startTime &&
+ detail.currentTime < op.endTime
+ ) {
+ opButton?.classList.remove("hidden");
+ } else {
+ opButton?.classList.add("hidden");
+ }
+
+ if (
+ ed &&
+ detail.currentTime > ed.startTime &&
+ detail.currentTime < ed.endTime
+ ) {
+ edButton?.classList.remove("hidden");
+ } else {
+ edButton?.classList.add("hidden");
+ }
+ }
+
+ function onSeeked(currentTime: number) {
+ const nextButton = document.querySelector(".next-button");
+ // console.log({ currentTime, duration });
+ if (currentTime !== duration) {
+ nextButton?.classList.add("hidden");
+ }
+ }
+
+ return (
+ <MediaPlayer
+ key={id}
+ className={`${style.player} player`}
+ title={
+ navigation?.playing?.title ||
+ `Episode ${navigation?.playing?.number}` ||
+ "Loading..."
+ }
+ load="idle"
+ crossorigin="anonymous"
+ src={{
+ src: defaultQuality?.url,
+ type: "application/vnd.apple.mpegurl",
+ }}
+ onTimeUpdate={onTimeUpdate}
+ playsinline
+ aspectRatio={aspectRatio}
+ onEnd={onEnded}
+ onSeeked={onSeeked}
+ onLoadedMetadata={onLoadedMetadata}
+ ref={player}
+ >
+ <MediaProvider>
+ {track &&
+ track?.subtitles &&
+ track?.subtitles?.map((track: Subtitle) => (
+ <Track {...track} key={track.src} />
+ ))}
+ {chapters?.length > 0 && (
+ <Track key={chapters} src={chapters} kind="chapters" default={true} />
+ )}
+ </MediaProvider>
+ <VideoLayout thumbnails={track?.thumbnails} navigation={navigation} />
+ </MediaPlayer>
+ );
+}
+
+export function calculateAspectRatio(width: number, height: number) {
+ if (width === 0 && height === 0) {
+ return "16/9";
+ }
+
+ const gcd = (a: number, b: number): any => (b === 0 ? a : gcd(b, a % b));
+ const divisor = gcd(width, height);
+ const aspectRatio = `${width / divisor}/${height / divisor}`;
+ return aspectRatio;
+}
diff --git a/components/watch/new-player/tracks.tsx b/components/watch/new-player/tracks.tsx
new file mode 100644
index 0000000..abc1fb5
--- /dev/null
+++ b/components/watch/new-player/tracks.tsx
@@ -0,0 +1,184 @@
+export const textTracks = [
+ // Subtitles
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/subs/english.vtt",
+ // label: "English",
+ // language: "en-US",
+ // kind: "subtitles",
+ // default: true,
+ // },
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/subs/spanish.vtt",
+ // label: "Spanish",
+ // language: "es-ES",
+ // kind: "subtitles",
+ // },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ara-3.vtt",
+ label: "Arabic",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/chi-4.vtt",
+ label: "Chinese - Chinese Simplified",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/chi-5.vtt",
+ label: "Chinese - Chinese Traditional",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/hrv-6.vtt",
+ label: "Croatian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/cze-7.vtt",
+ label: "Czech",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/dan-8.vtt",
+ label: "Danish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/dut-9.vtt",
+ label: "Dutch",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/32/6d/326d5416033fe39a1540b11908f191fe/326d5416033fe39a1540b11908f191fe.vtt",
+ label: "English",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/fin-10.vtt",
+ label: "Finnish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/fre-11.vtt",
+ label: "French",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ger-12.vtt",
+ label: "German",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/gre-13.vtt",
+ label: "Greek",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/heb-14.vtt",
+ label: "Hebrew",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/hun-15.vtt",
+ label: "Hungarian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ind-16.vtt",
+ label: "Indonesian",
+ kind: "subtitles",
+ default: true,
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ita-17.vtt",
+ label: "Italian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/jpn-18.vtt",
+ label: "Japanese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/kor-19.vtt",
+ label: "Korean",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/may-20.vtt",
+ label: "Malay",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/nob-21.vtt",
+ label: "Norwegian Bokmål - Norwegian Bokmal",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/pol-22.vtt",
+ label: "Polish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/por-23.vtt",
+ label: "Portuguese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/por-24.vtt",
+ label: "Portuguese - Brazilian Portuguese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/rum-25.vtt",
+ label: "Romanian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/rus-26.vtt",
+ label: "Russian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/spa-27.vtt",
+ label: "Spanish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/spa-28.vtt",
+ label: "Spanish - European Spanish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/swe-29.vtt",
+ label: "Swedish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/tha-30.vtt",
+ label: "Thai",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/tur-31.vtt",
+ label: "Turkish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ukr-32.vtt",
+ label: "Ukrainian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/vie-33.vtt",
+ label: "Vietnamese",
+ kind: "subtitles",
+ },
+ // // Chapters
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/chapters.vtt",
+ // kind: "chapters",
+ // language: "en-US",
+ // default: true,
+ // },
+] as const;