@zezosoft/react-player 0.0.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +536 -146
- package/dist/VideoPlayer/components/ErrorOverlay.d.ts +8 -0
- package/dist/VideoPlayer/hooks/index.d.ts +1 -0
- package/dist/VideoPlayer/hooks/useNetworkSpeed.d.ts +7 -0
- package/dist/VideoPlayer/hooks/useSubtitleStyling.d.ts +3 -0
- package/dist/VideoPlayer/hooks/useVideoError.d.ts +7 -0
- package/dist/VideoPlayer/hooks/useVideoTracking.d.ts +2 -2
- package/dist/VideoPlayer/types/AdTypes.d.ts +0 -3
- package/dist/VideoPlayer/types/VideoPlayerTypes.d.ts +35 -9
- package/dist/VideoPlayer/utils/index.d.ts +0 -11
- package/dist/components/ui/Settings.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +976 -425
- package/dist/store/slices/errorSlice.d.ts +5 -0
- package/dist/store/slices/index.d.ts +1 -0
- package/dist/store/types/StoreTypes.d.ts +11 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import React__default, { memo, useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
3
3
|
import { useShallow } from 'zustand/react/shallow';
|
|
4
|
+
import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
|
|
4
5
|
import { create } from 'zustand';
|
|
5
6
|
import { IoVolumeMuteOutline, IoVolumeHighOutline } from 'react-icons/io5';
|
|
6
7
|
import { IoMdClose } from 'react-icons/io';
|
|
7
|
-
import { Settings as Settings$1, ChevronRight, Check, Loader, ArrowRight, SkipForward } from 'lucide-react';
|
|
8
8
|
import screenfull from 'screenfull';
|
|
9
9
|
import Hls from 'hls.js';
|
|
10
10
|
import * as dashjs from 'dashjs';
|
|
@@ -36,7 +36,7 @@ function styleInject(css, ref) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-yellow-500: oklch(79.5% 0.184 86.047);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wide: 0.025em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .block {\n display: block;\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-24 {\n height: calc(var(--spacing) * 24);\n }\n .h-full {\n height: 100%;\n }\n .max-h-80 {\n max-height: calc(var(--spacing) * 80);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-24 {\n width: calc(var(--spacing) * 24);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[2px\\] {\n width: 2px;\n }\n .w-\\[10vw\\] {\n width: 10vw;\n }\n .w-\\[15vw\\] {\n width: 15vw;\n }\n .w-\\[720px\\] {\n width: 720px;\n }\n .w-fit {\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .rotate-45 {\n rotate: 45deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-not-allowed {\n cursor: not-allowed;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-3 {\n gap: calc(var(--spacing) * 3);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-\\[5px\\] {\n border-radius: 5px;\n }\n .rounded-\\[7px\\] {\n border-radius: 7px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-gray-600 {\n border-color: var(--color-gray-600);\n }\n .border-gray-700\\/60 {\n border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-gray-700) 60%, transparent);\n }\n }\n .border-white\\/10 {\n border-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .border-white\\/30 {\n border-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .border-white\\/40 {\n border-color: color-mix(in srgb, #fff 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 40%, transparent);\n }\n }\n .bg-\\[\\#2D2F31\\] {\n background-color: #2D2F31;\n }\n .bg-\\[\\#3a4049\\] {\n background-color: #3a4049;\n }\n .bg-\\[\\#454545\\] {\n background-color: #454545;\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/60 {\n background-color: color-mix(in srgb, #000 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 60%, transparent);\n }\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-gray-500 {\n background-color: var(--color-gray-500);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-white\\/5 {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n .bg-white\\/10 {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .bg-white\\/20 {\n background-color: color-mix(in srgb, #fff 20%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 20%, transparent);\n }\n }\n .bg-white\\/80 {\n background-color: color-mix(in srgb, #fff 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 80%, transparent);\n }\n }\n .bg-linear-to-b {\n --tw-gradient-position: to bottom;\n @supports (background-image: linear-gradient(in lab, red, red)) {\n --tw-gradient-position: to bottom in oklab;\n }\n background-image: linear-gradient(var(--tw-gradient-stops));\n }\n .from-black {\n --tw-gradient-from: var(--color-black);\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .from-black\\/80 {\n --tw-gradient-from: color-mix(in srgb, #000 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-from: color-mix(in oklab, var(--color-black) 80%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .via-transparent {\n --tw-gradient-via: transparent;\n --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-via-stops);\n }\n .to-black\\/90 {\n --tw-gradient-to: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-to: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\n }\n .object-contain {\n object-fit: contain;\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-10 {\n padding: calc(var(--spacing) * 10);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-5 {\n padding-inline: calc(var(--spacing) * 5);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .px-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .px-20 {\n padding-inline: calc(var(--spacing) * 20);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-6 {\n padding-top: calc(var(--spacing) * 6);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pb-4 {\n padding-bottom: calc(var(--spacing) * 4);\n }\n .pb-6 {\n padding-bottom: calc(var(--spacing) * 6);\n }\n .pb-10 {\n padding-bottom: calc(var(--spacing) * 10);\n }\n .pb-16 {\n padding-bottom: calc(var(--spacing) * 16);\n }\n .text-left {\n text-align: left;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wide {\n --tw-tracking: var(--tracking-wide);\n letter-spacing: var(--tracking-wide);\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-gray-200 {\n color: var(--color-gray-200);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-900 {\n color: var(--color-gray-900);\n }\n .text-sky-300 {\n color: var(--color-sky-300);\n }\n .text-white {\n color: var(--color-white);\n }\n .text-yellow-500 {\n color: var(--color-yellow-500);\n }\n .uppercase {\n text-transform: uppercase;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .backdrop-blur-md {\n --tw-backdrop-blur: blur(var(--blur-md));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .select-none {\n -webkit-user-select: none;\n user-select: none;\n }\n .hover\\:scale-105 {\n &:hover {\n @media (hover: hover) {\n --tw-scale-x: 105%;\n --tw-scale-y: 105%;\n --tw-scale-z: 105%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n }\n .hover\\:border-white\\/50 {\n &:hover {\n @media (hover: hover) {\n border-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n }\n .hover\\:bg-gray-300 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-gray-300);\n }\n }\n }\n .hover\\:bg-white\\/5 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/10 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/30 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 90%, transparent);\n }\n }\n }\n }\n .hover\\:text-gray-200 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n .hover\\:shadow-lg {\n &:hover {\n @media (hover: hover) {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-gray-400 {\n &:focus {\n --tw-ring-color: var(--color-gray-400);\n }\n }\n .focus\\:ring-offset-1 {\n &:focus {\n --tw-ring-offset-width: 1px;\n --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n .active\\:scale-95 {\n &:active {\n --tw-scale-x: 95%;\n --tw-scale-y: 95%;\n --tw-scale-z: 95%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:bg-white\\/50 {\n &:disabled {\n background-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .lg\\:h-32 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 32);\n }\n }\n .lg\\:w-32 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 32);\n }\n }\n .lg\\:pb-12 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 12);\n }\n }\n .lg\\:text-2xl {\n @media (width >= 64rem) {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n }\n .lg\\:text-3xl {\n @media (width >= 64rem) {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n }\n .lg\\:text-base {\n @media (width >= 64rem) {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\n}\n.icon-class {\n height: calc(var(--spacing) * 14);\n width: calc(var(--spacing) * 14);\n cursor: pointer;\n color: var(--color-gray-400);\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-gradient-position {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-via {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-to {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-via-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 0%;\n}\n@property --tw-gradient-via-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 50%;\n}\n@property --tw-gradient-to-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-backdrop-blur {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-scale-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-gradient-position: initial;\n --tw-gradient-from: #0000;\n --tw-gradient-via: #0000;\n --tw-gradient-to: #0000;\n --tw-gradient-stops: initial;\n --tw-gradient-via-stops: initial;\n --tw-gradient-from-position: 0%;\n --tw-gradient-via-position: 50%;\n --tw-gradient-to-position: 100%;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n }\n }\n}\n";
|
|
39
|
+
var css_248z$4 = "/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\",\n \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n \"Courier New\", monospace;\n --color-red-400: oklch(70.4% 0.191 22.216);\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-red-600: oklch(57.7% 0.245 27.325);\n --color-red-700: oklch(50.5% 0.213 27.518);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-sky-300: oklch(82.8% 0.111 230.318);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-purple-500: oklch(62.7% 0.265 303.9);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-gray-300: oklch(87.2% 0.01 258.338);\n --color-gray-400: oklch(70.7% 0.022 261.325);\n --color-gray-500: oklch(55.1% 0.027 264.364);\n --color-gray-600: oklch(44.6% 0.03 256.802);\n --color-gray-700: oklch(37.3% 0.034 259.733);\n --color-gray-900: oklch(21% 0.034 264.665);\n --color-black: #000;\n --color-white: #fff;\n --spacing: 0.25rem;\n --container-md: 28rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --text-3xl: 1.875rem;\n --text-3xl--line-height: calc(2.25 / 1.875);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --font-weight-bold: 700;\n --tracking-wider: 0.05em;\n --radius-md: 0.375rem;\n --radius-lg: 0.5rem;\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n --animate-spin: spin 1s linear infinite;\n --blur-sm: 8px;\n --blur-md: 12px;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type=\"button\"], [type=\"reset\"], [type=\"submit\"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden=\"until-found\"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .pointer-events-none {\n pointer-events: none;\n }\n .absolute {\n position: absolute;\n }\n .relative {\n position: relative;\n }\n .static {\n position: static;\n }\n .inset-0 {\n inset: calc(var(--spacing) * 0);\n }\n .-top-2 {\n top: calc(var(--spacing) * -2);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .top-1\\/2 {\n top: calc(1/2 * 100%);\n }\n .top-full {\n top: 100%;\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-32 {\n right: calc(var(--spacing) * 32);\n }\n .right-full {\n right: 100%;\n }\n .bottom-36 {\n bottom: calc(var(--spacing) * 36);\n }\n .bottom-full {\n bottom: 100%;\n }\n .left-0 {\n left: calc(var(--spacing) * 0);\n }\n .left-1\\/2 {\n left: calc(1/2 * 100%);\n }\n .left-32 {\n left: calc(var(--spacing) * 32);\n }\n .left-full {\n left: 100%;\n }\n .z-40 {\n z-index: 40;\n }\n .z-50 {\n z-index: 50;\n }\n .z-\\[-1\\] {\n z-index: -1;\n }\n .container {\n width: 100%;\n @media (width >= 40rem) {\n max-width: 40rem;\n }\n @media (width >= 48rem) {\n max-width: 48rem;\n }\n @media (width >= 64rem) {\n max-width: 64rem;\n }\n @media (width >= 80rem) {\n max-width: 80rem;\n }\n @media (width >= 96rem) {\n max-width: 96rem;\n }\n }\n .mx-2 {\n margin-inline: calc(var(--spacing) * 2);\n }\n .mx-auto {\n margin-inline: auto;\n }\n .mt-1 {\n margin-top: calc(var(--spacing) * 1);\n }\n .mt-2 {\n margin-top: calc(var(--spacing) * 2);\n }\n .mt-4 {\n margin-top: calc(var(--spacing) * 4);\n }\n .mr-2 {\n margin-right: calc(var(--spacing) * 2);\n }\n .mb-1 {\n margin-bottom: calc(var(--spacing) * 1);\n }\n .mb-2 {\n margin-bottom: calc(var(--spacing) * 2);\n }\n .mb-4 {\n margin-bottom: calc(var(--spacing) * 4);\n }\n .ml-1 {\n margin-left: calc(var(--spacing) * 1);\n }\n .ml-2 {\n margin-left: calc(var(--spacing) * 2);\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline {\n display: inline;\n }\n .inline-block {\n display: inline-block;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1 {\n height: calc(var(--spacing) * 1);\n }\n .h-3 {\n height: calc(var(--spacing) * 3);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-10 {\n height: calc(var(--spacing) * 10);\n }\n .h-12 {\n height: calc(var(--spacing) * 12);\n }\n .h-14 {\n height: calc(var(--spacing) * 14);\n }\n .h-full {\n height: 100%;\n }\n .max-h-80 {\n max-height: calc(var(--spacing) * 80);\n }\n .w-3 {\n width: calc(var(--spacing) * 3);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-5 {\n width: calc(var(--spacing) * 5);\n }\n .w-6 {\n width: calc(var(--spacing) * 6);\n }\n .w-12 {\n width: calc(var(--spacing) * 12);\n }\n .w-14 {\n width: calc(var(--spacing) * 14);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-\\[2px\\] {\n width: 2px;\n }\n .w-\\[10vw\\] {\n width: 10vw;\n }\n .w-\\[15vw\\] {\n width: 15vw;\n }\n .w-fit {\n width: fit-content;\n }\n .w-full {\n width: 100%;\n }\n .max-w-md {\n max-width: var(--container-md);\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .-translate-x-1\\/2 {\n --tw-translate-x: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1/2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .rotate-45 {\n rotate: 45deg;\n }\n .rotate-180 {\n rotate: 180deg;\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-default {\n cursor: default;\n }\n .cursor-not-allowed {\n cursor: not-allowed;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .flex-col {\n flex-direction: column;\n }\n .items-center {\n align-items: center;\n }\n .items-start {\n align-items: flex-start;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .gap-1 {\n gap: calc(var(--spacing) * 1);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-3 {\n gap: calc(var(--spacing) * 3);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-7 {\n gap: calc(var(--spacing) * 7);\n }\n .space-y-0 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .space-y-3 {\n :where(& > :not(:last-child)) {\n --tw-space-y-reverse: 0;\n margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));\n margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));\n }\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded {\n border-radius: 0.25rem;\n }\n .rounded-\\[5px\\] {\n border-radius: 5px;\n }\n .rounded-\\[7px\\] {\n border-radius: 7px;\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-md {\n border-radius: var(--radius-md);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-l {\n border-left-style: var(--tw-border-style);\n border-left-width: 1px;\n }\n .border-gray-600 {\n border-color: var(--color-gray-600);\n }\n .border-gray-700\\/60 {\n border-color: color-mix(in srgb, oklch(37.3% 0.034 259.733) 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-gray-700) 60%, transparent);\n }\n }\n .border-white\\/10 {\n border-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .border-white\\/30 {\n border-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n .border-white\\/40 {\n border-color: color-mix(in srgb, #fff 40%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 40%, transparent);\n }\n }\n .bg-\\[\\#3a4049\\] {\n background-color: #3a4049;\n }\n .bg-\\[\\#454545\\] {\n background-color: #454545;\n }\n .bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\] {\n background-color: rgba(0,0,0,0.5);\n }\n .bg-black {\n background-color: var(--color-black);\n }\n .bg-black\\/60 {\n background-color: color-mix(in srgb, #000 60%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 60%, transparent);\n }\n }\n .bg-black\\/90 {\n background-color: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-gray-500 {\n background-color: var(--color-gray-500);\n }\n .bg-gray-900 {\n background-color: var(--color-gray-900);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-purple-500 {\n background-color: var(--color-purple-500);\n }\n .bg-red-600 {\n background-color: var(--color-red-600);\n }\n .bg-transparent {\n background-color: transparent;\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .bg-white\\/10 {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n .bg-white\\/20 {\n background-color: color-mix(in srgb, #fff 20%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 20%, transparent);\n }\n }\n .bg-white\\/80 {\n background-color: color-mix(in srgb, #fff 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 80%, transparent);\n }\n }\n .bg-linear-to-b {\n --tw-gradient-position: to bottom;\n @supports (background-image: linear-gradient(in lab, red, red)) {\n --tw-gradient-position: to bottom in oklab;\n }\n background-image: linear-gradient(var(--tw-gradient-stops));\n }\n .from-black {\n --tw-gradient-from: var(--color-black);\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .from-black\\/80 {\n --tw-gradient-from: color-mix(in srgb, #000 80%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-from: color-mix(in oklab, var(--color-black) 80%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .via-transparent {\n --tw-gradient-via: transparent;\n --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);\n --tw-gradient-stops: var(--tw-gradient-via-stops);\n }\n .to-black\\/90 {\n --tw-gradient-to: color-mix(in srgb, #000 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n --tw-gradient-to: color-mix(in oklab, var(--color-black) 90%, transparent);\n }\n --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));\n }\n .bg-cover {\n background-size: cover;\n }\n .bg-center {\n background-position: center;\n }\n .object-contain {\n object-fit: contain;\n }\n .p-0 {\n padding: calc(var(--spacing) * 0);\n }\n .p-1 {\n padding: calc(var(--spacing) * 1);\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .p-6 {\n padding: calc(var(--spacing) * 6);\n }\n .p-10 {\n padding: calc(var(--spacing) * 10);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .px-5 {\n padding-inline: calc(var(--spacing) * 5);\n }\n .px-6 {\n padding-inline: calc(var(--spacing) * 6);\n }\n .px-10 {\n padding-inline: calc(var(--spacing) * 10);\n }\n .px-20 {\n padding-inline: calc(var(--spacing) * 20);\n }\n .py-1 {\n padding-block: calc(var(--spacing) * 1);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-6 {\n padding-top: calc(var(--spacing) * 6);\n }\n .pb-3 {\n padding-bottom: calc(var(--spacing) * 3);\n }\n .pb-4 {\n padding-bottom: calc(var(--spacing) * 4);\n }\n .pb-6 {\n padding-bottom: calc(var(--spacing) * 6);\n }\n .pb-10 {\n padding-bottom: calc(var(--spacing) * 10);\n }\n .pb-16 {\n padding-bottom: calc(var(--spacing) * 16);\n }\n .text-center {\n text-align: center;\n }\n .text-left {\n text-align: left;\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .font-bold {\n --tw-font-weight: var(--font-weight-bold);\n font-weight: var(--font-weight-bold);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wider {\n --tw-tracking: var(--tracking-wider);\n letter-spacing: var(--tracking-wider);\n }\n .whitespace-nowrap {\n white-space: nowrap;\n }\n .text-gray-200 {\n color: var(--color-gray-200);\n }\n .text-gray-300 {\n color: var(--color-gray-300);\n }\n .text-gray-400 {\n color: var(--color-gray-400);\n }\n .text-gray-500 {\n color: var(--color-gray-500);\n }\n .text-gray-900 {\n color: var(--color-gray-900);\n }\n .text-red-400 {\n color: var(--color-red-400);\n }\n .text-red-500 {\n color: var(--color-red-500);\n }\n .text-red-600 {\n color: var(--color-red-600);\n }\n .text-sky-300 {\n color: var(--color-sky-300);\n }\n .text-white {\n color: var(--color-white);\n }\n .normal-case {\n text-transform: none;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .opacity-50 {\n opacity: 50%;\n }\n .opacity-100 {\n opacity: 100%;\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-md {\n --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .backdrop-blur-md {\n --tw-backdrop-blur: blur(var(--blur-md));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .backdrop-blur-sm {\n --tw-backdrop-blur: blur(var(--blur-sm));\n -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);\n }\n .transition {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-all {\n transition-property: all;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .transition-opacity {\n transition-property: opacity;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .duration-200 {\n --tw-duration: 200ms;\n transition-duration: 200ms;\n }\n .duration-300 {\n --tw-duration: 300ms;\n transition-duration: 300ms;\n }\n .ease-in-out {\n --tw-ease: var(--ease-in-out);\n transition-timing-function: var(--ease-in-out);\n }\n .ease-out {\n --tw-ease: var(--ease-out);\n transition-timing-function: var(--ease-out);\n }\n .select-none {\n -webkit-user-select: none;\n user-select: none;\n }\n .hover\\:scale-105 {\n &:hover {\n @media (hover: hover) {\n --tw-scale-x: 105%;\n --tw-scale-y: 105%;\n --tw-scale-z: 105%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n }\n .hover\\:border-white\\/50 {\n &:hover {\n @media (hover: hover) {\n border-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n border-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n }\n .hover\\:bg-gray-300 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-gray-300);\n }\n }\n }\n .hover\\:bg-red-700 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-red-700);\n }\n }\n }\n .hover\\:bg-white\\/5 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 5%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 5%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/10 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 10%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 10%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/30 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 30%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 30%, transparent);\n }\n }\n }\n }\n .hover\\:bg-white\\/90 {\n &:hover {\n @media (hover: hover) {\n background-color: color-mix(in srgb, #fff 90%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 90%, transparent);\n }\n }\n }\n }\n .hover\\:text-gray-200 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n }\n .hover\\:text-white {\n &:hover {\n @media (hover: hover) {\n color: var(--color-white);\n }\n }\n }\n .hover\\:shadow-lg {\n &:hover {\n @media (hover: hover) {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n }\n .focus\\:ring-2 {\n &:focus {\n --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n }\n .focus\\:ring-gray-400 {\n &:focus {\n --tw-ring-color: var(--color-gray-400);\n }\n }\n .focus\\:ring-offset-1 {\n &:focus {\n --tw-ring-offset-width: 1px;\n --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);\n }\n }\n .focus\\:outline-none {\n &:focus {\n --tw-outline-style: none;\n outline-style: none;\n }\n }\n .active\\:scale-95 {\n &:active {\n --tw-scale-x: 95%;\n --tw-scale-y: 95%;\n --tw-scale-z: 95%;\n scale: var(--tw-scale-x) var(--tw-scale-y);\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:bg-white\\/50 {\n &:disabled {\n background-color: color-mix(in srgb, #fff 50%, transparent);\n @supports (color: color-mix(in lab, red, red)) {\n background-color: color-mix(in oklab, var(--color-white) 50%, transparent);\n }\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n .lg\\:h-18 {\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n }\n .lg\\:w-18 {\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n }\n .lg\\:pb-12 {\n @media (width >= 64rem) {\n padding-bottom: calc(var(--spacing) * 12);\n }\n }\n .lg\\:text-2xl {\n @media (width >= 64rem) {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n }\n .lg\\:text-3xl {\n @media (width >= 64rem) {\n font-size: var(--text-3xl);\n line-height: var(--tw-leading, var(--text-3xl--line-height));\n }\n }\n .lg\\:text-base {\n @media (width >= 64rem) {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n }\n}\n.noCursor {\n cursor: none !important;\n}\n.icon-class {\n height: calc(var(--spacing) * 14);\n width: calc(var(--spacing) * 14);\n cursor: pointer;\n color: var(--color-gray-400);\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n --tw-duration: 200ms;\n transition-duration: 200ms;\n &:hover {\n @media (hover: hover) {\n color: var(--color-gray-200);\n }\n }\n @media (width >= 64rem) {\n height: calc(var(--spacing) * 18);\n }\n @media (width >= 64rem) {\n width: calc(var(--spacing) * 18);\n }\n}\n@property --tw-translate-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-space-y-reverse {\n syntax: \"*\";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-border-style {\n syntax: \"*\";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-gradient-position {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-via {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-to {\n syntax: \"<color>\";\n inherits: false;\n initial-value: #0000;\n}\n@property --tw-gradient-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-via-stops {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-gradient-from-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 0%;\n}\n@property --tw-gradient-via-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 50%;\n}\n@property --tw-gradient-to-position {\n syntax: \"<length-percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-font-weight {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: \"<percentage>\";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: \"<length>\";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: \"*\";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: \"*\";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-backdrop-blur {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-brightness {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-contrast {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-grayscale {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-hue-rotate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-invert {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-opacity {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-saturate {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-backdrop-sepia {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-duration {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-ease {\n syntax: \"*\";\n inherits: false;\n}\n@property --tw-scale-x {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-y {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@property --tw-scale-z {\n syntax: \"*\";\n inherits: false;\n initial-value: 1;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-space-y-reverse: 0;\n --tw-border-style: solid;\n --tw-gradient-position: initial;\n --tw-gradient-from: #0000;\n --tw-gradient-via: #0000;\n --tw-gradient-to: #0000;\n --tw-gradient-stops: initial;\n --tw-gradient-via-stops: initial;\n --tw-gradient-from-position: 0%;\n --tw-gradient-via-position: 50%;\n --tw-gradient-to-position: 100%;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-backdrop-blur: initial;\n --tw-backdrop-brightness: initial;\n --tw-backdrop-contrast: initial;\n --tw-backdrop-grayscale: initial;\n --tw-backdrop-hue-rotate: initial;\n --tw-backdrop-invert: initial;\n --tw-backdrop-opacity: initial;\n --tw-backdrop-saturate: initial;\n --tw-backdrop-sepia: initial;\n --tw-duration: initial;\n --tw-ease: initial;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-scale-z: 1;\n }\n }\n}\n";
|
|
40
40
|
styleInject(css_248z$4,{"insertAt":"top"});
|
|
41
41
|
|
|
42
42
|
const createVideoRefsSlice = (set) => ({
|
|
@@ -138,6 +138,12 @@ const createAdsSlice = (set, get) => ({
|
|
|
138
138
|
setAdVideoRef: (adVideoRef) => set({ adVideoRef }),
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
const createErrorSlice = (set) => ({
|
|
142
|
+
error: null,
|
|
143
|
+
setError: (error) => set({ error }),
|
|
144
|
+
clearError: () => set({ error: null }),
|
|
145
|
+
});
|
|
146
|
+
|
|
141
147
|
const createResetSlice = (set, get) => ({
|
|
142
148
|
resetStore: () => {
|
|
143
149
|
const safeStopMediaElement = (media) => {
|
|
@@ -211,6 +217,7 @@ const useVideoStore = create()((set, get, store) => ({
|
|
|
211
217
|
...createEpisodesSlice(set),
|
|
212
218
|
...createIntroSlice(set),
|
|
213
219
|
...createAdsSlice(set),
|
|
220
|
+
...createErrorSlice(set),
|
|
214
221
|
...createResetSlice(set, get),
|
|
215
222
|
}));
|
|
216
223
|
|
|
@@ -635,7 +642,22 @@ const VideoSeekSlider = ({ max = 1000, currentTime = 0, bufferTime = 0, hideThum
|
|
|
635
642
|
React__default.createElement(Thumb, { max: max, currentTime: currentTime, isThumbActive: isThumbActive, trackColor: trackColor })));
|
|
636
643
|
};
|
|
637
644
|
|
|
638
|
-
const
|
|
645
|
+
const formatTimeMemo = (() => {
|
|
646
|
+
const cache = new Map();
|
|
647
|
+
return (seconds) => {
|
|
648
|
+
if (cache.has(seconds))
|
|
649
|
+
return cache.get(seconds);
|
|
650
|
+
const formatted = timeFormat(seconds);
|
|
651
|
+
cache.set(seconds, formatted);
|
|
652
|
+
if (cache.size > 100) {
|
|
653
|
+
const firstKey = cache.keys().next().value;
|
|
654
|
+
if (firstKey !== undefined)
|
|
655
|
+
cache.delete(firstKey);
|
|
656
|
+
}
|
|
657
|
+
return formatted;
|
|
658
|
+
};
|
|
659
|
+
})();
|
|
660
|
+
const BottomControls = memo(({ config }) => {
|
|
639
661
|
const { videoRef, currentTime, isFullscreen, bufferedProgress, isAdPlaying } = useVideoStore(useShallow((state) => ({
|
|
640
662
|
videoRef: state.videoRef,
|
|
641
663
|
currentTime: state.currentTime,
|
|
@@ -647,31 +669,33 @@ const BottomControls = ({ config }) => {
|
|
|
647
669
|
const currentTimeValue = currentTime || 0;
|
|
648
670
|
const bufferedValue = bufferedProgress || 0;
|
|
649
671
|
const handleSeek = useCallback((currentTimeInMs) => {
|
|
650
|
-
if (!videoRef)
|
|
672
|
+
if (!videoRef)
|
|
651
673
|
return;
|
|
652
|
-
}
|
|
653
674
|
videoRef.currentTime = currentTimeInMs / 1000;
|
|
654
675
|
}, [videoRef]);
|
|
655
676
|
const bufferTime = useMemo(() => {
|
|
656
|
-
if (!duration)
|
|
677
|
+
if (!duration)
|
|
657
678
|
return 0;
|
|
658
|
-
}
|
|
659
679
|
return secondsToMilliseconds(duration * (bufferedValue / 100));
|
|
660
680
|
}, [bufferedValue, duration]);
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
681
|
+
const roundedCurrentTime = useMemo(() => Math.floor(currentTimeValue), [currentTimeValue]);
|
|
682
|
+
const roundedDuration = useMemo(() => Math.floor(duration), [duration]);
|
|
683
|
+
const durationFormatted = useMemo(() => formatTimeMemo(roundedDuration), [roundedDuration]);
|
|
684
|
+
const currentTimeFormatted = useMemo(() => formatTimeMemo(roundedCurrentTime), [roundedCurrentTime]);
|
|
685
|
+
const seekSliderMax = useMemo(() => secondsToMilliseconds(duration), [duration]);
|
|
686
|
+
const seekSliderCurrentTime = useMemo(() => secondsToMilliseconds(currentTimeValue), [currentTimeValue]);
|
|
687
|
+
if (isAdPlaying)
|
|
664
688
|
return null;
|
|
665
|
-
}
|
|
666
689
|
return (React__default.createElement("div", { className: "px-10" },
|
|
667
|
-
React__default.createElement(VideoSeekSlider, { max:
|
|
690
|
+
React__default.createElement(VideoSeekSlider, { max: seekSliderMax, currentTime: seekSliderCurrentTime, bufferTime: bufferTime, onChange: handleSeek, secondsPrefix: "00:00:", minutesPrefix: "00:", getPreviewScreenUrl: config?.seekBarConfig?.getPreviewScreenUrl, timeCodes: config?.seekBarConfig?.timeCodes, trackColor: config?.seekBarConfig?.trackColor }),
|
|
668
691
|
React__default.createElement("div", { className: `pt-6 ${isFullscreen ? "pb-10" : "pb-16"} lg:pb-12 flex items-center gap-4 text-white` },
|
|
669
692
|
React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-white cursor-pointer hover:text-gray-200 transition-colors duration-200" }, currentTimeFormatted),
|
|
670
693
|
React__default.createElement("span", { className: "text-lg lg:text-3xl font-semibold text-gray-500 cursor-pointer hover:text-gray-200 transition-colors duration-200" }, "/"),
|
|
671
694
|
React__default.createElement("span", { className: "text-lg lg:text-2xl font-semibold text-gray-400 cursor-pointer hover:text-gray-200 transition-colors duration-200" }, durationFormatted))));
|
|
672
|
-
};
|
|
695
|
+
});
|
|
696
|
+
BottomControls.displayName = "BottomControls";
|
|
673
697
|
|
|
674
|
-
var css_248z$2 = ".icon-button {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.icon-button:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .icon-button {\n width: 32px;\n height: 32px;\n }\n}\n\n.fullscreen-icon {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.fullscreen-icon:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .fullscreen-icon {\n width: 32px;\n height: 32px;\n }\n}\n\n.pip-toggle {\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.pip-toggle:hover {\n color: #e5e7eb;\n}\n\n.pip-icon {\n width: 15px;\n height: 15px;\n}\n\n@media (min-width: 1024px) {\n .pip-icon {\n width: 28px;\n height: 28px;\n }\n}\n";
|
|
698
|
+
var css_248z$2 = ".icon-button {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.icon-button:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .icon-button {\n width: 32px;\n height: 32px;\n }\n}\n\n.fullscreen-icon {\n width: 20px;\n height: 20px;\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.fullscreen-icon:hover {\n color: #e5e7eb;\n}\n\n@media (min-width: 1024px) {\n .fullscreen-icon {\n width: 32px;\n height: 32px;\n }\n}\n\n.pip-toggle {\n cursor: pointer;\n color: #9ca3af;\n transition: color 0.2s ease-in-out;\n}\n\n.pip-toggle:hover {\n color: #e5e7eb;\n}\n\n.pip-icon {\n width: 15px;\n height: 15px;\n}\n\n@media (min-width: 1024px) {\n .pip-icon {\n width: 28px;\n height: 28px;\n }\n}\n\n/* Ad Badge Header - Dark/Black Background */\n.ad-badge-header {\n background: rgba(45, 47, 49, 0.95);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n box-shadow: \n 0 4px 16px rgba(0, 0, 0, 0.4),\n 0 0 0 1px rgba(255, 255, 255, 0.1);\n border: 1px solid rgba(255, 255, 255, 0.1);\n position: relative;\n}\n\n/* Time display in badge - lighter gray */\n.ad-badge-header > span:last-child {\n color: rgba(209, 213, 219, 0.9);\n font-weight: 500;\n letter-spacing: 0.025em;\n}\n\n/* Pulse animation for the dot */\n@keyframes pulse-dot {\n 0%, 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.7;\n transform: scale(0.9);\n }\n}\n\n.ad-badge-header .animate-pulse {\n animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n/* Responsive adjustments */\n@media (max-width: 768px) {\n .ad-badge-header {\n padding: 0.375rem 0.75rem;\n font-size: 0.625rem;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .ad-badge-header .animate-pulse {\n animation: none;\n }\n}\n\n@media (prefers-contrast: high) {\n .ad-badge-header {\n background: #2d2f31;\n border: 2px solid #fff;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n }\n}";
|
|
675
699
|
styleInject(css_248z$2,{"insertAt":"top"});
|
|
676
700
|
|
|
677
701
|
const FullScreenToggle = ({ isFullScreen, onClick, className = "fullscreen-icon", }) => {
|
|
@@ -691,7 +715,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
|
|
|
691
715
|
const [isOpen, setIsOpen] = useState(false);
|
|
692
716
|
const popoverRef = useRef(null);
|
|
693
717
|
const buttonRef = useRef(null);
|
|
694
|
-
// Close on outside click or Escape key
|
|
695
718
|
useEffect(() => {
|
|
696
719
|
const handleClickOutside = (event) => {
|
|
697
720
|
if (popoverRef.current &&
|
|
@@ -716,7 +739,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
|
|
|
716
739
|
const togglePopover = () => {
|
|
717
740
|
setIsOpen((prev) => (closeOnButtonClick ? !prev : true));
|
|
718
741
|
};
|
|
719
|
-
// Get alignment classes
|
|
720
742
|
const getAlignmentClasses = () => {
|
|
721
743
|
switch (align) {
|
|
722
744
|
case "center":
|
|
@@ -728,7 +750,6 @@ const Popover = ({ button, children, closeOnButtonClick = false, className = "",
|
|
|
728
750
|
return "left-0";
|
|
729
751
|
}
|
|
730
752
|
};
|
|
731
|
-
// Arrow is always centered regardless of popover alignment
|
|
732
753
|
const getArrowPositionClasses = () => {
|
|
733
754
|
return "left-1/2 -translate-x-1/2";
|
|
734
755
|
};
|
|
@@ -752,15 +773,39 @@ const Tooltip = ({ children, title, position = "top", className, }) => {
|
|
|
752
773
|
visible && (React__default.createElement("div", { className: `absolute z-50 px-3 py-1 text-sm text-white bg-gray-900 rounded-md shadow-md transition-opacity duration-200 ease-in-out whitespace-nowrap ${positionStyles[position]}` }, title))));
|
|
753
774
|
};
|
|
754
775
|
|
|
755
|
-
const
|
|
776
|
+
const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
777
|
+
const Settings = ({ iconClassName, qualityConfig, }) => {
|
|
778
|
+
const showQualityInSettings = qualityConfig?.showInSettings !== false;
|
|
756
779
|
const { qualityLevels, activeQuality, currentQuality, subtitles, activeSubtitle, setActiveSubtitle, videoRef, streamType, } = useVideoStore();
|
|
757
|
-
const
|
|
780
|
+
const getStoredPlaybackSpeed = () => {
|
|
781
|
+
try {
|
|
782
|
+
const stored = localStorage.getItem("react-player-playback-speed");
|
|
783
|
+
if (stored) {
|
|
784
|
+
const speed = parseFloat(stored);
|
|
785
|
+
if (speedOptions.includes(speed))
|
|
786
|
+
return speed;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
catch { }
|
|
790
|
+
return 1;
|
|
791
|
+
};
|
|
792
|
+
const [speed, setSpeed] = React.useState(getStoredPlaybackSpeed());
|
|
758
793
|
const [activeMenu, setActiveMenu] = React.useState("main");
|
|
794
|
+
React.useEffect(() => {
|
|
795
|
+
if (videoRef) {
|
|
796
|
+
const storedSpeed = getStoredPlaybackSpeed();
|
|
797
|
+
videoRef.playbackRate = storedSpeed;
|
|
798
|
+
setSpeed(storedSpeed);
|
|
799
|
+
}
|
|
800
|
+
}, [videoRef]);
|
|
759
801
|
const handleSpeedChange = (newSpeed) => {
|
|
760
802
|
setSpeed(newSpeed);
|
|
761
|
-
if (videoRef)
|
|
803
|
+
if (videoRef)
|
|
762
804
|
videoRef.playbackRate = newSpeed;
|
|
805
|
+
try {
|
|
806
|
+
localStorage.setItem("react-player-playback-speed", newSpeed.toString());
|
|
763
807
|
}
|
|
808
|
+
catch { }
|
|
764
809
|
};
|
|
765
810
|
const isAdaptiveStream = streamType === "hls" || streamType === "dash";
|
|
766
811
|
const qualityOptions = React.useMemo(() => {
|
|
@@ -785,27 +830,23 @@ const Settings = ({ iconClassName }) => {
|
|
|
785
830
|
return b.originalIndex - a.originalIndex;
|
|
786
831
|
});
|
|
787
832
|
}, [qualityLevels, isAdaptiveStream, streamType]);
|
|
788
|
-
const speedOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
789
833
|
const handleBack = () => setActiveMenu("main");
|
|
790
834
|
const formatBitrate = (bitrate) => {
|
|
791
835
|
if (!bitrate || bitrate <= 0)
|
|
792
836
|
return "";
|
|
793
|
-
if (bitrate >= 1000000)
|
|
837
|
+
if (bitrate >= 1000000)
|
|
794
838
|
return `${(bitrate / 1000000).toFixed(1)} Mbps`;
|
|
795
|
-
}
|
|
796
839
|
return `${Math.round(bitrate / 1000)} Kbps`;
|
|
797
840
|
};
|
|
798
|
-
// Get quality label: show explicit resolution to avoid duplicates
|
|
799
841
|
const getQualityName = (height, bitrate) => {
|
|
800
842
|
if (height && height > 0)
|
|
801
843
|
return `${height}p`;
|
|
802
844
|
const bitrateLabel = formatBitrate(bitrate);
|
|
803
845
|
return bitrateLabel || "Quality";
|
|
804
846
|
};
|
|
805
|
-
// Get quality label for display
|
|
806
847
|
const getQualityLabel = () => {
|
|
807
|
-
if (!isAdaptiveStream)
|
|
808
|
-
return "
|
|
848
|
+
if (!isAdaptiveStream || qualityOptions.length === 0)
|
|
849
|
+
return "Off";
|
|
809
850
|
if (currentQuality === "auto")
|
|
810
851
|
return "Auto";
|
|
811
852
|
const option = qualityOptions.find((q) => q.value === currentQuality);
|
|
@@ -814,15 +855,12 @@ const Settings = ({ iconClassName }) => {
|
|
|
814
855
|
const label = getQualityName(option.height, option.bitrate);
|
|
815
856
|
return label === "Quality" ? "Custom" : label;
|
|
816
857
|
};
|
|
817
|
-
|
|
858
|
+
const hasQualityOptions = isAdaptiveStream && qualityOptions.length > 0;
|
|
818
859
|
const getDataUsage = (height, bitrate) => {
|
|
819
|
-
// bitrate in bits/sec -> GB/hour
|
|
820
860
|
if (bitrate && bitrate > 0) {
|
|
821
861
|
const gbPerHour = (bitrate * 3600) / 8 / 1e9;
|
|
822
|
-
|
|
823
|
-
return `Uses about ${rounded} GB per hour`;
|
|
862
|
+
return `Uses about ${gbPerHour.toFixed(2)} GB per hour`;
|
|
824
863
|
}
|
|
825
|
-
// Fallback by resolution when bitrate missing
|
|
826
864
|
if (height >= 2160)
|
|
827
865
|
return "Uses about 7.00 GB per hour";
|
|
828
866
|
if (height >= 1440)
|
|
@@ -844,7 +882,7 @@ const Settings = ({ iconClassName }) => {
|
|
|
844
882
|
React.createElement("h3", { className: "text-white font-bold text-xl mb-4" }, "Settings"),
|
|
845
883
|
React.createElement("p", { className: "text-gray-300 text-sm mb-4" }, "Customize playback"),
|
|
846
884
|
React.createElement("div", { className: "space-y-0 border-t border-gray-600" },
|
|
847
|
-
React.createElement("button", { onClick: () => setActiveMenu("quality"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
|
|
885
|
+
showQualityInSettings && (React.createElement("button", { onClick: () => setActiveMenu("quality"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
|
|
848
886
|
React.createElement("div", { className: "flex items-center gap-3" },
|
|
849
887
|
React.createElement("div", { className: "p-2 bg-blue-500 rounded-md" },
|
|
850
888
|
React.createElement("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
@@ -852,7 +890,7 @@ const Settings = ({ iconClassName }) => {
|
|
|
852
890
|
React.createElement("div", { className: "text-left" },
|
|
853
891
|
React.createElement("div", { className: "text-white font-semibold" }, "Quality"),
|
|
854
892
|
React.createElement("div", { className: "text-gray-400 text-sm" }, getQualityLabel()))),
|
|
855
|
-
React.createElement(ChevronRight, { className: "w-5 h-5 text-gray-400" })),
|
|
893
|
+
React.createElement(ChevronRight, { className: "w-5 h-5 text-gray-400" }))),
|
|
856
894
|
React.createElement("button", { onClick: () => setActiveMenu("subtitles"), className: "w-full flex items-center justify-between py-4 border-b border-gray-600 rounded-[5px] transition-colors" },
|
|
857
895
|
React.createElement("div", { className: "flex items-center gap-3" },
|
|
858
896
|
React.createElement("div", { className: "p-2 bg-purple-500 rounded-md" },
|
|
@@ -878,30 +916,26 @@ const Settings = ({ iconClassName }) => {
|
|
|
878
916
|
React.createElement("button", { onClick: handleBack, className: "p-1 rounded-md transition-colors" },
|
|
879
917
|
React.createElement(ChevronRight, { className: "w-6 h-6 text-white rotate-180" })),
|
|
880
918
|
React.createElement("h3", { className: "text-white font-bold text-xl" }, "Video Quality")),
|
|
881
|
-
React.createElement("div", { className: "space-y-3" },
|
|
882
|
-
React.createElement("button", { onClick: () => {
|
|
883
|
-
if (isAdaptiveStream) {
|
|
884
|
-
QualityManager.setQuality(streamType, "auto");
|
|
885
|
-
}
|
|
886
|
-
}, disabled: !isAdaptiveStream, className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
|
|
919
|
+
React.createElement("div", { className: "space-y-3" }, hasQualityOptions ? (React.createElement(React.Fragment, null,
|
|
920
|
+
React.createElement("button", { onClick: () => QualityManager.setQuality(streamType, "auto"), className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === "auto"
|
|
887
921
|
? "bg-white/10"
|
|
888
|
-
:
|
|
889
|
-
? "hover:bg-white/5"
|
|
890
|
-
: "opacity-50 cursor-not-allowed"}` },
|
|
922
|
+
: "hover:bg-white/5"}` },
|
|
891
923
|
React.createElement("div", { className: "flex items-start justify-between" },
|
|
892
924
|
React.createElement("div", null,
|
|
893
925
|
React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, "Auto"),
|
|
894
926
|
React.createElement("div", { className: "text-gray-400 text-sm" }, "Adjust to your connection")),
|
|
895
927
|
activeQuality === "auto" && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" })))),
|
|
896
|
-
|
|
928
|
+
qualityOptions.map((level) => (React.createElement("button", { key: level.value, onClick: () => QualityManager.setQuality(streamType, level.value), className: `w-full text-left px-4 py-3 rounded-md transition-all ${activeQuality === level.value
|
|
897
929
|
? "bg-white/10"
|
|
898
930
|
: "hover:bg-white/5"}` },
|
|
899
931
|
React.createElement("div", { className: "flex items-start justify-between" },
|
|
900
932
|
React.createElement("div", null,
|
|
901
933
|
React.createElement("div", { className: "text-white font-semibold text-lg mb-1" }, getQualityName(level.height, level.bitrate)),
|
|
902
934
|
React.createElement("div", { className: "text-gray-400 text-sm" }, getDataUsage(level.height, level.bitrate))),
|
|
903
|
-
|
|
904
|
-
|
|
935
|
+
activeQuality === level.value && (React.createElement(Check, { className: "w-6 h-6 text-white mt-1" })))))))) : (React.createElement("button", { className: "w-full text-left px-4 py-3 rounded-md bg-white/10 cursor-default" },
|
|
936
|
+
React.createElement("div", { className: "flex items-start justify-between" },
|
|
937
|
+
React.createElement("span", { className: "text-white font-semibold text-lg" }, "Off"),
|
|
938
|
+
React.createElement(Check, { className: "w-6 h-6 text-white mt-1" }))))))),
|
|
905
939
|
activeMenu === "subtitles" && (React.createElement("div", { className: "p-4" },
|
|
906
940
|
React.createElement("div", { className: "flex items-center gap-3 mb-4" },
|
|
907
941
|
React.createElement("button", { onClick: handleBack, className: "p-1 hover:bg-white/10 rounded-md transition-colors" },
|
|
@@ -910,7 +944,7 @@ const Settings = ({ iconClassName }) => {
|
|
|
910
944
|
React.createElement("div", { className: "space-y-3" },
|
|
911
945
|
React.createElement("button", { onClick: () => setActiveSubtitle(null), className: `w-full text-left px-4 py-3 rounded-[5px] transition-all flex items-center justify-between ${!activeSubtitle ? "bg-[#454545]" : ""}` },
|
|
912
946
|
React.createElement("span", { className: "text-white font-semibold text-lg" }, "Off"),
|
|
913
|
-
!activeSubtitle && React.createElement(Check, { className: "w-6 h-6 text-white" })),
|
|
947
|
+
!activeSubtitle && (React.createElement(Check, { className: "w-6 h-6 text-white" }))),
|
|
914
948
|
subtitles?.map((subtitle, index) => (React.createElement("button", { key: index, onClick: () => setActiveSubtitle(subtitle), className: `w-full text-left px-4 py-3 rounded-md transition-all flex items-center justify-between ${activeSubtitle?.label === subtitle.label
|
|
915
949
|
? "bg-white/10"
|
|
916
950
|
: "hover:bg-white/5"}` },
|
|
@@ -923,12 +957,12 @@ const Settings = ({ iconClassName }) => {
|
|
|
923
957
|
React.createElement("h3", { className: "text-white font-bold text-xl" }, "Playback Speed")),
|
|
924
958
|
React.createElement("div", { className: "space-y-3 max-h-80 overflow-y-auto" }, speedOptions.map((s) => (React.createElement("button", { key: s, onClick: () => handleSpeedChange(s), className: `w-full text-left px-4 py-3 rounded-[5px] transition-all flex items-center justify-between ${speed === s ? "bg-[#454545]" : ""}` },
|
|
925
959
|
React.createElement("span", { className: "text-white font-semibold text-lg" }, s === 1 ? "Normal" : `${s}x`),
|
|
926
|
-
speed === s && React.createElement(Check, { className: "w-6 h-6 text-white" })))))))))));
|
|
960
|
+
speed === s && (React.createElement(Check, { className: "w-6 h-6 text-white" }))))))))))));
|
|
927
961
|
};
|
|
928
962
|
|
|
929
963
|
const ControlsHeader = ({ config }) => {
|
|
930
964
|
const iconClassName = "icon-button";
|
|
931
|
-
const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, } = useVideoStore(useShallow((state) => ({
|
|
965
|
+
const { videoWrapperRef, videoRef, adVideoRef, episodeList, currentEpisodeIndex, resetStore, isAdPlaying, muted, setMuted, adCurrentTime, } = useVideoStore(useShallow((state) => ({
|
|
932
966
|
videoWrapperRef: state.videoWrapperRef,
|
|
933
967
|
videoRef: state.videoRef,
|
|
934
968
|
adVideoRef: state.adVideoRef,
|
|
@@ -938,7 +972,40 @@ const ControlsHeader = ({ config }) => {
|
|
|
938
972
|
isAdPlaying: state.isAdPlaying,
|
|
939
973
|
muted: state.muted,
|
|
940
974
|
setMuted: state.setMuted,
|
|
975
|
+
adCurrentTime: state.adCurrentTime,
|
|
941
976
|
})));
|
|
977
|
+
const [adDuration, setAdDuration] = React.useState(0);
|
|
978
|
+
React.useEffect(() => {
|
|
979
|
+
if (!adVideoRef || !isAdPlaying) {
|
|
980
|
+
setAdDuration(0);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const updateDuration = () => {
|
|
984
|
+
if (adVideoRef.duration && Number.isFinite(adVideoRef.duration)) {
|
|
985
|
+
setAdDuration(adVideoRef.duration);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
updateDuration();
|
|
989
|
+
adVideoRef.addEventListener("loadedmetadata", updateDuration);
|
|
990
|
+
adVideoRef.addEventListener("durationchange", updateDuration);
|
|
991
|
+
return () => {
|
|
992
|
+
adVideoRef.removeEventListener("loadedmetadata", updateDuration);
|
|
993
|
+
adVideoRef.removeEventListener("durationchange", updateDuration);
|
|
994
|
+
};
|
|
995
|
+
}, [adVideoRef, isAdPlaying]);
|
|
996
|
+
const formatTime = React.useCallback((seconds) => {
|
|
997
|
+
if (isNaN(seconds) || seconds < 0)
|
|
998
|
+
return "0:00";
|
|
999
|
+
const mins = Math.floor(seconds / 60);
|
|
1000
|
+
const secs = Math.floor(seconds % 60);
|
|
1001
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
1002
|
+
}, []);
|
|
1003
|
+
const adTimeRemaining = React.useMemo(() => {
|
|
1004
|
+
if (adDuration <= 0 || adCurrentTime <= 0)
|
|
1005
|
+
return null;
|
|
1006
|
+
const remaining = Math.max(0, adDuration - adCurrentTime);
|
|
1007
|
+
return formatTime(remaining);
|
|
1008
|
+
}, [adDuration, adCurrentTime, formatTime]);
|
|
942
1009
|
const [isPipActive, setIsPipActive] = React.useState(false);
|
|
943
1010
|
const [isFullscreen, setIsFullscreen] = React.useState(false);
|
|
944
1011
|
const handleFullscreen = () => {
|
|
@@ -986,7 +1053,7 @@ const ControlsHeader = ({ config }) => {
|
|
|
986
1053
|
setIsPipActive(false);
|
|
987
1054
|
}
|
|
988
1055
|
}
|
|
989
|
-
catch
|
|
1056
|
+
catch { }
|
|
990
1057
|
};
|
|
991
1058
|
React.useEffect(() => {
|
|
992
1059
|
const handlePipChange = () => setIsPipActive(!!document.pictureInPictureElement);
|
|
@@ -999,12 +1066,12 @@ const ControlsHeader = ({ config }) => {
|
|
|
999
1066
|
}, []);
|
|
1000
1067
|
const handleClose = () => {
|
|
1001
1068
|
resetStore();
|
|
1002
|
-
|
|
1003
|
-
config.onClose();
|
|
1004
|
-
}
|
|
1069
|
+
config?.onClose?.();
|
|
1005
1070
|
};
|
|
1006
1071
|
const renderAdHeader = () => (React.createElement("div", { className: "flex items-center gap-4" },
|
|
1007
|
-
React.createElement("span", { className: "inline-flex items-center rounded
|
|
1072
|
+
React.createElement("span", { className: "inline-flex items-center gap-1 rounded-full px-4 py-2 font-medium text-xs tracking-wider text-red-600 border border-gray-700/60" },
|
|
1073
|
+
React.createElement("span", null, "Ad"),
|
|
1074
|
+
adTimeRemaining && (React.createElement("span", { className: "text-gray-300 font-normal normal-case ml-1 text-xs" }, adTimeRemaining)))));
|
|
1008
1075
|
const renderVideoHeader = () => (React.createElement("div", { className: "flex" },
|
|
1009
1076
|
React.createElement("div", null,
|
|
1010
1077
|
React.createElement("h1", { className: "text-gray-200 text-lg lg:text-2xl font-semibold" }, episodeList.length > 0
|
|
@@ -1014,7 +1081,7 @@ const ControlsHeader = ({ config }) => {
|
|
|
1014
1081
|
return (React.createElement("div", { className: "flex items-center justify-between p-10 bg-linear-to-b from-black" },
|
|
1015
1082
|
isAdPlaying ? renderAdHeader() : renderVideoHeader(),
|
|
1016
1083
|
React.createElement("div", { className: "flex items-center gap-7 text-white" },
|
|
1017
|
-
!isAdPlaying && React.createElement(Settings, { iconClassName: iconClassName }),
|
|
1084
|
+
!isAdPlaying && (React.createElement(Settings, { iconClassName: iconClassName, qualityConfig: config?.qualityConfig })),
|
|
1018
1085
|
React.createElement("div", { onClick: handleMute }, muted ? (React.createElement(Tooltip, { title: "Unmute" },
|
|
1019
1086
|
React.createElement(IoVolumeMuteOutline, { className: iconClassName }))) : (React.createElement(Tooltip, { title: "Mute" },
|
|
1020
1087
|
React.createElement(IoVolumeHighOutline, { className: iconClassName })))),
|
|
@@ -1062,7 +1129,10 @@ const MiddleControls = () => {
|
|
|
1062
1129
|
setIsPlaying: state.setIsPlaying,
|
|
1063
1130
|
isAdPlaying: state.isAdPlaying,
|
|
1064
1131
|
})));
|
|
1065
|
-
const
|
|
1132
|
+
const { setIsBuffering } = useVideoStore(useShallow((state) => ({
|
|
1133
|
+
setIsBuffering: state.setIsBuffering,
|
|
1134
|
+
})));
|
|
1135
|
+
const [isBuffering, setIsBufferingLocal] = useState(false);
|
|
1066
1136
|
const videoElement = isAdPlaying ? adVideoRef : videoRef;
|
|
1067
1137
|
const resetControlsVisibility = useCallback(() => {
|
|
1068
1138
|
if (typeof window === "undefined") {
|
|
@@ -1103,15 +1173,33 @@ const MiddleControls = () => {
|
|
|
1103
1173
|
useEffect(() => {
|
|
1104
1174
|
if (!videoElement)
|
|
1105
1175
|
return;
|
|
1106
|
-
const handleWaiting = () =>
|
|
1107
|
-
|
|
1176
|
+
const handleWaiting = () => {
|
|
1177
|
+
setIsBufferingLocal(true);
|
|
1178
|
+
setIsBuffering(true);
|
|
1179
|
+
};
|
|
1180
|
+
const handlePlaying = () => {
|
|
1181
|
+
setIsBufferingLocal(false);
|
|
1182
|
+
setIsBuffering(false);
|
|
1183
|
+
};
|
|
1184
|
+
const handleCanPlay = () => {
|
|
1185
|
+
setIsBufferingLocal(false);
|
|
1186
|
+
setIsBuffering(false);
|
|
1187
|
+
};
|
|
1188
|
+
const handleStalled = () => {
|
|
1189
|
+
setIsBufferingLocal(true);
|
|
1190
|
+
setIsBuffering(true);
|
|
1191
|
+
};
|
|
1108
1192
|
videoElement.addEventListener("waiting", handleWaiting);
|
|
1109
1193
|
videoElement.addEventListener("playing", handlePlaying);
|
|
1194
|
+
videoElement.addEventListener("canplay", handleCanPlay);
|
|
1195
|
+
videoElement.addEventListener("stalled", handleStalled);
|
|
1110
1196
|
return () => {
|
|
1111
1197
|
videoElement.removeEventListener("waiting", handleWaiting);
|
|
1112
1198
|
videoElement.removeEventListener("playing", handlePlaying);
|
|
1199
|
+
videoElement.removeEventListener("canplay", handleCanPlay);
|
|
1200
|
+
videoElement.removeEventListener("stalled", handleStalled);
|
|
1113
1201
|
};
|
|
1114
|
-
}, [videoElement, isAdPlaying]);
|
|
1202
|
+
}, [videoElement, isAdPlaying, setIsBuffering]);
|
|
1115
1203
|
useEffect(() => {
|
|
1116
1204
|
const handleKeyDown = (e) => {
|
|
1117
1205
|
if (!videoElement || isAdPlaying)
|
|
@@ -1143,11 +1231,12 @@ const MiddleControls = () => {
|
|
|
1143
1231
|
]);
|
|
1144
1232
|
if (isAdPlaying) {
|
|
1145
1233
|
return (React__default.createElement("div", { className: "flex justify-center items-center" },
|
|
1146
|
-
React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(
|
|
1234
|
+
React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement("div", { className: "relative" },
|
|
1235
|
+
React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" }))) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) })));
|
|
1147
1236
|
}
|
|
1148
1237
|
return (React__default.createElement("div", { className: "flex justify-center items-center" },
|
|
1149
1238
|
React__default.createElement(ControlButton, { onClick: handleBackward, className: "w-[15vw]", icon: React__default.createElement(BackwardIcon, null) }),
|
|
1150
|
-
React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(Loader, { className: "w-
|
|
1239
|
+
React__default.createElement(ControlButton, { onClick: handlePlayPause, className: "w-[10vw]", icon: isBuffering ? (React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" })) : isPlaying ? (React__default.createElement(PauseIcon, null)) : (React__default.createElement(PlayIcon, null)) }),
|
|
1151
1240
|
React__default.createElement(ControlButton, { onClick: handleForward, className: "w-[15vw]", icon: React__default.createElement(ForwardIcon, null) })));
|
|
1152
1241
|
};
|
|
1153
1242
|
|
|
@@ -1159,8 +1248,7 @@ const VideoPlayerControls = ({ config }) => {
|
|
|
1159
1248
|
React.createElement(BottomControls, { config: config?.bottomConfig?.config }))));
|
|
1160
1249
|
};
|
|
1161
1250
|
|
|
1162
|
-
const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "left", }) => {
|
|
1163
|
-
// Increase icon size and apply consistent color to icon
|
|
1251
|
+
const VideoActionButton = React__default.memo(({ text, onClick, icon, disabled = false, position = "left", }) => {
|
|
1164
1252
|
const renderedIcon = icon
|
|
1165
1253
|
? React__default.cloneElement(icon, {
|
|
1166
1254
|
className: "h-5 w-5 text-gray-900",
|
|
@@ -1170,9 +1258,10 @@ const VideoActionButton = ({ text, onClick, icon, disabled = false, position = "
|
|
|
1170
1258
|
React__default.createElement("button", { onClick: onClick, disabled: disabled, className: "\n bg-white/80 text-gray-900 font-semibold px-6 py-2 \n rounded-md text-sm flex items-center\n backdrop-blur-sm shadow-md\n hover:bg-white/90\n transition\n focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-gray-400\n disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-white/50\n " },
|
|
1171
1259
|
renderedIcon && React__default.createElement("span", { className: "inline mr-2" }, renderedIcon),
|
|
1172
1260
|
text)));
|
|
1173
|
-
};
|
|
1261
|
+
});
|
|
1262
|
+
VideoActionButton.displayName = "VideoActionButton";
|
|
1174
1263
|
|
|
1175
|
-
const Overlay = ({ config }) => {
|
|
1264
|
+
const Overlay = React__default.memo(({ config }) => {
|
|
1176
1265
|
const controlsTimerRef = useRef(null);
|
|
1177
1266
|
const containerRef = useRef(null);
|
|
1178
1267
|
const { setControls, controls, showCountdown, countdownTime, setShowCountdown, setAutoPlayNext, setCurrentEpisodeIndex, episodeList, setCountdownTime, videoRef, currentEpisodeIndex, isAdPlaying, } = useVideoStore(useShallow((state) => ({
|
|
@@ -1219,7 +1308,9 @@ const Overlay = ({ config }) => {
|
|
|
1219
1308
|
let timer;
|
|
1220
1309
|
if (showCountdown && countdownTime > 0 && episodeList.length > 0) {
|
|
1221
1310
|
timer = setInterval(() => {
|
|
1222
|
-
|
|
1311
|
+
const currentTime = useVideoStore.getState().countdownTime;
|
|
1312
|
+
const next = currentTime - 1;
|
|
1313
|
+
setCountdownTime(next > 0 ? next : 0);
|
|
1223
1314
|
}, 1000);
|
|
1224
1315
|
}
|
|
1225
1316
|
return () => {
|
|
@@ -1239,7 +1330,7 @@ const Overlay = ({ config }) => {
|
|
|
1239
1330
|
window.removeEventListener(CONTROL_INTERACTION_EVENT, handleExternalInteraction);
|
|
1240
1331
|
};
|
|
1241
1332
|
}, [handleControlsInteraction]);
|
|
1242
|
-
const handleNextEpisodeManually = () => {
|
|
1333
|
+
const handleNextEpisodeManually = useCallback(() => {
|
|
1243
1334
|
const nextIndex = currentEpisodeIndex + 1;
|
|
1244
1335
|
if (nextIndex < episodeList.length && videoRef && episodeList[nextIndex]) {
|
|
1245
1336
|
setCurrentEpisodeIndex(nextIndex);
|
|
@@ -1253,63 +1344,168 @@ const Overlay = ({ config }) => {
|
|
|
1253
1344
|
else if (onClose) {
|
|
1254
1345
|
onClose();
|
|
1255
1346
|
}
|
|
1256
|
-
}
|
|
1347
|
+
}, [
|
|
1348
|
+
currentEpisodeIndex,
|
|
1349
|
+
episodeList,
|
|
1350
|
+
videoRef,
|
|
1351
|
+
setCurrentEpisodeIndex,
|
|
1352
|
+
setAutoPlayNext,
|
|
1353
|
+
setShowCountdown,
|
|
1354
|
+
setCountdownTime,
|
|
1355
|
+
handleControlsInteraction,
|
|
1356
|
+
onClose,
|
|
1357
|
+
]);
|
|
1358
|
+
// Memoize countdown display to prevent unnecessary re-renders
|
|
1359
|
+
const shouldShowCountdown = useMemo(() => showCountdown &&
|
|
1360
|
+
episodeList.length > 0 &&
|
|
1361
|
+
currentEpisodeIndex + 1 < episodeList.length, [showCountdown, episodeList.length, currentEpisodeIndex]);
|
|
1257
1362
|
return (React__default.createElement("div", { id: "videoPlayerControls", ref: containerRef, className: "absolute inset-0", onMouseMove: handleMouseEnter },
|
|
1258
1363
|
controls && !isAdPlaying && React__default.createElement(VideoPlayerControls, { config: config }),
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1364
|
+
shouldShowCountdown && (React__default.createElement(VideoActionButton, { text: "Next Episode", onClick: handleNextEpisodeManually, icon: React__default.createElement(ArrowRight, { className: "h-5 w-5 text-gray-900" }), disabled: currentEpisodeIndex + 1 >= episodeList.length, position: "right" }))));
|
|
1365
|
+
});
|
|
1366
|
+
Overlay.displayName = "Overlay";
|
|
1367
|
+
|
|
1368
|
+
/** Returns true if value is a Tailwind background class (e.g. bg-red, bg-black, bg-transparent) ya CSS "transparent" */
|
|
1369
|
+
const isTailwindBackground = (value) => typeof value === "string" &&
|
|
1370
|
+
(value.trim().startsWith("bg-") || value.trim().toLowerCase() === "transparent");
|
|
1371
|
+
const useSubtitleStyling = (config) => {
|
|
1372
|
+
const { videoRef } = useVideoStore();
|
|
1373
|
+
useEffect(() => {
|
|
1374
|
+
if (!videoRef)
|
|
1375
|
+
return;
|
|
1376
|
+
const applySubtitleStyles = () => {
|
|
1377
|
+
const style = document.createElement("style");
|
|
1378
|
+
style.id = "custom-subtitle-styles";
|
|
1379
|
+
const existingStyle = document.getElementById("custom-subtitle-styles");
|
|
1380
|
+
if (existingStyle) {
|
|
1381
|
+
existingStyle.remove();
|
|
1382
|
+
}
|
|
1383
|
+
const bgRaw = config?.backgroundColor;
|
|
1384
|
+
const useTailwindBg = isTailwindBackground(bgRaw);
|
|
1385
|
+
const cueBackground = useTailwindBg
|
|
1386
|
+
? "transparent"
|
|
1387
|
+
: bgRaw || "linear-gradient(135deg, #fbbf24, #f59e0b)";
|
|
1388
|
+
const styles = `
|
|
1389
|
+
.video-player video::cue {
|
|
1390
|
+
font-size: ${config?.fontSize || "1.75rem"} !important;
|
|
1391
|
+
background: ${cueBackground} !important;
|
|
1392
|
+
color: ${config?.textColor || "#000000"} !important;
|
|
1393
|
+
border-radius: ${config?.borderRadius || "12px"} !important;
|
|
1394
|
+
padding: ${config?.padding || "12px 20px"} !important;
|
|
1395
|
+
max-width: ${config?.maxWidth || "80%"} !important;
|
|
1396
|
+
${config?.position === "top"
|
|
1397
|
+
? "top: 10% !important; bottom: auto !important;"
|
|
1398
|
+
: ""}
|
|
1399
|
+
${config?.position === "center"
|
|
1400
|
+
? "top: 50% !important; bottom: auto !important; transform: translateX(-50%) translateY(-50%) !important;"
|
|
1401
|
+
: ""}
|
|
1402
|
+
${config?.position === "bottom" || !config?.position
|
|
1403
|
+
? "bottom: 15% !important; top: auto !important;"
|
|
1404
|
+
: ""}
|
|
1405
|
+
}
|
|
1406
|
+
`;
|
|
1407
|
+
style.textContent = styles;
|
|
1408
|
+
document.head.appendChild(style);
|
|
1409
|
+
};
|
|
1410
|
+
applySubtitleStyles();
|
|
1411
|
+
return () => {
|
|
1412
|
+
const existingStyle = document.getElementById("custom-subtitle-styles");
|
|
1413
|
+
if (existingStyle) {
|
|
1414
|
+
existingStyle.remove();
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}, [videoRef, config]);
|
|
1262
1418
|
};
|
|
1263
1419
|
|
|
1420
|
+
const getPositionStyles = (position) => {
|
|
1421
|
+
const pos = position || "bottom";
|
|
1422
|
+
switch (pos) {
|
|
1423
|
+
case "top":
|
|
1424
|
+
return {
|
|
1425
|
+
top: "10%",
|
|
1426
|
+
left: "50%",
|
|
1427
|
+
transform: "translateX(-50%)",
|
|
1428
|
+
};
|
|
1429
|
+
case "center":
|
|
1430
|
+
return {
|
|
1431
|
+
top: "50%",
|
|
1432
|
+
left: "50%",
|
|
1433
|
+
transform: "translate(-50%, -50%)",
|
|
1434
|
+
};
|
|
1435
|
+
case "bottom":
|
|
1436
|
+
default:
|
|
1437
|
+
return {
|
|
1438
|
+
bottom: "15%",
|
|
1439
|
+
left: "50%",
|
|
1440
|
+
transform: "translateX(-50%)",
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1264
1444
|
const SubtitleOverlay = ({ styleConfig }) => {
|
|
1265
|
-
const { videoRef, activeSubtitle } = useVideoStore()
|
|
1445
|
+
const { videoRef, activeSubtitle } = useVideoStore(useShallow((state) => ({
|
|
1446
|
+
videoRef: state.videoRef,
|
|
1447
|
+
activeSubtitle: state.activeSubtitle,
|
|
1448
|
+
})));
|
|
1266
1449
|
const [currentSubtitle, setCurrentSubtitle] = useState("");
|
|
1267
1450
|
const [isVisible, setIsVisible] = useState(false);
|
|
1451
|
+
const rafRef = useRef(null);
|
|
1268
1452
|
useEffect(() => {
|
|
1269
1453
|
if (!videoRef)
|
|
1270
1454
|
return;
|
|
1271
1455
|
const handleTimeUpdate = () => {
|
|
1272
|
-
if (
|
|
1273
|
-
setCurrentSubtitle("");
|
|
1274
|
-
setIsVisible(false);
|
|
1456
|
+
if (rafRef.current !== null)
|
|
1275
1457
|
return;
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1458
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
1459
|
+
rafRef.current = null;
|
|
1460
|
+
if (!activeSubtitle) {
|
|
1461
|
+
setCurrentSubtitle("");
|
|
1462
|
+
setIsVisible(false);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const currentTime = videoRef.currentTime;
|
|
1466
|
+
const textTracks = Array.from(videoRef.textTracks);
|
|
1467
|
+
const activeTrack = textTracks.find((track) => track.mode === "showing" && track.label === activeSubtitle.label);
|
|
1468
|
+
if (activeTrack?.cues) {
|
|
1469
|
+
const activeCues = Array.from(activeTrack.cues).filter((cue) => currentTime >= cue.startTime && currentTime <= cue.endTime);
|
|
1470
|
+
if (activeCues.length > 0) {
|
|
1471
|
+
const cue = activeCues[0];
|
|
1472
|
+
let cueText = "";
|
|
1473
|
+
try {
|
|
1474
|
+
if ("text" in cue) {
|
|
1475
|
+
cueText = cue.text;
|
|
1476
|
+
}
|
|
1477
|
+
else if (typeof cue.getCueAsHTML === "function") {
|
|
1478
|
+
const fragment = cue.getCueAsHTML();
|
|
1479
|
+
cueText = fragment?.textContent ?? "";
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
cueText = cue.toString() ?? "";
|
|
1483
|
+
}
|
|
1293
1484
|
}
|
|
1294
|
-
|
|
1295
|
-
cueText =
|
|
1485
|
+
catch {
|
|
1486
|
+
cueText = "";
|
|
1296
1487
|
}
|
|
1488
|
+
setCurrentSubtitle(cueText);
|
|
1489
|
+
setIsVisible(!!cueText);
|
|
1297
1490
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1491
|
+
else {
|
|
1492
|
+
setCurrentSubtitle("");
|
|
1493
|
+
setIsVisible(false);
|
|
1300
1494
|
}
|
|
1301
|
-
setCurrentSubtitle(cueText);
|
|
1302
|
-
setIsVisible(!!cueText);
|
|
1303
1495
|
}
|
|
1304
1496
|
else {
|
|
1305
1497
|
setCurrentSubtitle("");
|
|
1306
1498
|
setIsVisible(false);
|
|
1307
1499
|
}
|
|
1308
|
-
}
|
|
1500
|
+
});
|
|
1309
1501
|
};
|
|
1310
1502
|
videoRef.addEventListener("timeupdate", handleTimeUpdate);
|
|
1311
1503
|
return () => {
|
|
1312
1504
|
videoRef.removeEventListener("timeupdate", handleTimeUpdate);
|
|
1505
|
+
if (rafRef.current !== null) {
|
|
1506
|
+
cancelAnimationFrame(rafRef.current);
|
|
1507
|
+
rafRef.current = null;
|
|
1508
|
+
}
|
|
1313
1509
|
};
|
|
1314
1510
|
}, [videoRef, activeSubtitle]);
|
|
1315
1511
|
useEffect(() => {
|
|
@@ -1318,55 +1514,43 @@ const SubtitleOverlay = ({ styleConfig }) => {
|
|
|
1318
1514
|
setIsVisible(false);
|
|
1319
1515
|
}
|
|
1320
1516
|
}, [activeSubtitle]);
|
|
1517
|
+
const bgValue = styleConfig?.backgroundColor ?? "rgba(0, 0, 0, 0.4)";
|
|
1518
|
+
const isTailwindBg = isTailwindBackground(bgValue);
|
|
1519
|
+
const bgClassName = isTailwindBg
|
|
1520
|
+
? (bgValue.trim().toLowerCase() === "transparent" ? "bg-transparent" : bgValue.trim())
|
|
1521
|
+
: undefined;
|
|
1522
|
+
const isTransparentBg = bgValue.trim().toLowerCase() === "transparent" ||
|
|
1523
|
+
bgClassName === "bg-transparent";
|
|
1524
|
+
const subtitleStyle = useMemo(() => {
|
|
1525
|
+
const base = {
|
|
1526
|
+
position: "absolute",
|
|
1527
|
+
...getPositionStyles(styleConfig?.position),
|
|
1528
|
+
fontSize: styleConfig?.fontSize ?? "1.2rem",
|
|
1529
|
+
fontWeight: "500",
|
|
1530
|
+
lineHeight: "1.2",
|
|
1531
|
+
textAlign: "center",
|
|
1532
|
+
color: styleConfig?.textColor ?? "#fff",
|
|
1533
|
+
padding: styleConfig?.padding ?? "8px 16px",
|
|
1534
|
+
borderRadius: styleConfig?.borderRadius ?? "5px",
|
|
1535
|
+
maxWidth: styleConfig?.maxWidth ?? "60%",
|
|
1536
|
+
minWidth: "fit-content",
|
|
1537
|
+
boxShadow: isTransparentBg ? "none" : "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
1538
|
+
backdropFilter: isTransparentBg ? "none" : "blur(6px)",
|
|
1539
|
+
border: isTransparentBg ? "none" : "1px solid rgba(255, 255, 255, 0.2)",
|
|
1540
|
+
transition: "all 0.2s ease-in-out",
|
|
1541
|
+
opacity: isVisible ? 1 : 0,
|
|
1542
|
+
zIndex: 10,
|
|
1543
|
+
pointerEvents: "none",
|
|
1544
|
+
};
|
|
1545
|
+
if (!isTailwindBg)
|
|
1546
|
+
base.background = bgValue;
|
|
1547
|
+
return base;
|
|
1548
|
+
}, [styleConfig, isVisible, bgValue, isTailwindBg, isTransparentBg]);
|
|
1321
1549
|
if (!isVisible || !currentSubtitle)
|
|
1322
1550
|
return null;
|
|
1323
|
-
|
|
1324
|
-
const position = styleConfig?.position || "bottom";
|
|
1325
|
-
switch (position) {
|
|
1326
|
-
case "top":
|
|
1327
|
-
return {
|
|
1328
|
-
top: "10%",
|
|
1329
|
-
left: "50%",
|
|
1330
|
-
transform: "translateX(-50%)",
|
|
1331
|
-
};
|
|
1332
|
-
case "center":
|
|
1333
|
-
return {
|
|
1334
|
-
top: "50%",
|
|
1335
|
-
left: "50%",
|
|
1336
|
-
transform: "translate(-50%, -50%)",
|
|
1337
|
-
};
|
|
1338
|
-
case "bottom":
|
|
1339
|
-
default:
|
|
1340
|
-
return {
|
|
1341
|
-
bottom: "15%",
|
|
1342
|
-
left: "50%",
|
|
1343
|
-
transform: "translateX(-50%)",
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
const subtitleStyle = {
|
|
1348
|
-
position: "absolute",
|
|
1349
|
-
...getPositionStyles(),
|
|
1350
|
-
fontSize: styleConfig?.fontSize || "1.2rem",
|
|
1351
|
-
fontWeight: "500",
|
|
1352
|
-
lineHeight: "1.2",
|
|
1353
|
-
textAlign: "center",
|
|
1354
|
-
background: styleConfig?.backgroundColor || "rgba(0, 0, 0, 0.4)",
|
|
1355
|
-
color: styleConfig?.textColor || "#fff",
|
|
1356
|
-
padding: styleConfig?.padding || "8px 16px",
|
|
1357
|
-
borderRadius: styleConfig?.borderRadius || "5px",
|
|
1358
|
-
maxWidth: styleConfig?.maxWidth || "60%",
|
|
1359
|
-
minWidth: "fit-content",
|
|
1360
|
-
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
1361
|
-
backdropFilter: "blur(6px)",
|
|
1362
|
-
border: "1px solid rgba(255, 255, 255, 0.2)",
|
|
1363
|
-
transition: "all 0.2s ease-in-out",
|
|
1364
|
-
opacity: isVisible ? 1 : 0,
|
|
1365
|
-
zIndex: 10,
|
|
1366
|
-
pointerEvents: "none",
|
|
1367
|
-
};
|
|
1368
|
-
return React__default.createElement("div", { style: subtitleStyle }, currentSubtitle);
|
|
1551
|
+
return (React__default.createElement("div", { className: bgClassName, style: subtitleStyle }, currentSubtitle));
|
|
1369
1552
|
};
|
|
1553
|
+
SubtitleOverlay.displayName = "SubtitleOverlay";
|
|
1370
1554
|
|
|
1371
1555
|
const HLS_CONFIG = {
|
|
1372
1556
|
enableWorker: true,
|
|
@@ -1821,50 +2005,62 @@ const useVideoSource = (trackSrc, type) => {
|
|
|
1821
2005
|
|
|
1822
2006
|
const useSubtitles = (subtitles) => {
|
|
1823
2007
|
const { videoRef, activeSubtitle, setSubtitles } = useVideoStore();
|
|
2008
|
+
const timeoutIdsRef = useRef([]);
|
|
1824
2009
|
useEffect(() => {
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2010
|
+
// Clear any pending timeouts from previous effect runs
|
|
2011
|
+
timeoutIdsRef.current.forEach((id) => clearTimeout(id));
|
|
2012
|
+
timeoutIdsRef.current = [];
|
|
2013
|
+
if (!videoRef)
|
|
2014
|
+
return;
|
|
2015
|
+
const tracks = videoRef.getElementsByTagName("track");
|
|
2016
|
+
while (tracks.length > 0) {
|
|
2017
|
+
videoRef.removeChild(tracks[0]);
|
|
2018
|
+
}
|
|
2019
|
+
Array.from(videoRef.textTracks).forEach((track) => {
|
|
2020
|
+
track.mode = "disabled";
|
|
2021
|
+
});
|
|
2022
|
+
let trackElement = null;
|
|
2023
|
+
let handleTrackLoad = null;
|
|
2024
|
+
if (activeSubtitle && subtitles) {
|
|
2025
|
+
const index = subtitles.findIndex((s) => s.label === activeSubtitle.label);
|
|
2026
|
+
if (index !== -1) {
|
|
2027
|
+
trackElement = document.createElement("track");
|
|
2028
|
+
trackElement.kind = "subtitles";
|
|
2029
|
+
trackElement.label = activeSubtitle.label;
|
|
2030
|
+
trackElement.srclang = activeSubtitle.lang;
|
|
2031
|
+
trackElement.src = activeSubtitle.url;
|
|
2032
|
+
trackElement.default = false;
|
|
2033
|
+
videoRef.appendChild(trackElement);
|
|
2034
|
+
handleTrackLoad = () => {
|
|
2035
|
+
const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
|
|
2036
|
+
if (textTrack) {
|
|
2037
|
+
textTrack.mode = "showing";
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
trackElement.addEventListener("load", handleTrackLoad);
|
|
2041
|
+
// Fallback attempts with proper cleanup tracking
|
|
2042
|
+
const attempts = [100, 500, 1000];
|
|
2043
|
+
attempts.forEach((delay) => {
|
|
2044
|
+
const timeoutId = setTimeout(() => {
|
|
1845
2045
|
const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
|
|
1846
|
-
if (textTrack) {
|
|
2046
|
+
if (textTrack && textTrack.mode !== "showing") {
|
|
1847
2047
|
textTrack.mode = "showing";
|
|
1848
2048
|
}
|
|
1849
|
-
};
|
|
1850
|
-
|
|
1851
|
-
const attempts = [100, 500, 1000];
|
|
1852
|
-
attempts.forEach((delay) => {
|
|
1853
|
-
setTimeout(() => {
|
|
1854
|
-
const textTrack = Array.from(videoRef.textTracks).find((track) => track.label === activeSubtitle.label);
|
|
1855
|
-
if (textTrack && textTrack.mode !== "showing") {
|
|
1856
|
-
textTrack.mode = "showing";
|
|
1857
|
-
}
|
|
1858
|
-
}, delay);
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
else {
|
|
1863
|
-
Array.from(videoRef.textTracks).forEach((track) => {
|
|
1864
|
-
track.mode = "disabled";
|
|
2049
|
+
}, delay);
|
|
2050
|
+
timeoutIdsRef.current.push(timeoutId);
|
|
1865
2051
|
});
|
|
1866
2052
|
}
|
|
1867
2053
|
}
|
|
2054
|
+
// Cleanup function
|
|
2055
|
+
return () => {
|
|
2056
|
+
// Clear all pending timeouts
|
|
2057
|
+
timeoutIdsRef.current.forEach((id) => clearTimeout(id));
|
|
2058
|
+
timeoutIdsRef.current = [];
|
|
2059
|
+
// Remove event listener if it was added
|
|
2060
|
+
if (trackElement && handleTrackLoad) {
|
|
2061
|
+
trackElement.removeEventListener("load", handleTrackLoad);
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
1868
2064
|
}, [videoRef, activeSubtitle, subtitles]);
|
|
1869
2065
|
useEffect(() => {
|
|
1870
2066
|
if (subtitles) {
|
|
@@ -1873,75 +2069,32 @@ const useSubtitles = (subtitles) => {
|
|
|
1873
2069
|
}, [subtitles, setSubtitles]);
|
|
1874
2070
|
};
|
|
1875
2071
|
|
|
1876
|
-
const
|
|
1877
|
-
const { videoRef } = useVideoStore();
|
|
2072
|
+
const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
|
|
2073
|
+
const { videoRef, setShowCountdown } = useVideoStore();
|
|
2074
|
+
const isViewCounted = useRef(false);
|
|
2075
|
+
const lastVideoSrcRef = useRef(null);
|
|
2076
|
+
// Reset view count when video source changes
|
|
1878
2077
|
useEffect(() => {
|
|
1879
2078
|
if (!videoRef)
|
|
1880
2079
|
return;
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
existingStyle.remove();
|
|
1887
|
-
}
|
|
1888
|
-
const styles = `
|
|
1889
|
-
.video-player video::cue {
|
|
1890
|
-
font-size: ${config?.fontSize || "1.75rem"} !important;
|
|
1891
|
-
background: ${config?.backgroundColor ||
|
|
1892
|
-
"linear-gradient(135deg, #fbbf24, #f59e0b)"} !important;
|
|
1893
|
-
color: ${config?.textColor || "#000000"} !important;
|
|
1894
|
-
border-radius: ${config?.borderRadius || "12px"} !important;
|
|
1895
|
-
padding: ${config?.padding || "12px 20px"} !important;
|
|
1896
|
-
max-width: ${config?.maxWidth || "80%"} !important;
|
|
1897
|
-
${config?.position === "top"
|
|
1898
|
-
? "top: 10% !important; bottom: auto !important;"
|
|
1899
|
-
: ""}
|
|
1900
|
-
${config?.position === "center"
|
|
1901
|
-
? "top: 50% !important; bottom: auto !important; transform: translateX(-50%) translateY(-50%) !important;"
|
|
1902
|
-
: ""}
|
|
1903
|
-
${config?.position === "bottom" || !config?.position
|
|
1904
|
-
? "bottom: 15% !important; top: auto !important;"
|
|
1905
|
-
: ""}
|
|
2080
|
+
const currentSrc = videoRef.src || videoRef.currentSrc;
|
|
2081
|
+
// If video source changed, reset the view count
|
|
2082
|
+
if (lastVideoSrcRef.current !== currentSrc) {
|
|
2083
|
+
isViewCounted.current = false;
|
|
2084
|
+
lastVideoSrcRef.current = currentSrc;
|
|
1906
2085
|
}
|
|
1907
|
-
|
|
1908
|
-
style.textContent = styles;
|
|
1909
|
-
document.head.appendChild(style);
|
|
1910
|
-
};
|
|
1911
|
-
applySubtitleStyles();
|
|
1912
|
-
return () => {
|
|
1913
|
-
const existingStyle = document.getElementById("custom-subtitle-styles");
|
|
1914
|
-
if (existingStyle) {
|
|
1915
|
-
existingStyle.remove();
|
|
1916
|
-
}
|
|
1917
|
-
};
|
|
1918
|
-
}, [videoRef, config]);
|
|
1919
|
-
};
|
|
1920
|
-
|
|
1921
|
-
const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) => {
|
|
1922
|
-
const { videoRef, setIsPlaying, setShowCountdown } = useVideoStore();
|
|
1923
|
-
const startTime = useRef(null);
|
|
1924
|
-
const isViewCounted = useRef(false);
|
|
2086
|
+
}, [videoRef?.src, videoRef?.currentSrc, videoRef]);
|
|
1925
2087
|
useEffect(() => {
|
|
1926
2088
|
if (!videoRef)
|
|
1927
2089
|
return;
|
|
2090
|
+
// Only handle view tracking on play - setIsPlaying is handled by useVideoEvents
|
|
1928
2091
|
const onPlay = () => {
|
|
1929
2092
|
if (!isViewCounted.current) {
|
|
1930
2093
|
isViewCounted.current = true;
|
|
1931
2094
|
tracking?.onViewed?.();
|
|
1932
2095
|
}
|
|
1933
|
-
startTime.current = Date.now();
|
|
1934
|
-
setIsPlaying(true);
|
|
1935
|
-
};
|
|
1936
|
-
const onPause = () => {
|
|
1937
|
-
if (startTime.current) {
|
|
1938
|
-
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
1939
|
-
const getCurrentTime = localStorage.getItem("current_time");
|
|
1940
|
-
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
1941
|
-
startTime.current = null;
|
|
1942
|
-
}
|
|
1943
|
-
setIsPlaying(false);
|
|
1944
2096
|
};
|
|
2097
|
+
// Handle episode end logic - playback state is handled by useVideoEvents
|
|
1945
2098
|
const onEnded = () => {
|
|
1946
2099
|
if (episodeList &&
|
|
1947
2100
|
episodeList.length > 0 &&
|
|
@@ -1959,11 +2112,9 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
|
|
|
1959
2112
|
}
|
|
1960
2113
|
};
|
|
1961
2114
|
videoRef.addEventListener("play", onPlay);
|
|
1962
|
-
videoRef.addEventListener("pause", onPause);
|
|
1963
2115
|
videoRef.addEventListener("ended", onEnded);
|
|
1964
2116
|
return () => {
|
|
1965
2117
|
videoRef.removeEventListener("play", onPlay);
|
|
1966
|
-
videoRef.removeEventListener("pause", onPause);
|
|
1967
2118
|
videoRef.removeEventListener("ended", onEnded);
|
|
1968
2119
|
};
|
|
1969
2120
|
}, [
|
|
@@ -1972,29 +2123,8 @@ const useVideoTracking = (tracking, episodeList, currentEpisodeIndex, onClose) =
|
|
|
1972
2123
|
currentEpisodeIndex,
|
|
1973
2124
|
onClose,
|
|
1974
2125
|
tracking,
|
|
1975
|
-
setIsPlaying,
|
|
1976
2126
|
setShowCountdown,
|
|
1977
2127
|
]);
|
|
1978
|
-
useEffect(() => {
|
|
1979
|
-
const handleUnload = () => {
|
|
1980
|
-
if (startTime.current) {
|
|
1981
|
-
const elapsedTime = (Date.now() - startTime.current) / 1000;
|
|
1982
|
-
const getCurrentTime = localStorage.getItem("current_time");
|
|
1983
|
-
localStorage.setItem("current_time", (Number(getCurrentTime || 0) + elapsedTime).toString());
|
|
1984
|
-
}
|
|
1985
|
-
const totalTimeWatched = Number(localStorage.getItem("current_time") || 0);
|
|
1986
|
-
if (totalTimeWatched >= 30) {
|
|
1987
|
-
tracking?.onWatchTimeUpdated?.({
|
|
1988
|
-
watchTime: totalTimeWatched,
|
|
1989
|
-
});
|
|
1990
|
-
}
|
|
1991
|
-
localStorage.setItem("current_time", "0");
|
|
1992
|
-
};
|
|
1993
|
-
window.addEventListener("unload", handleUnload);
|
|
1994
|
-
return () => {
|
|
1995
|
-
window.removeEventListener("unload", handleUnload);
|
|
1996
|
-
};
|
|
1997
|
-
}, [tracking]);
|
|
1998
2128
|
};
|
|
1999
2129
|
|
|
2000
2130
|
const useIntroSkip = (intro) => {
|
|
@@ -2159,7 +2289,6 @@ const useVideoEvents = () => {
|
|
|
2159
2289
|
const onLoadedMetadata = (e) => {
|
|
2160
2290
|
const duration = e?.currentTarget?.duration;
|
|
2161
2291
|
if (typeof duration === "number" && !Number.isNaN(duration)) {
|
|
2162
|
-
localStorage.setItem("current_time", "0");
|
|
2163
2292
|
setDuration(duration);
|
|
2164
2293
|
}
|
|
2165
2294
|
};
|
|
@@ -2267,8 +2396,13 @@ const useAdManager = (adConfig) => {
|
|
|
2267
2396
|
})));
|
|
2268
2397
|
const preRollPlayedRef = useRef(false);
|
|
2269
2398
|
const postRollPlayedRef = useRef(false);
|
|
2270
|
-
const midRollCheckIntervalRef = useRef(null);
|
|
2271
2399
|
const resumeAfterAdRef = useRef(false);
|
|
2400
|
+
// Track maximum time reached to prevent ad replay when seeking backward
|
|
2401
|
+
const maxTimeReachedRef = useRef(0);
|
|
2402
|
+
// Throttle ad checking to prevent performance issues
|
|
2403
|
+
const adCheckThrottleRef = useRef(null);
|
|
2404
|
+
// Track if we're currently processing an ad to prevent race conditions
|
|
2405
|
+
const isProcessingAdRef = useRef(false);
|
|
2272
2406
|
const stopMediaElement = useCallback((media) => {
|
|
2273
2407
|
if (!media)
|
|
2274
2408
|
return;
|
|
@@ -2288,36 +2422,29 @@ const useAdManager = (adConfig) => {
|
|
|
2288
2422
|
setMidRollQueue([]);
|
|
2289
2423
|
return;
|
|
2290
2424
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
let lastAdTime = 0;
|
|
2306
|
-
for (const ad of ads) {
|
|
2307
|
-
if (ad.time < minGap)
|
|
2308
|
-
continue;
|
|
2309
|
-
if (ad.time > videoDuration - avoidNearEnd)
|
|
2310
|
-
continue;
|
|
2311
|
-
if (ad.time - lastAdTime < minGap)
|
|
2312
|
-
continue;
|
|
2313
|
-
validAds.push(ad);
|
|
2314
|
-
const adLength = Number.isFinite(ad.duration)
|
|
2315
|
-
? ad.duration
|
|
2316
|
-
: 0;
|
|
2317
|
-
lastAdTime = ad.time + adLength;
|
|
2425
|
+
// Filter out invalid ads and ensure all required fields are present
|
|
2426
|
+
const validAds = adConfig.midRoll.filter((ad) => ad &&
|
|
2427
|
+
typeof ad.time === "number" &&
|
|
2428
|
+
ad.time >= 0 &&
|
|
2429
|
+
ad.time < Number.MAX_SAFE_INTEGER &&
|
|
2430
|
+
typeof ad.id === "string" &&
|
|
2431
|
+
ad.id.trim() !== "" &&
|
|
2432
|
+
typeof ad.adUrl === "string" &&
|
|
2433
|
+
ad.adUrl.trim() !== "" &&
|
|
2434
|
+
typeof ad.type === "string" &&
|
|
2435
|
+
ad.type === "mid-roll");
|
|
2436
|
+
if (validAds.length === 0) {
|
|
2437
|
+
setMidRollQueue([]);
|
|
2438
|
+
return;
|
|
2318
2439
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2440
|
+
// Sort ads by time to ensure they play in order
|
|
2441
|
+
const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
|
|
2442
|
+
// Remove duplicate IDs (keep first occurrence)
|
|
2443
|
+
const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
|
|
2444
|
+
setMidRollQueue(uniqueAds);
|
|
2445
|
+
}, [adConfig?.midRoll, setMidRollQueue]);
|
|
2446
|
+
// Removed smartPlacement - users should configure exact ad times
|
|
2447
|
+
// This ensures ads appear exactly when specified
|
|
2321
2448
|
const playPreRollAd = async () => {
|
|
2322
2449
|
if (!adConfig?.preRoll || preRollPlayedRef.current || !videoRef)
|
|
2323
2450
|
return;
|
|
@@ -2333,6 +2460,46 @@ const useAdManager = (adConfig) => {
|
|
|
2333
2460
|
setAdType("pre-roll");
|
|
2334
2461
|
adConfig.onAdStart?.(adBreak);
|
|
2335
2462
|
};
|
|
2463
|
+
const playMidRollAd = useCallback(async (adBreak) => {
|
|
2464
|
+
if (!videoRef || isAdPlaying || isProcessingAdRef.current)
|
|
2465
|
+
return;
|
|
2466
|
+
// Prevent duplicate ad playback
|
|
2467
|
+
if (playedAdBreaks.includes(adBreak.id))
|
|
2468
|
+
return;
|
|
2469
|
+
// Mark as processing to prevent race conditions
|
|
2470
|
+
isProcessingAdRef.current = true;
|
|
2471
|
+
const updatedQueue = midRollQueue.filter((ad) => ad.id !== adBreak.id);
|
|
2472
|
+
setMidRollQueue(updatedQueue);
|
|
2473
|
+
addPlayedAdBreak(adBreak.id);
|
|
2474
|
+
const wasPlaying = !videoRef.paused;
|
|
2475
|
+
videoRef.pause();
|
|
2476
|
+
setPlaying(false);
|
|
2477
|
+
setIsPlaying(false);
|
|
2478
|
+
resumeAfterAdRef.current = wasPlaying;
|
|
2479
|
+
stopMediaElement(useVideoStore.getState().adVideoRef);
|
|
2480
|
+
setIsAdPlaying(true);
|
|
2481
|
+
setCurrentAd(adBreak);
|
|
2482
|
+
setAdType("mid-roll");
|
|
2483
|
+
adConfig?.onAdStart?.(adBreak);
|
|
2484
|
+
// Reset processing flag after a short delay
|
|
2485
|
+
setTimeout(() => {
|
|
2486
|
+
isProcessingAdRef.current = false;
|
|
2487
|
+
}, 100);
|
|
2488
|
+
}, [
|
|
2489
|
+
videoRef,
|
|
2490
|
+
isAdPlaying,
|
|
2491
|
+
playedAdBreaks,
|
|
2492
|
+
midRollQueue,
|
|
2493
|
+
setMidRollQueue,
|
|
2494
|
+
addPlayedAdBreak,
|
|
2495
|
+
setPlaying,
|
|
2496
|
+
setIsPlaying,
|
|
2497
|
+
setCurrentAd,
|
|
2498
|
+
setAdType,
|
|
2499
|
+
setIsAdPlaying,
|
|
2500
|
+
adConfig,
|
|
2501
|
+
stopMediaElement,
|
|
2502
|
+
]);
|
|
2336
2503
|
const playPostRollAd = async () => {
|
|
2337
2504
|
if (!adConfig?.postRoll || postRollPlayedRef.current || !videoRef)
|
|
2338
2505
|
return;
|
|
@@ -2350,23 +2517,40 @@ const useAdManager = (adConfig) => {
|
|
|
2350
2517
|
const adTypeState = useVideoStore.getState().adType;
|
|
2351
2518
|
const videoRefState = useVideoStore.getState().videoRef;
|
|
2352
2519
|
const adVideoRefState = useVideoStore.getState().adVideoRef;
|
|
2353
|
-
if (!
|
|
2520
|
+
if (!currentAdState)
|
|
2354
2521
|
return;
|
|
2522
|
+
// Reset processing flag
|
|
2523
|
+
isProcessingAdRef.current = false;
|
|
2524
|
+
// Clean up ad video element
|
|
2355
2525
|
if (adVideoRefState) {
|
|
2356
2526
|
stopMediaElement(adVideoRefState);
|
|
2357
2527
|
setAdVideoRef(null);
|
|
2358
2528
|
}
|
|
2529
|
+
// Reset ad state
|
|
2359
2530
|
setIsAdPlaying(false);
|
|
2360
2531
|
setCurrentAd(null);
|
|
2361
2532
|
setAdType(null);
|
|
2362
2533
|
setAdCurrentTime(0);
|
|
2363
2534
|
setCanSkipAd(false);
|
|
2364
2535
|
setSkipCountdown(0);
|
|
2536
|
+
// Call end callback
|
|
2365
2537
|
adConfig?.onAdEnd?.(currentAdState);
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2538
|
+
// Resume main video if needed
|
|
2539
|
+
if (resumeAfterAdRef.current &&
|
|
2540
|
+
adTypeState !== "post-roll" &&
|
|
2541
|
+
videoRefState) {
|
|
2542
|
+
// Small delay to ensure ad cleanup is complete
|
|
2543
|
+
setTimeout(() => {
|
|
2544
|
+
if (videoRefState && !videoRefState.paused)
|
|
2545
|
+
return;
|
|
2546
|
+
videoRefState.play().catch(() => {
|
|
2547
|
+
// If autoplay fails, user will need to click play
|
|
2548
|
+
setPlaying(false);
|
|
2549
|
+
setIsPlaying(false);
|
|
2550
|
+
});
|
|
2551
|
+
setPlaying(true);
|
|
2552
|
+
setIsPlaying(true);
|
|
2553
|
+
}, 100);
|
|
2370
2554
|
}
|
|
2371
2555
|
resumeAfterAdRef.current = false;
|
|
2372
2556
|
}, [
|
|
@@ -2443,46 +2627,87 @@ const useAdManager = (adConfig) => {
|
|
|
2443
2627
|
videoRef.removeEventListener("canplay", handleCanPlay);
|
|
2444
2628
|
};
|
|
2445
2629
|
}, [videoRef, adConfig?.preRoll]);
|
|
2630
|
+
// Precise mid-roll ad checking with accurate timing
|
|
2446
2631
|
useEffect(() => {
|
|
2447
2632
|
if (!videoRef || !adConfig?.midRoll || isAdPlaying) {
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2633
|
+
// Clear any pending throttle
|
|
2634
|
+
if (adCheckThrottleRef.current !== null) {
|
|
2635
|
+
cancelAnimationFrame(adCheckThrottleRef.current);
|
|
2636
|
+
adCheckThrottleRef.current = null;
|
|
2451
2637
|
}
|
|
2452
2638
|
return;
|
|
2453
2639
|
}
|
|
2454
|
-
|
|
2640
|
+
// Precise ad check function
|
|
2641
|
+
const checkMidRollAds = () => {
|
|
2642
|
+
// Clear throttle ref
|
|
2643
|
+
adCheckThrottleRef.current = null;
|
|
2455
2644
|
const state = useVideoStore.getState();
|
|
2456
|
-
if
|
|
2457
|
-
|
|
2458
|
-
|
|
2645
|
+
// Skip if ad is already playing or being processed
|
|
2646
|
+
if (state.isAdPlaying || isProcessingAdRef.current) {
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
// Check if we have mid-roll ads in queue
|
|
2650
|
+
if (!state.midRollQueue || state.midRollQueue.length === 0) {
|
|
2459
2651
|
return;
|
|
2460
2652
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
state.
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2653
|
+
// Get current time directly from video element for maximum accuracy
|
|
2654
|
+
const currentVideoTime = videoRef.currentTime || 0;
|
|
2655
|
+
// Update max time reached (only forward, not backward)
|
|
2656
|
+
if (currentVideoTime > maxTimeReachedRef.current) {
|
|
2657
|
+
maxTimeReachedRef.current = currentVideoTime;
|
|
2658
|
+
}
|
|
2659
|
+
// Find the next ad in queue that should play
|
|
2660
|
+
// Check ads in order, but skip already-played ones
|
|
2661
|
+
for (const ad of state.midRollQueue) {
|
|
2662
|
+
// Skip if already played
|
|
2663
|
+
if (state.playedAdBreaks.includes(ad.id)) {
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
// Precise timing check: ad should trigger when we reach or pass its time
|
|
2667
|
+
// Use 1 second tolerance to catch ads even if timeupdate fires slightly late
|
|
2668
|
+
const timeDifference = currentVideoTime - ad.time;
|
|
2669
|
+
const shouldTrigger = timeDifference >= 0 && timeDifference <= 1.0;
|
|
2670
|
+
// Also check if we've reached the max time (prevents replay on backward seek)
|
|
2671
|
+
// This ensures ads only play if we've actually watched past them
|
|
2672
|
+
const hasReachedMaxTime = maxTimeReachedRef.current >= ad.time;
|
|
2673
|
+
if (shouldTrigger && hasReachedMaxTime) {
|
|
2674
|
+
// Play the ad and break (only one ad at a time)
|
|
2675
|
+
playMidRollAd(ad);
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
// Throttle function using requestAnimationFrame for smooth performance
|
|
2681
|
+
const throttledCheck = () => {
|
|
2682
|
+
if (adCheckThrottleRef.current === null) {
|
|
2683
|
+
adCheckThrottleRef.current = requestAnimationFrame(checkMidRollAds);
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
// Listen to timeupdate event (throttled for performance)
|
|
2687
|
+
videoRef.addEventListener("timeupdate", throttledCheck);
|
|
2688
|
+
// Also check immediately on seek to catch rapid seeks past ad times
|
|
2689
|
+
const handleSeeking = () => {
|
|
2690
|
+
// Force immediate check when seeking
|
|
2691
|
+
if (adCheckThrottleRef.current !== null) {
|
|
2692
|
+
cancelAnimationFrame(adCheckThrottleRef.current);
|
|
2693
|
+
adCheckThrottleRef.current = null;
|
|
2694
|
+
}
|
|
2695
|
+
// Check immediately
|
|
2696
|
+
checkMidRollAds();
|
|
2697
|
+
};
|
|
2698
|
+
videoRef.addEventListener("seeking", handleSeeking);
|
|
2699
|
+
videoRef.addEventListener("seeked", handleSeeking);
|
|
2479
2700
|
return () => {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2701
|
+
videoRef.removeEventListener("timeupdate", throttledCheck);
|
|
2702
|
+
videoRef.removeEventListener("seeking", handleSeeking);
|
|
2703
|
+
videoRef.removeEventListener("seeked", handleSeeking);
|
|
2704
|
+
// Clear any pending animation frame
|
|
2705
|
+
if (adCheckThrottleRef.current !== null) {
|
|
2706
|
+
cancelAnimationFrame(adCheckThrottleRef.current);
|
|
2707
|
+
adCheckThrottleRef.current = null;
|
|
2483
2708
|
}
|
|
2484
2709
|
};
|
|
2485
|
-
}, [videoRef, isAdPlaying, adConfig]);
|
|
2710
|
+
}, [videoRef, isAdPlaying, adConfig, playMidRollAd]);
|
|
2486
2711
|
useEffect(() => {
|
|
2487
2712
|
if (!videoRef || !adConfig?.postRoll || postRollPlayedRef.current)
|
|
2488
2713
|
return;
|
|
@@ -2499,19 +2724,46 @@ const useAdManager = (adConfig) => {
|
|
|
2499
2724
|
useEffect(() => {
|
|
2500
2725
|
if (!videoRef?.src)
|
|
2501
2726
|
return;
|
|
2727
|
+
// Reset ad state when video source changes
|
|
2502
2728
|
preRollPlayedRef.current = false;
|
|
2503
2729
|
postRollPlayedRef.current = false;
|
|
2504
2730
|
resumeAfterAdRef.current = false;
|
|
2731
|
+
maxTimeReachedRef.current = 0;
|
|
2732
|
+
isProcessingAdRef.current = false;
|
|
2733
|
+
// Clear any pending throttle
|
|
2734
|
+
if (adCheckThrottleRef.current !== null) {
|
|
2735
|
+
cancelAnimationFrame(adCheckThrottleRef.current);
|
|
2736
|
+
adCheckThrottleRef.current = null;
|
|
2737
|
+
}
|
|
2505
2738
|
setIsAdPlaying(false);
|
|
2506
2739
|
setCurrentAd(null);
|
|
2507
2740
|
setAdType(null);
|
|
2741
|
+
// Re-initialize mid-roll queue with strict validation
|
|
2508
2742
|
if (adConfig?.midRoll && adConfig.midRoll.length > 0) {
|
|
2509
|
-
|
|
2510
|
-
|
|
2743
|
+
// Filter and validate ads
|
|
2744
|
+
const validAds = adConfig.midRoll.filter((ad) => ad &&
|
|
2745
|
+
typeof ad.time === "number" &&
|
|
2746
|
+
ad.time >= 0 &&
|
|
2747
|
+
typeof ad.id === "string" &&
|
|
2748
|
+
ad.id.trim() !== "" &&
|
|
2749
|
+
typeof ad.adUrl === "string" &&
|
|
2750
|
+
ad.adUrl.trim() !== "" &&
|
|
2751
|
+
typeof ad.type === "string" &&
|
|
2752
|
+
ad.type === "mid-roll");
|
|
2753
|
+
if (validAds.length > 0) {
|
|
2754
|
+
// Sort by time and remove duplicates
|
|
2755
|
+
const sortedMidRolls = [...validAds].sort((a, b) => a.time - b.time);
|
|
2756
|
+
const uniqueAds = sortedMidRolls.filter((ad, index, self) => index === self.findIndex((a) => a.id === ad.id));
|
|
2757
|
+
setMidRollQueue(uniqueAds);
|
|
2758
|
+
}
|
|
2759
|
+
else {
|
|
2760
|
+
setMidRollQueue([]);
|
|
2761
|
+
}
|
|
2511
2762
|
}
|
|
2512
2763
|
else {
|
|
2513
2764
|
setMidRollQueue([]);
|
|
2514
2765
|
}
|
|
2766
|
+
// Clean up any lingering ad video
|
|
2515
2767
|
const lingeringAdRef = useVideoStore.getState().adVideoRef;
|
|
2516
2768
|
if (lingeringAdRef) {
|
|
2517
2769
|
stopMediaElement(lingeringAdRef);
|
|
@@ -2721,13 +2973,93 @@ const usePrimaryVideoLifecycle = ({ hasPreRoll, trackSrc, }) => {
|
|
|
2721
2973
|
};
|
|
2722
2974
|
};
|
|
2723
2975
|
|
|
2724
|
-
const
|
|
2725
|
-
|
|
2976
|
+
const getErrorType = (code) => {
|
|
2977
|
+
// MediaError codes: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
|
|
2978
|
+
switch (code) {
|
|
2979
|
+
case 1: // MEDIA_ERR_ABORTED
|
|
2980
|
+
return "unknown";
|
|
2981
|
+
case 2: // MEDIA_ERR_NETWORK
|
|
2982
|
+
return "network";
|
|
2983
|
+
case 3: // MEDIA_ERR_DECODE
|
|
2984
|
+
return "decode";
|
|
2985
|
+
case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
|
|
2986
|
+
return "src";
|
|
2987
|
+
default:
|
|
2988
|
+
return "unknown";
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
const getErrorMessage = (code) => {
|
|
2992
|
+
switch (code) {
|
|
2993
|
+
case 1:
|
|
2994
|
+
return "Video playback was aborted.";
|
|
2995
|
+
case 2:
|
|
2996
|
+
return "A network error occurred while loading the video.";
|
|
2997
|
+
case 3:
|
|
2998
|
+
return "An error occurred while decoding the video.";
|
|
2999
|
+
case 4:
|
|
3000
|
+
return "The video format is not supported.";
|
|
3001
|
+
default:
|
|
3002
|
+
return "An unknown error occurred.";
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
const useVideoError = () => {
|
|
3006
|
+
const { setError, clearError, error } = useVideoStore();
|
|
3007
|
+
const handleVideoError = useCallback((e) => {
|
|
3008
|
+
const video = e.currentTarget;
|
|
3009
|
+
const mediaError = video.error;
|
|
3010
|
+
if (mediaError) {
|
|
3011
|
+
const errorData = {
|
|
3012
|
+
code: mediaError.code,
|
|
3013
|
+
message: mediaError.message || getErrorMessage(mediaError.code),
|
|
3014
|
+
type: getErrorType(mediaError.code),
|
|
3015
|
+
};
|
|
3016
|
+
setError(errorData);
|
|
3017
|
+
}
|
|
3018
|
+
else {
|
|
3019
|
+
setError({
|
|
3020
|
+
code: 0,
|
|
3021
|
+
message: "An unknown error occurred.",
|
|
3022
|
+
type: "unknown",
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
}, [setError]);
|
|
3026
|
+
const retry = useCallback(() => {
|
|
3027
|
+
clearError();
|
|
3028
|
+
const { videoRef } = useVideoStore.getState();
|
|
3029
|
+
if (videoRef) {
|
|
3030
|
+
videoRef.load();
|
|
3031
|
+
videoRef.play().catch(() => undefined);
|
|
3032
|
+
}
|
|
3033
|
+
}, [clearError]);
|
|
3034
|
+
return {
|
|
3035
|
+
error,
|
|
3036
|
+
handleVideoError,
|
|
3037
|
+
clearError,
|
|
3038
|
+
retry,
|
|
3039
|
+
};
|
|
3040
|
+
};
|
|
3041
|
+
|
|
3042
|
+
const AdOverlay = React__default.memo(({ adBreak, onSkip, config }) => {
|
|
3043
|
+
const { adVideoRef, setAdVideoRef, adCurrentTime, setAdCurrentTime, canSkipAd, setCanSkipAd, skipCountdown, setSkipCountdown, videoRef, muted, setIsPlaying, } = useVideoStore(useShallow((state) => ({
|
|
3044
|
+
adVideoRef: state.adVideoRef,
|
|
3045
|
+
setAdVideoRef: state.setAdVideoRef,
|
|
3046
|
+
adCurrentTime: state.adCurrentTime,
|
|
3047
|
+
setAdCurrentTime: state.setAdCurrentTime,
|
|
3048
|
+
canSkipAd: state.canSkipAd,
|
|
3049
|
+
setCanSkipAd: state.setCanSkipAd,
|
|
3050
|
+
skipCountdown: state.skipCountdown,
|
|
3051
|
+
setSkipCountdown: state.setSkipCountdown,
|
|
3052
|
+
videoRef: state.videoRef,
|
|
3053
|
+
muted: state.muted,
|
|
3054
|
+
setIsPlaying: state.setIsPlaying,
|
|
3055
|
+
})));
|
|
2726
3056
|
const [showControls, setShowControls] = useState(true);
|
|
2727
3057
|
const [isHovered, setIsHovered] = useState(false);
|
|
2728
3058
|
const [adDuration, setAdDuration] = useState(0);
|
|
2729
3059
|
const [requiresInteraction, setRequiresInteraction] = useState(false);
|
|
3060
|
+
const [adLoadError, setAdLoadError] = useState(false);
|
|
2730
3061
|
const controlsTimeoutRef = useRef(null);
|
|
3062
|
+
const loadTimeoutRef = useRef(null);
|
|
2731
3063
|
const safelySetCanSkipAd = useCallback((value) => {
|
|
2732
3064
|
if (useVideoStore.getState().canSkipAd !== value) {
|
|
2733
3065
|
setCanSkipAd(value);
|
|
@@ -2768,7 +3100,23 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2768
3100
|
useEffect(() => {
|
|
2769
3101
|
setAdDuration(0);
|
|
2770
3102
|
setRequiresInteraction(false);
|
|
2771
|
-
|
|
3103
|
+
setAdLoadError(false);
|
|
3104
|
+
setAdCurrentTime(0);
|
|
3105
|
+
if (loadTimeoutRef.current) {
|
|
3106
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3107
|
+
loadTimeoutRef.current = null;
|
|
3108
|
+
}
|
|
3109
|
+
if (adBreak.skipable !== undefined) {
|
|
3110
|
+
setCanSkipAd(false);
|
|
3111
|
+
setSkipCountdown(0);
|
|
3112
|
+
}
|
|
3113
|
+
}, [
|
|
3114
|
+
adBreak.id,
|
|
3115
|
+
adBreak.skipable,
|
|
3116
|
+
setAdCurrentTime,
|
|
3117
|
+
setCanSkipAd,
|
|
3118
|
+
setSkipCountdown,
|
|
3119
|
+
]);
|
|
2772
3120
|
useEffect(() => {
|
|
2773
3121
|
if (!adBreak.skipable) {
|
|
2774
3122
|
safelySetCanSkipAd(false);
|
|
@@ -2792,43 +3140,76 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2792
3140
|
if (!adVideoRef)
|
|
2793
3141
|
return;
|
|
2794
3142
|
setRequiresInteraction(false);
|
|
3143
|
+
setAdLoadError(false);
|
|
3144
|
+
if (!adVideoRef.src && adBreak.adUrl) {
|
|
3145
|
+
adVideoRef.src = adBreak.adUrl;
|
|
3146
|
+
adVideoRef.load();
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
2795
3149
|
const playPromise = adVideoRef.play();
|
|
2796
3150
|
if (playPromise && "catch" in playPromise) {
|
|
2797
|
-
playPromise.catch(() => {
|
|
3151
|
+
playPromise.catch((error) => {
|
|
3152
|
+
console.warn("Ad play failed:", error);
|
|
2798
3153
|
setRequiresInteraction(true);
|
|
2799
3154
|
setIsPlaying(false);
|
|
2800
3155
|
});
|
|
2801
3156
|
}
|
|
2802
|
-
}, [adVideoRef, setIsPlaying]);
|
|
3157
|
+
}, [adVideoRef, adBreak.adUrl, setIsPlaying]);
|
|
3158
|
+
const timeUpdateRafRef = useRef(null);
|
|
3159
|
+
const lastUpdateTimeRef = useRef(0);
|
|
2803
3160
|
useEffect(() => {
|
|
2804
3161
|
if (!adVideoRef)
|
|
2805
3162
|
return;
|
|
2806
3163
|
const handleTimeUpdate = () => {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3164
|
+
if (timeUpdateRafRef.current !== null)
|
|
3165
|
+
return;
|
|
3166
|
+
timeUpdateRafRef.current = requestAnimationFrame(() => {
|
|
3167
|
+
timeUpdateRafRef.current = null;
|
|
3168
|
+
const currentTime = adVideoRef.currentTime;
|
|
3169
|
+
if (Math.abs(currentTime - lastUpdateTimeRef.current) < 0.1) {
|
|
3170
|
+
return;
|
|
2814
3171
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3172
|
+
lastUpdateTimeRef.current = currentTime;
|
|
3173
|
+
setAdCurrentTime(currentTime);
|
|
3174
|
+
if (adBreak.skipable) {
|
|
3175
|
+
const remaining = skipAfter - currentTime;
|
|
3176
|
+
if (remaining <= 0) {
|
|
3177
|
+
safelySetCanSkipAd(true);
|
|
3178
|
+
safelySetSkipCountdown(0);
|
|
3179
|
+
}
|
|
3180
|
+
else {
|
|
3181
|
+
const remainingForDisplay = Math.max(Math.ceil(remaining), 0);
|
|
3182
|
+
safelySetSkipCountdown(remainingForDisplay);
|
|
3183
|
+
if (canSkipAd) {
|
|
3184
|
+
safelySetCanSkipAd(false);
|
|
3185
|
+
}
|
|
2820
3186
|
}
|
|
2821
3187
|
}
|
|
2822
|
-
}
|
|
3188
|
+
});
|
|
2823
3189
|
};
|
|
2824
3190
|
const handleLoadedMetadata = () => {
|
|
3191
|
+
if (loadTimeoutRef.current) {
|
|
3192
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3193
|
+
loadTimeoutRef.current = null;
|
|
3194
|
+
}
|
|
2825
3195
|
const duration = Number.isFinite(adVideoRef.duration)
|
|
2826
3196
|
? adVideoRef.duration
|
|
2827
3197
|
: 0;
|
|
2828
3198
|
setAdDuration(duration);
|
|
3199
|
+
setAdLoadError(false);
|
|
2829
3200
|
setIsPlaying(!adVideoRef.paused);
|
|
2830
3201
|
attemptAdPlayback();
|
|
2831
3202
|
};
|
|
3203
|
+
if (loadTimeoutRef.current) {
|
|
3204
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3205
|
+
}
|
|
3206
|
+
loadTimeoutRef.current = setTimeout(() => {
|
|
3207
|
+
if (adVideoRef && adVideoRef.readyState < 2) {
|
|
3208
|
+
console.warn("Ad load timeout:", adBreak.id);
|
|
3209
|
+
setAdLoadError(true);
|
|
3210
|
+
setRequiresInteraction(true);
|
|
3211
|
+
}
|
|
3212
|
+
}, 30000);
|
|
2832
3213
|
const handlePlay = () => {
|
|
2833
3214
|
setIsPlaying(true);
|
|
2834
3215
|
setRequiresInteraction(false);
|
|
@@ -2843,7 +3224,21 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2843
3224
|
setIsPlaying(true);
|
|
2844
3225
|
setRequiresInteraction(false);
|
|
2845
3226
|
};
|
|
2846
|
-
const handleError = () => {
|
|
3227
|
+
const handleError = (e) => {
|
|
3228
|
+
if (loadTimeoutRef.current) {
|
|
3229
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3230
|
+
loadTimeoutRef.current = null;
|
|
3231
|
+
}
|
|
3232
|
+
const error = e.target;
|
|
3233
|
+
const errorCode = error.error?.code;
|
|
3234
|
+
const errorMessage = error.error?.message || "Unknown ad error";
|
|
3235
|
+
console.error("Ad playback error:", {
|
|
3236
|
+
adId: adBreak.id,
|
|
3237
|
+
errorCode,
|
|
3238
|
+
errorMessage,
|
|
3239
|
+
src: adVideoRef.src,
|
|
3240
|
+
});
|
|
3241
|
+
setAdLoadError(true);
|
|
2847
3242
|
setRequiresInteraction(true);
|
|
2848
3243
|
setIsPlaying(false);
|
|
2849
3244
|
};
|
|
@@ -2862,10 +3257,20 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2862
3257
|
adVideoRef.removeEventListener("waiting", handleWaiting);
|
|
2863
3258
|
adVideoRef.removeEventListener("playing", handlePlaying);
|
|
2864
3259
|
adVideoRef.removeEventListener("error", handleError);
|
|
3260
|
+
if (loadTimeoutRef.current) {
|
|
3261
|
+
clearTimeout(loadTimeoutRef.current);
|
|
3262
|
+
loadTimeoutRef.current = null;
|
|
3263
|
+
}
|
|
3264
|
+
if (timeUpdateRafRef.current !== null) {
|
|
3265
|
+
cancelAnimationFrame(timeUpdateRafRef.current);
|
|
3266
|
+
timeUpdateRafRef.current = null;
|
|
3267
|
+
}
|
|
3268
|
+
lastUpdateTimeRef.current = 0;
|
|
2865
3269
|
};
|
|
2866
3270
|
}, [
|
|
2867
3271
|
adVideoRef,
|
|
2868
3272
|
adBreak.skipable,
|
|
3273
|
+
adBreak.id,
|
|
2869
3274
|
skipAfter,
|
|
2870
3275
|
canSkipAd,
|
|
2871
3276
|
setAdCurrentTime,
|
|
@@ -2877,41 +3282,78 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2877
3282
|
useEffect(() => {
|
|
2878
3283
|
if (!adVideoRef || !videoRef)
|
|
2879
3284
|
return;
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3285
|
+
// Sync volume and muted state
|
|
3286
|
+
adVideoRef.volume = videoRef.volume;
|
|
3287
|
+
adVideoRef.muted = muted;
|
|
3288
|
+
// Check if src needs to be updated
|
|
3289
|
+
const currentSrc = adVideoRef.src || adVideoRef.currentSrc || "";
|
|
3290
|
+
const needsReload = !currentSrc || currentSrc !== adBreak.adUrl;
|
|
3291
|
+
// Load ad if needed
|
|
3292
|
+
if (needsReload && adBreak.adUrl) {
|
|
3293
|
+
// Clear previous src
|
|
3294
|
+
try {
|
|
3295
|
+
adVideoRef.pause();
|
|
3296
|
+
adVideoRef.removeAttribute("src");
|
|
3297
|
+
adVideoRef.src = "";
|
|
3298
|
+
// Set new src
|
|
3299
|
+
adVideoRef.src = adBreak.adUrl;
|
|
3300
|
+
adVideoRef.load();
|
|
3301
|
+
}
|
|
3302
|
+
catch (error) {
|
|
3303
|
+
console.warn("Error loading ad:", error);
|
|
3304
|
+
setAdLoadError(true);
|
|
3305
|
+
}
|
|
2884
3306
|
}
|
|
2885
3307
|
const handleCanPlay = () => {
|
|
2886
|
-
if (adVideoRef
|
|
3308
|
+
if (!adVideoRef || adVideoRef.paused === false)
|
|
2887
3309
|
return;
|
|
2888
3310
|
attemptAdPlayback();
|
|
2889
3311
|
};
|
|
3312
|
+
const handleLoadedData = () => {
|
|
3313
|
+
// Ensure volume is synced after load
|
|
3314
|
+
if (videoRef && adVideoRef) {
|
|
3315
|
+
try {
|
|
3316
|
+
adVideoRef.volume = videoRef.volume;
|
|
3317
|
+
adVideoRef.muted = muted;
|
|
3318
|
+
}
|
|
3319
|
+
catch (error) {
|
|
3320
|
+
// Ignore errors during cleanup
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
2890
3324
|
adVideoRef.addEventListener("canplay", handleCanPlay);
|
|
2891
|
-
|
|
3325
|
+
adVideoRef.addEventListener("loadeddata", handleLoadedData);
|
|
3326
|
+
// Try to play if already ready and src matches
|
|
3327
|
+
if (adVideoRef.readyState >= 3 && !needsReload) {
|
|
2892
3328
|
attemptAdPlayback();
|
|
2893
3329
|
}
|
|
2894
3330
|
return () => {
|
|
2895
|
-
adVideoRef
|
|
3331
|
+
if (adVideoRef) {
|
|
3332
|
+
adVideoRef.removeEventListener("canplay", handleCanPlay);
|
|
3333
|
+
adVideoRef.removeEventListener("loadeddata", handleLoadedData);
|
|
3334
|
+
}
|
|
2896
3335
|
};
|
|
2897
|
-
}, [adVideoRef, videoRef, attemptAdPlayback]);
|
|
3336
|
+
}, [adVideoRef, videoRef, muted, adBreak.adUrl, attemptAdPlayback]);
|
|
2898
3337
|
useEffect(() => {
|
|
2899
|
-
if (adVideoRef)
|
|
3338
|
+
if (!adVideoRef)
|
|
3339
|
+
return;
|
|
3340
|
+
try {
|
|
3341
|
+
// Sync muted state
|
|
2900
3342
|
adVideoRef.muted = muted;
|
|
3343
|
+
// Sync volume with main video
|
|
3344
|
+
if (videoRef) {
|
|
3345
|
+
adVideoRef.volume = videoRef.volume;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
catch (error) {
|
|
3349
|
+
// Ignore errors during state sync
|
|
2901
3350
|
}
|
|
2902
|
-
}, [adVideoRef, muted]);
|
|
3351
|
+
}, [adVideoRef, muted, videoRef]);
|
|
2903
3352
|
const handleSkip = () => {
|
|
2904
3353
|
if (canSkipAd && onSkip) {
|
|
2905
3354
|
onSkip();
|
|
2906
3355
|
}
|
|
2907
3356
|
};
|
|
2908
|
-
const formatTime = (seconds) => {
|
|
2909
|
-
if (isNaN(seconds) || seconds < 0)
|
|
2910
|
-
return "0:00";
|
|
2911
|
-
const mins = Math.floor(seconds / 60);
|
|
2912
|
-
const secs = Math.floor(seconds % 60);
|
|
2913
|
-
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2914
|
-
};
|
|
2915
3357
|
const progressPercent = adDuration > 0 ? (adCurrentTime / adDuration) * 100 : 0;
|
|
2916
3358
|
return (React__default.createElement("div", { className: "absolute inset-0 bg-black z-50 flex flex-col overflow-hidden transition-opacity duration-300", onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), onMouseMove: () => {
|
|
2917
3359
|
setIsHovered(true);
|
|
@@ -2919,23 +3361,31 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2919
3361
|
} },
|
|
2920
3362
|
React__default.createElement("div", { className: "relative flex-1 w-full flex items-center justify-center" },
|
|
2921
3363
|
React__default.createElement("video", { ref: (ref) => {
|
|
2922
|
-
if (ref
|
|
2923
|
-
|
|
3364
|
+
if (!ref)
|
|
3365
|
+
return;
|
|
3366
|
+
if (ref !== adVideoRef) {
|
|
2924
3367
|
setAdVideoRef(ref);
|
|
2925
|
-
|
|
3368
|
+
}
|
|
3369
|
+
ref.muted = muted;
|
|
3370
|
+
if (videoRef) {
|
|
3371
|
+
ref.volume = videoRef.volume;
|
|
3372
|
+
}
|
|
3373
|
+
if (adBreak.adUrl) {
|
|
3374
|
+
const currentSrc = ref.src || ref.currentSrc || "";
|
|
3375
|
+
if (currentSrc !== adBreak.adUrl) {
|
|
2926
3376
|
ref.src = adBreak.adUrl;
|
|
2927
|
-
ref.load();
|
|
2928
3377
|
}
|
|
2929
3378
|
}
|
|
2930
3379
|
}, className: "w-full h-full object-contain", autoPlay: true, playsInline: true, muted: muted, preload: "auto", key: adBreak.id }),
|
|
2931
|
-
requiresInteraction && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
|
|
2932
|
-
React__default.createElement("
|
|
3380
|
+
(requiresInteraction || adLoadError) && (React__default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm" },
|
|
3381
|
+
React__default.createElement("div", { className: "flex flex-col items-center gap-4" },
|
|
3382
|
+
adLoadError && (React__default.createElement("p", { className: "text-red-400 text-sm" }, "Ad failed to load")),
|
|
3383
|
+
React__default.createElement("button", { onClick: attemptAdPlayback, className: "px-5 py-3 rounded bg-white/20 text-white font-semibold border border-white/40 hover:bg-white/30 transition" }, adLoadError ? "Retry Ad" : "Tap to Play Ad"))))),
|
|
2933
3384
|
React__default.createElement("div", { className: `absolute inset-0 transition-all duration-300 ${showControls ? "opacity-100" : "opacity-0 pointer-events-none"}` },
|
|
2934
3385
|
React__default.createElement("div", { className: "absolute inset-0 bg-linear-to-b from-black/80 via-transparent to-black/90 flex flex-col justify-between" },
|
|
2935
3386
|
React__default.createElement("div", { className: "shrink-0 relative" },
|
|
2936
3387
|
React__default.createElement(ControlsHeader, { config: {
|
|
2937
|
-
title:
|
|
2938
|
-
config?.config?.headerConfig?.config?.title ||
|
|
3388
|
+
title: config?.config?.headerConfig?.config?.title ||
|
|
2939
3389
|
"Advertisement",
|
|
2940
3390
|
isTrailer: config?.config?.headerConfig?.config?.isTrailer,
|
|
2941
3391
|
onClose: config?.config?.headerConfig?.config?.onClose,
|
|
@@ -2955,89 +3405,190 @@ const AdOverlay = ({ adBreak, onSkip, config }) => {
|
|
|
2955
3405
|
React__default.createElement("div", { className: "relative h-1 bg-white/20 rounded-full overflow-hidden pointer-events-none select-none" },
|
|
2956
3406
|
React__default.createElement("div", { className: "absolute left-0 top-0 h-full bg-white rounded-full transition-all duration-300 ease-out", style: { width: `${progressPercent}%` } }),
|
|
2957
3407
|
React__default.createElement("div", { className: "absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full shadow-lg transition-all duration-300 ease-out pointer-events-none", style: { left: `calc(${progressPercent}% - 6px)` } }))),
|
|
2958
|
-
React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-
|
|
2959
|
-
React__default.createElement("
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3408
|
+
sponsoredUrl && (React__default.createElement("div", { className: "px-10 pb-6 flex items-center justify-end" },
|
|
3409
|
+
React__default.createElement("a", { href: sponsoredUrl, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-semibold text-sky-300 hover:text-white transition-colors" }, "Learn More"))))))));
|
|
3410
|
+
});
|
|
3411
|
+
AdOverlay.displayName = "AdOverlay";
|
|
3412
|
+
|
|
3413
|
+
const ErrorOverlay = React__default.memo(({ error, onRetry }) => {
|
|
3414
|
+
const getIcon = () => {
|
|
3415
|
+
switch (error.type) {
|
|
3416
|
+
case "network":
|
|
3417
|
+
return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
3418
|
+
React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" })));
|
|
3419
|
+
case "src":
|
|
3420
|
+
return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
3421
|
+
React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" }),
|
|
3422
|
+
React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 3l18 18" })));
|
|
3423
|
+
default:
|
|
3424
|
+
return (React__default.createElement("svg", { className: "w-12 h-12 text-red-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
3425
|
+
React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" })));
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3428
|
+
return (React__default.createElement("div", { className: "absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90" },
|
|
3429
|
+
React__default.createElement("div", { className: "flex flex-col items-center gap-4 p-6 text-center" },
|
|
3430
|
+
getIcon(),
|
|
3431
|
+
React__default.createElement("h3", { className: "text-xl font-semibold text-white" }, error.type === "network"
|
|
3432
|
+
? "Network Error"
|
|
3433
|
+
: error.type === "src"
|
|
3434
|
+
? "Video Unavailable"
|
|
3435
|
+
: "Playback Error"),
|
|
3436
|
+
React__default.createElement("p", { className: "text-sm text-gray-400 max-w-md" }, error.message),
|
|
3437
|
+
React__default.createElement("button", { onClick: onRetry, className: "mt-4 px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center gap-2" },
|
|
3438
|
+
React__default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
3439
|
+
React__default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" })),
|
|
3440
|
+
"Retry"))));
|
|
3441
|
+
});
|
|
3442
|
+
ErrorOverlay.displayName = "ErrorOverlay";
|
|
2965
3443
|
|
|
2966
3444
|
var css_248z$1 = ".video-player video::cue {\n display: none !important;\n opacity: 0 !important;\n visibility: hidden !important;\n}\n\n.custom-subtitle-overlay {\n position: absolute;\n bottom: 10%;\n left: 50%;\n transform: translateX(-50%);\n\n font-size: 1.5rem;\n font-weight: 600;\n line-height: 1.4;\n text-align: center;\n\n color: #000;\n background: rgba(255, 255, 255, 0.6);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n\n padding: 10px 16px;\n border-radius: 10px;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n\n max-width: 70%;\n min-width: fit-content;\n\n transition: all 0.2s ease-in-out;\n}\n\n.custom-subtitle-overlay:hover {\n transform: translateX(-50%) scale(1.02);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);\n}\n\n@media (max-width: 768px) {\n .custom-subtitle-overlay {\n font-size: 1.25rem;\n padding: 8px 14px;\n bottom: 8%;\n max-width: 85%;\n }\n}\n\n@media (max-width: 480px) {\n .custom-subtitle-overlay {\n font-size: 1rem;\n padding: 6px 10px;\n bottom: 6%;\n max-width: 90%;\n }\n}\n\n@media (prefers-contrast: high) {\n .custom-subtitle-overlay {\n background: #ffff00;\n color: #000;\n border: 3px solid #000;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-subtitle-overlay {\n transition: none;\n }\n\n .custom-subtitle-overlay:hover {\n transform: translateX(-50%);\n }\n}\n";
|
|
2967
3445
|
styleInject(css_248z$1,{"insertAt":"top"});
|
|
2968
3446
|
|
|
2969
|
-
var css_248z = "
|
|
3447
|
+
var css_248z = "\n.loader {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n display: inline-block;\n border-top: 3px solid #fff;\n border-right: 3px solid transparent;\n box-sizing: border-box;\n animation: rotation 1s linear infinite;\n}\n\n@keyframes rotation {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .loader {\n animation: none;\n }\n}\n";
|
|
2970
3448
|
styleInject(css_248z,{"insertAt":"top"});
|
|
2971
3449
|
|
|
2972
|
-
const VideoPlayer = ({
|
|
2973
|
-
const {
|
|
3450
|
+
const VideoPlayer = React__default.memo(({ video, style, events, features }) => {
|
|
3451
|
+
const { src: trackSrc, title: trackTitle, poster: trackPoster, type, isTrailer, showControls = true, isMute = false, startFrom, } = video;
|
|
3452
|
+
const { className, width, height, subtitleStyle, qualityConfig } = style || {};
|
|
3453
|
+
const { onEnded, onError, onClose, onWatchHistoryUpdate } = events || {};
|
|
3454
|
+
const { timeCodes, getPreviewScreenUrl, tracking, subtitles, episodeList, currentEpisodeIndex = 0, intro, nextEpisodeConfig, ads, } = features || {};
|
|
3455
|
+
const { setVideoWrapperRef, setActiveQuality } = useVideoStore(useShallow((state) => ({
|
|
2974
3456
|
setVideoWrapperRef: state.setVideoWrapperRef,
|
|
3457
|
+
setActiveQuality: state.setActiveQuality,
|
|
2975
3458
|
})));
|
|
3459
|
+
React__default.useEffect(() => {
|
|
3460
|
+
if (qualityConfig?.defaultQuality) {
|
|
3461
|
+
setActiveQuality(qualityConfig.defaultQuality);
|
|
3462
|
+
}
|
|
3463
|
+
}, [qualityConfig?.defaultQuality, setActiveQuality]);
|
|
2976
3464
|
const effectiveAds = React__default.useMemo(() => (isTrailer ? undefined : ads), [ads, isTrailer]);
|
|
2977
3465
|
const hasPreRoll = React__default.useMemo(() => Boolean(effectiveAds?.preRoll), [effectiveAds?.preRoll]);
|
|
2978
3466
|
const { registerVideoRef, videoRef, isAdPlaying, currentAd, initialAdFinished, shouldCoverMainVideo, shouldShowPlaceholder, } = usePrimaryVideoLifecycle({
|
|
2979
3467
|
hasPreRoll,
|
|
2980
3468
|
trackSrc,
|
|
2981
3469
|
});
|
|
3470
|
+
const onWatchHistoryUpdateRef = React__default.useRef(onWatchHistoryUpdate);
|
|
3471
|
+
React__default.useEffect(() => {
|
|
3472
|
+
onWatchHistoryUpdateRef.current = onWatchHistoryUpdate;
|
|
3473
|
+
}, [onWatchHistoryUpdate]);
|
|
3474
|
+
const getWatchHistoryData = React__default.useCallback(() => {
|
|
3475
|
+
const video = useVideoStore.getState().videoRef;
|
|
3476
|
+
if (!video || !video.duration || isNaN(video.duration))
|
|
3477
|
+
return null;
|
|
3478
|
+
const currentTime = video.currentTime || 0;
|
|
3479
|
+
const duration = video.duration;
|
|
3480
|
+
const progress = Math.round((currentTime / duration) * 100);
|
|
3481
|
+
const isCompleted = progress >= 90;
|
|
3482
|
+
return {
|
|
3483
|
+
currentTime,
|
|
3484
|
+
duration,
|
|
3485
|
+
progress,
|
|
3486
|
+
isCompleted,
|
|
3487
|
+
watchedAt: Date.now(),
|
|
3488
|
+
};
|
|
3489
|
+
}, []);
|
|
3490
|
+
const handleClose = React__default.useCallback(() => {
|
|
3491
|
+
const historyData = getWatchHistoryData();
|
|
3492
|
+
if (historyData && onWatchHistoryUpdate) {
|
|
3493
|
+
onWatchHistoryUpdate(historyData);
|
|
3494
|
+
}
|
|
3495
|
+
onClose?.();
|
|
3496
|
+
}, [getWatchHistoryData, onWatchHistoryUpdate, onClose]);
|
|
3497
|
+
const overlayConfig = React__default.useMemo(() => ({
|
|
3498
|
+
headerConfig: {
|
|
3499
|
+
config: {
|
|
3500
|
+
isTrailer: isTrailer,
|
|
3501
|
+
title: trackTitle,
|
|
3502
|
+
onClose: handleClose,
|
|
3503
|
+
videoRef: videoRef,
|
|
3504
|
+
qualityConfig,
|
|
3505
|
+
},
|
|
3506
|
+
},
|
|
3507
|
+
bottomConfig: {
|
|
3508
|
+
config: {
|
|
3509
|
+
seekBarConfig: {
|
|
3510
|
+
timeCodes: timeCodes,
|
|
3511
|
+
trackColor: "red",
|
|
3512
|
+
getPreviewScreenUrl,
|
|
3513
|
+
},
|
|
3514
|
+
},
|
|
3515
|
+
},
|
|
3516
|
+
}), [
|
|
3517
|
+
isTrailer,
|
|
3518
|
+
trackTitle,
|
|
3519
|
+
handleClose,
|
|
3520
|
+
videoRef,
|
|
3521
|
+
timeCodes,
|
|
3522
|
+
getPreviewScreenUrl,
|
|
3523
|
+
qualityConfig,
|
|
3524
|
+
]);
|
|
3525
|
+
const adOverlayConfig = React__default.useMemo(() => ({
|
|
3526
|
+
config: {
|
|
3527
|
+
headerConfig: {
|
|
3528
|
+
config: {
|
|
3529
|
+
isTrailer: isTrailer,
|
|
3530
|
+
title: trackTitle,
|
|
3531
|
+
onClose: handleClose,
|
|
3532
|
+
},
|
|
3533
|
+
},
|
|
3534
|
+
bottomConfig: {
|
|
3535
|
+
config: {
|
|
3536
|
+
seekBarConfig: {
|
|
3537
|
+
timeCodes: timeCodes,
|
|
3538
|
+
trackColor: "red",
|
|
3539
|
+
getPreviewScreenUrl,
|
|
3540
|
+
},
|
|
3541
|
+
},
|
|
3542
|
+
},
|
|
3543
|
+
},
|
|
3544
|
+
}), [isTrailer, trackTitle, handleClose, timeCodes, getPreviewScreenUrl]);
|
|
2982
3545
|
useVideoSource(trackSrc, type);
|
|
2983
3546
|
useSubtitles(subtitles);
|
|
2984
3547
|
useSubtitleStyling(subtitleStyle);
|
|
2985
|
-
useVideoTracking(tracking, episodeList, currentEpisodeIndex,
|
|
3548
|
+
useVideoTracking(tracking, episodeList, currentEpisodeIndex, handleClose);
|
|
2986
3549
|
const { showSkipIntro, handleSkipIntro } = useIntroSkip(intro);
|
|
2987
3550
|
useEpisodes(episodeList, currentEpisodeIndex, nextEpisodeConfig);
|
|
2988
3551
|
const { onSeeked, onTimeUpdate, onLoadedMetadata, onProgress, onPlay, onPause, onEnded: onEndedHook, } = useVideoEvents();
|
|
2989
3552
|
const { skipAd } = useAdManager(effectiveAds);
|
|
3553
|
+
const { error, handleVideoError, retry } = useVideoError();
|
|
3554
|
+
const hasResumedRef = React__default.useRef(false);
|
|
3555
|
+
React__default.useEffect(() => {
|
|
3556
|
+
return () => {
|
|
3557
|
+
const historyData = getWatchHistoryData();
|
|
3558
|
+
if (historyData && onWatchHistoryUpdateRef.current) {
|
|
3559
|
+
onWatchHistoryUpdateRef.current(historyData);
|
|
3560
|
+
}
|
|
3561
|
+
};
|
|
3562
|
+
}, [getWatchHistoryData]);
|
|
3563
|
+
React__default.useEffect(() => {
|
|
3564
|
+
if (!videoRef || !startFrom || hasResumedRef.current)
|
|
3565
|
+
return;
|
|
3566
|
+
const handleCanPlay = () => {
|
|
3567
|
+
if (!hasResumedRef.current && startFrom > 0) {
|
|
3568
|
+
videoRef.currentTime = startFrom;
|
|
3569
|
+
hasResumedRef.current = true;
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
videoRef.addEventListener("canplay", handleCanPlay);
|
|
3573
|
+
return () => videoRef.removeEventListener("canplay", handleCanPlay);
|
|
3574
|
+
}, [videoRef, startFrom]);
|
|
2990
3575
|
return (React__default.createElement("div", { ref: setVideoWrapperRef, className: `video-player ${height || "h-full"} ${width || "w-full"} mx-auto absolute` },
|
|
2991
3576
|
trackPoster && (React__default.createElement("div", { className: "pip-poster absolute inset-0 bg-center bg-cover hidden", style: { backgroundImage: `url(${trackPoster})` } })),
|
|
2992
3577
|
React__default.createElement("video", { playsInline: true, preload: hasPreRoll ? "metadata" : "auto", ref: registerVideoRef, onSeeked: onSeeked, poster: trackPoster, crossOrigin: "anonymous", controls: false, disableRemotePlayback: true, controlsList: "nodownload", onContextMenu: (e) => e.preventDefault(), onTimeUpdate: onTimeUpdate, onLoadedMetadata: onLoadedMetadata, onProgress: onProgress, onPlay: onPlay, onPause: onPause, onEnded: (e) => {
|
|
2993
3578
|
onEndedHook(e);
|
|
2994
3579
|
onEnded?.(e);
|
|
2995
3580
|
}, onError: (e) => {
|
|
3581
|
+
handleVideoError(e);
|
|
2996
3582
|
onError?.(e);
|
|
2997
3583
|
}, autoPlay: !hasPreRoll, muted: isMute, className: `w-full h-full relative ${className || ""} ${shouldCoverMainVideo ? "opacity-0" : "opacity-100"} transition-opacity duration-200 ease-out` }),
|
|
2998
|
-
shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex items-center justify-center bg-black" },
|
|
2999
|
-
React__default.createElement(
|
|
3000
|
-
showControls && initialAdFinished && (React__default.createElement(Overlay, { config:
|
|
3001
|
-
headerConfig: {
|
|
3002
|
-
config: {
|
|
3003
|
-
isTrailer: isTrailer,
|
|
3004
|
-
title: trackTitle,
|
|
3005
|
-
onClose: onClose,
|
|
3006
|
-
videoRef: videoRef,
|
|
3007
|
-
},
|
|
3008
|
-
},
|
|
3009
|
-
bottomConfig: {
|
|
3010
|
-
config: {
|
|
3011
|
-
seekBarConfig: {
|
|
3012
|
-
timeCodes: timeCodes,
|
|
3013
|
-
trackColor: "red",
|
|
3014
|
-
getPreviewScreenUrl,
|
|
3015
|
-
},
|
|
3016
|
-
},
|
|
3017
|
-
},
|
|
3018
|
-
} })),
|
|
3584
|
+
shouldShowPlaceholder && (React__default.createElement("div", { className: "absolute inset-0 z-40 flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm" },
|
|
3585
|
+
React__default.createElement(Loader, { className: "w-14 h-14 lg:w-18 lg:h-18 animate-spin text-white" }))),
|
|
3586
|
+
showControls && initialAdFinished && (React__default.createElement(Overlay, { config: overlayConfig })),
|
|
3019
3587
|
React__default.createElement(SubtitleOverlay, { styleConfig: subtitleStyle }),
|
|
3020
3588
|
showSkipIntro && !isAdPlaying && initialAdFinished && (React__default.createElement(VideoActionButton, { text: "Skip Intro", onClick: handleSkipIntro, position: "left" })),
|
|
3021
|
-
isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config:
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
isTrailer: isTrailer,
|
|
3026
|
-
title: trackTitle,
|
|
3027
|
-
onClose: onClose,
|
|
3028
|
-
},
|
|
3029
|
-
},
|
|
3030
|
-
bottomConfig: {
|
|
3031
|
-
config: {
|
|
3032
|
-
seekBarConfig: {
|
|
3033
|
-
timeCodes: timeCodes,
|
|
3034
|
-
trackColor: "red",
|
|
3035
|
-
getPreviewScreenUrl,
|
|
3036
|
-
},
|
|
3037
|
-
},
|
|
3038
|
-
},
|
|
3039
|
-
},
|
|
3040
|
-
} }))));
|
|
3041
|
-
};
|
|
3589
|
+
isAdPlaying && currentAd && (React__default.createElement(AdOverlay, { adBreak: currentAd, onSkip: skipAd, config: adOverlayConfig })),
|
|
3590
|
+
error && onError && React__default.createElement(ErrorOverlay, { error: error, onRetry: retry })));
|
|
3591
|
+
});
|
|
3592
|
+
VideoPlayer.displayName = "VideoPlayer";
|
|
3042
3593
|
|
|
3043
3594
|
export { VideoPlayer, useVideoStore };
|