@lightbird/ui 0.1.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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/index.cjs +2286 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +2247 -0
- package/dist/styles.css +1 -0
- package/package.json +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React10 from 'react';
|
|
3
|
+
import React10__default, { useMemo, useState, useRef, useEffect, useCallback, Component } from 'react';
|
|
4
|
+
import { clsx } from 'clsx';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
9
|
+
import { cva } from 'class-variance-authority';
|
|
10
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
11
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
12
|
+
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
13
|
+
import { Circle, SkipBack, Pause, Play, SkipForward, VolumeX, Volume2, Rewind, FastForward, AudioLines, Loader2, Subtitles, Plus, X, List, Settings2, Info, Keyboard, Camera, RotateCcw, PictureInPicture2, Minimize, Maximize, ChevronDown, ChevronUp, Check, ChevronLeft, ListVideo, Download, Upload, Pin, PinOff, Minimize2, Maximize2, ChevronRight, FilePlus, FolderOpen, Link, AlertCircle, GripVertical, Tv } from 'lucide-react';
|
|
14
|
+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
15
|
+
import { DndContext, closestCenter } from '@dnd-kit/core';
|
|
16
|
+
import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
|
|
17
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
18
|
+
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
|
19
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
20
|
+
import { exportPlaylist, formatShortcutKey, loadShortcuts, ProgressEstimator, createVideoPlayer, CancellationError, parseMediaError, captureVideoThumbnail, validateFile, parseM3U8, matchesShortcut, saveShortcuts, DEFAULT_SHORTCUTS } from '@lightbird/core';
|
|
21
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
22
|
+
import { usePlaylist, useVideoPlayback, useVideoFilters, useSubtitles, useFullscreen, usePictureInPicture, useVideoInfo, useProgressPersistence, useChapters, useKeyboardShortcuts, useMediaSession } from '@lightbird/core/react';
|
|
23
|
+
import * as ToastPrimitives from '@radix-ui/react-toast';
|
|
24
|
+
|
|
25
|
+
function cn(...inputs) {
|
|
26
|
+
return twMerge(clsx(inputs));
|
|
27
|
+
}
|
|
28
|
+
var Slider = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
29
|
+
SliderPrimitive.Root,
|
|
30
|
+
{
|
|
31
|
+
ref,
|
|
32
|
+
className: cn(
|
|
33
|
+
"relative flex w-full touch-none select-none items-center",
|
|
34
|
+
className
|
|
35
|
+
),
|
|
36
|
+
...props,
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsx(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsx(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
|
|
39
|
+
/* @__PURE__ */ jsx(SliderPrimitive.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
));
|
|
43
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
44
|
+
var buttonVariants = cva(
|
|
45
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
46
|
+
{
|
|
47
|
+
variants: {
|
|
48
|
+
variant: {
|
|
49
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
50
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
51
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
52
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
53
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
54
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
55
|
+
},
|
|
56
|
+
size: {
|
|
57
|
+
default: "h-10 px-4 py-2",
|
|
58
|
+
sm: "h-9 rounded-md px-3",
|
|
59
|
+
lg: "h-11 rounded-md px-8",
|
|
60
|
+
icon: "h-10 w-10"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
variant: "default",
|
|
65
|
+
size: "default"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
var Button = React10.forwardRef(
|
|
70
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
71
|
+
const Comp = asChild ? Slot : "button";
|
|
72
|
+
return /* @__PURE__ */ jsx(
|
|
73
|
+
Comp,
|
|
74
|
+
{
|
|
75
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
76
|
+
ref,
|
|
77
|
+
...props
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
Button.displayName = "Button";
|
|
83
|
+
var Popover = PopoverPrimitive.Root;
|
|
84
|
+
var PopoverTrigger = PopoverPrimitive.Trigger;
|
|
85
|
+
var PopoverContent = React10.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
86
|
+
PopoverPrimitive.Content,
|
|
87
|
+
{
|
|
88
|
+
ref,
|
|
89
|
+
align,
|
|
90
|
+
sideOffset,
|
|
91
|
+
className: cn(
|
|
92
|
+
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
93
|
+
className
|
|
94
|
+
),
|
|
95
|
+
...props
|
|
96
|
+
}
|
|
97
|
+
) }));
|
|
98
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
99
|
+
var TooltipProvider = TooltipPrimitive.Provider;
|
|
100
|
+
var Tooltip = TooltipPrimitive.Root;
|
|
101
|
+
var TooltipTrigger = TooltipPrimitive.Trigger;
|
|
102
|
+
var TooltipContent = React10.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
103
|
+
TooltipPrimitive.Content,
|
|
104
|
+
{
|
|
105
|
+
ref,
|
|
106
|
+
sideOffset,
|
|
107
|
+
className: cn(
|
|
108
|
+
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
109
|
+
className
|
|
110
|
+
),
|
|
111
|
+
...props
|
|
112
|
+
}
|
|
113
|
+
));
|
|
114
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
115
|
+
var labelVariants = cva(
|
|
116
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
117
|
+
);
|
|
118
|
+
var Label = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
119
|
+
LabelPrimitive.Root,
|
|
120
|
+
{
|
|
121
|
+
ref,
|
|
122
|
+
className: cn(labelVariants(), className),
|
|
123
|
+
...props
|
|
124
|
+
}
|
|
125
|
+
));
|
|
126
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
127
|
+
var RadioGroup = React10.forwardRef(({ className, ...props }, ref) => {
|
|
128
|
+
return /* @__PURE__ */ jsx(
|
|
129
|
+
RadioGroupPrimitive.Root,
|
|
130
|
+
{
|
|
131
|
+
className: cn("grid gap-2", className),
|
|
132
|
+
...props,
|
|
133
|
+
ref
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
138
|
+
var RadioGroupItem = React10.forwardRef(({ className, ...props }, ref) => {
|
|
139
|
+
return /* @__PURE__ */ jsx(
|
|
140
|
+
RadioGroupPrimitive.Item,
|
|
141
|
+
{
|
|
142
|
+
ref,
|
|
143
|
+
className: cn(
|
|
144
|
+
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
145
|
+
className
|
|
146
|
+
),
|
|
147
|
+
...props,
|
|
148
|
+
children: /* @__PURE__ */ jsx(RadioGroupPrimitive.Indicator, { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx(Circle, { className: "h-2.5 w-2.5 fill-current text-current" }) })
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
|
153
|
+
var formatTime = (time) => {
|
|
154
|
+
if (isNaN(time)) return "00:00";
|
|
155
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
156
|
+
date.setSeconds(time);
|
|
157
|
+
const timeString = date.toISOString().substr(11, 8);
|
|
158
|
+
return timeString.startsWith("00:") ? timeString.substr(3) : timeString;
|
|
159
|
+
};
|
|
160
|
+
var PlayerControls = React10__default.memo(function PlayerControls2({
|
|
161
|
+
isPlaying,
|
|
162
|
+
progress,
|
|
163
|
+
duration,
|
|
164
|
+
volume,
|
|
165
|
+
isMuted,
|
|
166
|
+
playbackRate,
|
|
167
|
+
loop,
|
|
168
|
+
isFullScreen,
|
|
169
|
+
filters,
|
|
170
|
+
zoom,
|
|
171
|
+
subtitles,
|
|
172
|
+
activeSubtitle,
|
|
173
|
+
audioTracks,
|
|
174
|
+
activeAudioTrack,
|
|
175
|
+
chapters = [],
|
|
176
|
+
currentChapter = null,
|
|
177
|
+
onPlayPause,
|
|
178
|
+
onSeek,
|
|
179
|
+
onVolumeChange,
|
|
180
|
+
onMuteToggle,
|
|
181
|
+
onPlaybackRateChange,
|
|
182
|
+
onLoopToggle,
|
|
183
|
+
onFullScreenToggle,
|
|
184
|
+
onFrameStep,
|
|
185
|
+
onScreenshot,
|
|
186
|
+
onNext,
|
|
187
|
+
onPrevious,
|
|
188
|
+
onFiltersChange,
|
|
189
|
+
onZoomChange,
|
|
190
|
+
onSubtitleChange,
|
|
191
|
+
onAudioTrackChange,
|
|
192
|
+
tracksLoading = false,
|
|
193
|
+
onSubtitleUpload,
|
|
194
|
+
onSubtitleRemove,
|
|
195
|
+
onShowInfo,
|
|
196
|
+
onOpenShortcuts,
|
|
197
|
+
onGoToChapter,
|
|
198
|
+
onTogglePiP,
|
|
199
|
+
isPiP = false,
|
|
200
|
+
pipSupported = false
|
|
201
|
+
}) {
|
|
202
|
+
const formattedProgress = useMemo(() => formatTime(progress), [progress]);
|
|
203
|
+
const formattedDuration = useMemo(() => formatTime(duration), [duration]);
|
|
204
|
+
const [chaptersMenuOpen, setChaptersMenuOpen] = useState(false);
|
|
205
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("div", { className: "absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-in-out flex flex-col gap-2", children: [
|
|
206
|
+
/* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
|
|
207
|
+
/* @__PURE__ */ jsx(
|
|
208
|
+
Slider,
|
|
209
|
+
{
|
|
210
|
+
value: [progress],
|
|
211
|
+
max: duration,
|
|
212
|
+
step: 1,
|
|
213
|
+
onValueChange: ([val]) => onSeek(val),
|
|
214
|
+
className: "w-full h-2"
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
chapters.length > 0 && duration > 0 && chapters.slice(1).map((chapter) => /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
218
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
219
|
+
"div",
|
|
220
|
+
{
|
|
221
|
+
"data-testid": "chapter-tick",
|
|
222
|
+
style: {
|
|
223
|
+
position: "absolute",
|
|
224
|
+
left: `${chapter.startTime / duration * 100}%`,
|
|
225
|
+
top: 0,
|
|
226
|
+
width: "2px",
|
|
227
|
+
height: "100%",
|
|
228
|
+
background: "white",
|
|
229
|
+
opacity: 0.5,
|
|
230
|
+
pointerEvents: "none",
|
|
231
|
+
transform: "translateX(-1px)"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
) }),
|
|
235
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
|
|
236
|
+
chapter.title,
|
|
237
|
+
" \u2014 ",
|
|
238
|
+
formatTime(chapter.startTime)
|
|
239
|
+
] }) })
|
|
240
|
+
] }, chapter.index))
|
|
241
|
+
] }),
|
|
242
|
+
currentChapter && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: currentChapter.title }),
|
|
243
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-white", children: [
|
|
244
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
|
|
245
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
246
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPrevious, children: /* @__PURE__ */ jsx(SkipBack, {}) }) }),
|
|
247
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Previous (N)" }) })
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
250
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPlayPause, children: isPlaying ? /* @__PURE__ */ jsx(Pause, {}) : /* @__PURE__ */ jsx(Play, {}) }) }),
|
|
251
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
|
|
252
|
+
isPlaying ? "Pause" : "Play",
|
|
253
|
+
" (Space)"
|
|
254
|
+
] }) })
|
|
255
|
+
] }),
|
|
256
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
257
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onNext, children: /* @__PURE__ */ jsx(SkipForward, {}) }) }),
|
|
258
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Next (P)" }) })
|
|
259
|
+
] }),
|
|
260
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
261
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
262
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onMuteToggle, children: isMuted || volume === 0 ? /* @__PURE__ */ jsx(VolumeX, {}) : /* @__PURE__ */ jsx(Volume2, {}) }) }),
|
|
263
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Mute (M)" }) })
|
|
264
|
+
] }),
|
|
265
|
+
/* @__PURE__ */ jsx(Slider, { value: [isMuted ? 0 : volume], max: 1, step: 0.05, onValueChange: ([val]) => onVolumeChange(val), className: "w-24" })
|
|
266
|
+
] }),
|
|
267
|
+
/* @__PURE__ */ jsxs("span", { className: "font-mono text-sm", children: [
|
|
268
|
+
formattedProgress,
|
|
269
|
+
" / ",
|
|
270
|
+
formattedDuration
|
|
271
|
+
] })
|
|
272
|
+
] }),
|
|
273
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
274
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
275
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: () => onFrameStep("backward"), children: /* @__PURE__ */ jsx(Rewind, { size: 18 }) }) }),
|
|
276
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Frame Backward" }) })
|
|
277
|
+
] }),
|
|
278
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
279
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: () => onFrameStep("forward"), children: /* @__PURE__ */ jsx(FastForward, { size: 18 }) }) }),
|
|
280
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Frame Forward" }) })
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxs(Popover, { children: [
|
|
283
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "font-mono w-16", children: [
|
|
284
|
+
playbackRate,
|
|
285
|
+
"x"
|
|
286
|
+
] }) }),
|
|
287
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-40", children: /* @__PURE__ */ jsx(RadioGroup, { value: String(playbackRate), onValueChange: (val) => onPlaybackRateChange(Number(val)), children: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4].map((rate) => /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
|
|
288
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { value: String(rate), id: `rate-${rate}` }),
|
|
289
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: `rate-${rate}`, children: [
|
|
290
|
+
rate,
|
|
291
|
+
"x"
|
|
292
|
+
] })
|
|
293
|
+
] }, rate)) }) })
|
|
294
|
+
] }),
|
|
295
|
+
audioTracks.length > 0 && /* @__PURE__ */ jsxs(Popover, { children: [
|
|
296
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", children: [
|
|
297
|
+
/* @__PURE__ */ jsx(AudioLines, {}),
|
|
298
|
+
tracksLoading && /* @__PURE__ */ jsx(Loader2, { className: "absolute top-0 right-0 h-2.5 w-2.5 animate-spin text-primary" })
|
|
299
|
+
] }) }),
|
|
300
|
+
/* @__PURE__ */ jsx(PopoverContent, { children: /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto overscroll-contain pr-1", children: /* @__PURE__ */ jsx(RadioGroup, { value: activeAudioTrack, onValueChange: onAudioTrackChange, children: audioTracks.map((track) => /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
|
|
301
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { value: track.id, id: `audio-${track.id}` }),
|
|
302
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `audio-${track.id}`, children: track.name })
|
|
303
|
+
] }, track.id)) }) }) })
|
|
304
|
+
] }),
|
|
305
|
+
/* @__PURE__ */ jsxs(Popover, { children: [
|
|
306
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", children: [
|
|
307
|
+
/* @__PURE__ */ jsx(Subtitles, {}),
|
|
308
|
+
tracksLoading ? /* @__PURE__ */ jsx(Loader2, { className: "absolute top-0 right-0 h-2.5 w-2.5 animate-spin text-primary" }) : activeSubtitle !== "-1" && /* @__PURE__ */ jsx("span", { className: "absolute top-0 right-0 block h-2 w-2 rounded-full bg-primary ring-2 ring-background" })
|
|
309
|
+
] }) }),
|
|
310
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-64", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
311
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
312
|
+
/* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: "Subtitles" }),
|
|
313
|
+
/* @__PURE__ */ jsxs(
|
|
314
|
+
Button,
|
|
315
|
+
{
|
|
316
|
+
variant: "outline",
|
|
317
|
+
size: "sm",
|
|
318
|
+
onClick: onSubtitleUpload,
|
|
319
|
+
className: "h-7 px-2",
|
|
320
|
+
children: [
|
|
321
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3 mr-1" }),
|
|
322
|
+
"Add"
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
] }),
|
|
327
|
+
subtitles.length > 0 ? /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto overscroll-contain pr-1", children: /* @__PURE__ */ jsxs(RadioGroup, { value: activeSubtitle, onValueChange: onSubtitleChange, children: [
|
|
328
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
|
|
329
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { value: "-1", id: "sub-off" }),
|
|
330
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "sub-off", children: "Off" })
|
|
331
|
+
] }),
|
|
332
|
+
subtitles.map((sub) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between space-x-2", children: [
|
|
333
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 flex-1", children: [
|
|
334
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { value: sub.id, id: `sub-${sub.id}` }),
|
|
335
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `sub-${sub.id}`, className: "truncate", children: sub.name })
|
|
336
|
+
] }),
|
|
337
|
+
sub.type === "external" && onSubtitleRemove && /* @__PURE__ */ jsx(
|
|
338
|
+
Button,
|
|
339
|
+
{
|
|
340
|
+
variant: "ghost",
|
|
341
|
+
size: "sm",
|
|
342
|
+
onClick: () => onSubtitleRemove(sub.id),
|
|
343
|
+
className: "h-6 w-6 p-0",
|
|
344
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
] }, sub.id))
|
|
348
|
+
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground text-center py-2", children: "No subtitles available" })
|
|
349
|
+
] }) })
|
|
350
|
+
] }),
|
|
351
|
+
chapters.length > 0 && /* @__PURE__ */ jsxs(Popover, { open: chaptersMenuOpen, onOpenChange: setChaptersMenuOpen, children: [
|
|
352
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
353
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Chapters", children: /* @__PURE__ */ jsx(List, { className: "h-4 w-4" }) }) }) }),
|
|
354
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Chapters" }) })
|
|
355
|
+
] }),
|
|
356
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-72 p-0", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col max-h-64 overflow-y-auto", children: chapters.map((chapter) => /* @__PURE__ */ jsxs(
|
|
357
|
+
"button",
|
|
358
|
+
{
|
|
359
|
+
className: cn(
|
|
360
|
+
"flex items-center justify-between px-4 py-2 text-sm hover:bg-accent transition-colors text-left",
|
|
361
|
+
currentChapter?.index === chapter.index && "bg-accent font-medium"
|
|
362
|
+
),
|
|
363
|
+
onClick: () => {
|
|
364
|
+
onGoToChapter?.(chapter.index);
|
|
365
|
+
setChaptersMenuOpen(false);
|
|
366
|
+
},
|
|
367
|
+
children: [
|
|
368
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: chapter.title }),
|
|
369
|
+
/* @__PURE__ */ jsx("span", { className: "ml-4 font-mono text-xs text-muted-foreground shrink-0", children: formatTime(chapter.startTime) })
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
chapter.index
|
|
373
|
+
)) }) })
|
|
374
|
+
] }),
|
|
375
|
+
/* @__PURE__ */ jsxs(Popover, { children: [
|
|
376
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx(Settings2, {}) }) }),
|
|
377
|
+
/* @__PURE__ */ jsxs(PopoverContent, { className: "w-64 space-y-4", children: [
|
|
378
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
379
|
+
/* @__PURE__ */ jsxs(Label, { children: [
|
|
380
|
+
"Brightness: ",
|
|
381
|
+
filters.brightness,
|
|
382
|
+
"%"
|
|
383
|
+
] }),
|
|
384
|
+
/* @__PURE__ */ jsx(Slider, { value: [filters.brightness], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, brightness: val }) })
|
|
385
|
+
] }),
|
|
386
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
387
|
+
/* @__PURE__ */ jsxs(Label, { children: [
|
|
388
|
+
"Contrast: ",
|
|
389
|
+
filters.contrast,
|
|
390
|
+
"%"
|
|
391
|
+
] }),
|
|
392
|
+
/* @__PURE__ */ jsx(Slider, { value: [filters.contrast], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, contrast: val }) })
|
|
393
|
+
] }),
|
|
394
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
395
|
+
/* @__PURE__ */ jsxs(Label, { children: [
|
|
396
|
+
"Saturation: ",
|
|
397
|
+
filters.saturate,
|
|
398
|
+
"%"
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ jsx(Slider, { value: [filters.saturate], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, saturate: val }) })
|
|
401
|
+
] }),
|
|
402
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
403
|
+
/* @__PURE__ */ jsxs(Label, { children: [
|
|
404
|
+
"Hue: ",
|
|
405
|
+
filters.hue,
|
|
406
|
+
"\xB0"
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsx(Slider, { value: [filters.hue], max: 360, onValueChange: ([val]) => onFiltersChange({ ...filters, hue: val }) })
|
|
409
|
+
] }),
|
|
410
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
411
|
+
/* @__PURE__ */ jsxs(Label, { children: [
|
|
412
|
+
"Zoom: ",
|
|
413
|
+
Math.round(zoom * 100),
|
|
414
|
+
"%"
|
|
415
|
+
] }),
|
|
416
|
+
/* @__PURE__ */ jsx(Slider, { value: [zoom], min: 1, max: 3, step: 0.1, onValueChange: ([val]) => onZoomChange(val) })
|
|
417
|
+
] })
|
|
418
|
+
] })
|
|
419
|
+
] }),
|
|
420
|
+
onShowInfo && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
421
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onShowInfo, children: /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }) }) }),
|
|
422
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Video Information" }) })
|
|
423
|
+
] }),
|
|
424
|
+
onOpenShortcuts && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
425
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onOpenShortcuts, children: /* @__PURE__ */ jsx(Keyboard, { className: "h-4 w-4" }) }) }),
|
|
426
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Keyboard Shortcuts" }) })
|
|
427
|
+
] }),
|
|
428
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
429
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onScreenshot, children: /* @__PURE__ */ jsx(Camera, {}) }) }),
|
|
430
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Screenshot" }) })
|
|
431
|
+
] }),
|
|
432
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
433
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onLoopToggle, "data-active": loop, className: "data-[active=true]:text-primary", children: /* @__PURE__ */ jsx(RotateCcw, {}) }) }),
|
|
434
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Loop" }) })
|
|
435
|
+
] }),
|
|
436
|
+
pipSupported && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
437
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
438
|
+
Button,
|
|
439
|
+
{
|
|
440
|
+
variant: "ghost",
|
|
441
|
+
size: "icon",
|
|
442
|
+
onClick: onTogglePiP,
|
|
443
|
+
"aria-label": isPiP ? "Exit picture-in-picture" : "Enter picture-in-picture",
|
|
444
|
+
className: isPiP ? "text-primary" : "",
|
|
445
|
+
children: /* @__PURE__ */ jsx(PictureInPicture2, {})
|
|
446
|
+
}
|
|
447
|
+
) }),
|
|
448
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: isPiP ? "Exit picture-in-picture" : "Enter picture-in-picture" }) })
|
|
449
|
+
] }),
|
|
450
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
451
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onFullScreenToggle, children: isFullScreen ? /* @__PURE__ */ jsx(Minimize, {}) : /* @__PURE__ */ jsx(Maximize, {}) }) }),
|
|
452
|
+
/* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Fullscreen (F)" }) })
|
|
453
|
+
] })
|
|
454
|
+
] })
|
|
455
|
+
] })
|
|
456
|
+
] }) });
|
|
457
|
+
});
|
|
458
|
+
var player_controls_default = PlayerControls;
|
|
459
|
+
var ScrollArea = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
460
|
+
ScrollAreaPrimitive.Root,
|
|
461
|
+
{
|
|
462
|
+
ref,
|
|
463
|
+
className: cn("relative overflow-hidden", className),
|
|
464
|
+
...props,
|
|
465
|
+
children: [
|
|
466
|
+
/* @__PURE__ */ jsx(ScrollAreaPrimitive.Viewport, { className: "h-full w-full rounded-[inherit]", children }),
|
|
467
|
+
/* @__PURE__ */ jsx(ScrollBar, {}),
|
|
468
|
+
/* @__PURE__ */ jsx(ScrollAreaPrimitive.Corner, {})
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
));
|
|
472
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
473
|
+
var ScrollBar = React10.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx(
|
|
474
|
+
ScrollAreaPrimitive.ScrollAreaScrollbar,
|
|
475
|
+
{
|
|
476
|
+
ref,
|
|
477
|
+
orientation,
|
|
478
|
+
className: cn(
|
|
479
|
+
"flex touch-none select-none transition-colors",
|
|
480
|
+
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
481
|
+
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
482
|
+
className
|
|
483
|
+
),
|
|
484
|
+
...props,
|
|
485
|
+
children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
|
|
486
|
+
}
|
|
487
|
+
));
|
|
488
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
489
|
+
var Input = React10.forwardRef(
|
|
490
|
+
({ className, type, ...props }, ref) => {
|
|
491
|
+
return /* @__PURE__ */ jsx(
|
|
492
|
+
"input",
|
|
493
|
+
{
|
|
494
|
+
type,
|
|
495
|
+
className: cn(
|
|
496
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
497
|
+
className
|
|
498
|
+
),
|
|
499
|
+
ref,
|
|
500
|
+
...props
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
Input.displayName = "Input";
|
|
506
|
+
var Select = SelectPrimitive.Root;
|
|
507
|
+
var SelectValue = SelectPrimitive.Value;
|
|
508
|
+
var SelectTrigger = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
509
|
+
SelectPrimitive.Trigger,
|
|
510
|
+
{
|
|
511
|
+
ref,
|
|
512
|
+
className: cn(
|
|
513
|
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
514
|
+
className
|
|
515
|
+
),
|
|
516
|
+
...props,
|
|
517
|
+
children: [
|
|
518
|
+
children,
|
|
519
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
|
|
520
|
+
]
|
|
521
|
+
}
|
|
522
|
+
));
|
|
523
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
524
|
+
var SelectScrollUpButton = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
525
|
+
SelectPrimitive.ScrollUpButton,
|
|
526
|
+
{
|
|
527
|
+
ref,
|
|
528
|
+
className: cn(
|
|
529
|
+
"flex cursor-default items-center justify-center py-1",
|
|
530
|
+
className
|
|
531
|
+
),
|
|
532
|
+
...props,
|
|
533
|
+
children: /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" })
|
|
534
|
+
}
|
|
535
|
+
));
|
|
536
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
|
537
|
+
var SelectScrollDownButton = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
538
|
+
SelectPrimitive.ScrollDownButton,
|
|
539
|
+
{
|
|
540
|
+
ref,
|
|
541
|
+
className: cn(
|
|
542
|
+
"flex cursor-default items-center justify-center py-1",
|
|
543
|
+
className
|
|
544
|
+
),
|
|
545
|
+
...props,
|
|
546
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
|
|
547
|
+
}
|
|
548
|
+
));
|
|
549
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
|
550
|
+
var SelectContent = React10.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
|
|
551
|
+
SelectPrimitive.Content,
|
|
552
|
+
{
|
|
553
|
+
ref,
|
|
554
|
+
className: cn(
|
|
555
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
556
|
+
position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
557
|
+
className
|
|
558
|
+
),
|
|
559
|
+
position,
|
|
560
|
+
...props,
|
|
561
|
+
children: [
|
|
562
|
+
/* @__PURE__ */ jsx(SelectScrollUpButton, {}),
|
|
563
|
+
/* @__PURE__ */ jsx(
|
|
564
|
+
SelectPrimitive.Viewport,
|
|
565
|
+
{
|
|
566
|
+
className: cn(
|
|
567
|
+
"p-1",
|
|
568
|
+
position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
569
|
+
),
|
|
570
|
+
children
|
|
571
|
+
}
|
|
572
|
+
),
|
|
573
|
+
/* @__PURE__ */ jsx(SelectScrollDownButton, {})
|
|
574
|
+
]
|
|
575
|
+
}
|
|
576
|
+
) }));
|
|
577
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
|
578
|
+
var SelectLabel = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
579
|
+
SelectPrimitive.Label,
|
|
580
|
+
{
|
|
581
|
+
ref,
|
|
582
|
+
className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
|
|
583
|
+
...props
|
|
584
|
+
}
|
|
585
|
+
));
|
|
586
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
|
587
|
+
var SelectItem = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
588
|
+
SelectPrimitive.Item,
|
|
589
|
+
{
|
|
590
|
+
ref,
|
|
591
|
+
className: cn(
|
|
592
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
593
|
+
className
|
|
594
|
+
),
|
|
595
|
+
...props,
|
|
596
|
+
children: [
|
|
597
|
+
/* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }) }),
|
|
598
|
+
/* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
));
|
|
602
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
603
|
+
var SelectSeparator = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
604
|
+
SelectPrimitive.Separator,
|
|
605
|
+
{
|
|
606
|
+
ref,
|
|
607
|
+
className: cn("-mx-1 my-1 h-px bg-muted", className),
|
|
608
|
+
...props
|
|
609
|
+
}
|
|
610
|
+
));
|
|
611
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
|
612
|
+
var SIZE_WIDTHS = {
|
|
613
|
+
sm: "w-60",
|
|
614
|
+
md: "w-80",
|
|
615
|
+
lg: "w-96"
|
|
616
|
+
};
|
|
617
|
+
var NEXT_SIZE = {
|
|
618
|
+
sm: "md",
|
|
619
|
+
md: "lg",
|
|
620
|
+
lg: "sm"
|
|
621
|
+
};
|
|
622
|
+
function formatTime2(seconds) {
|
|
623
|
+
if (!seconds || !isFinite(seconds)) return "";
|
|
624
|
+
const h = Math.floor(seconds / 3600);
|
|
625
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
626
|
+
const s = Math.floor(seconds % 60);
|
|
627
|
+
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
628
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
629
|
+
}
|
|
630
|
+
var VIDEO_EXTENSIONS_RE = /\.(mp4|mkv|webm|mov|avi|wmv|flv|m4v)$/i;
|
|
631
|
+
function SortablePlaylistItem({ item, index, isActive, onSelect, onRemove }) {
|
|
632
|
+
const {
|
|
633
|
+
attributes,
|
|
634
|
+
listeners: listeners2,
|
|
635
|
+
setNodeRef,
|
|
636
|
+
transform,
|
|
637
|
+
transition,
|
|
638
|
+
isDragging
|
|
639
|
+
} = useSortable({ id: item.id });
|
|
640
|
+
const style = {
|
|
641
|
+
transform: CSS.Transform.toString(transform),
|
|
642
|
+
transition,
|
|
643
|
+
opacity: isDragging ? 0.5 : 1
|
|
644
|
+
};
|
|
645
|
+
const duration = formatTime2(item.duration ?? 0);
|
|
646
|
+
return /* @__PURE__ */ jsxs(
|
|
647
|
+
"div",
|
|
648
|
+
{
|
|
649
|
+
ref: setNodeRef,
|
|
650
|
+
style,
|
|
651
|
+
className: cn(
|
|
652
|
+
"flex items-center gap-1 p-1.5 rounded-md text-xs group",
|
|
653
|
+
"hover:bg-muted transition-colors",
|
|
654
|
+
isActive ? "bg-primary/20 text-primary-foreground" : ""
|
|
655
|
+
),
|
|
656
|
+
children: [
|
|
657
|
+
/* @__PURE__ */ jsx(
|
|
658
|
+
"button",
|
|
659
|
+
{
|
|
660
|
+
...attributes,
|
|
661
|
+
...listeners2,
|
|
662
|
+
className: "shrink-0 cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground p-0.5",
|
|
663
|
+
"aria-label": "Drag to reorder",
|
|
664
|
+
tabIndex: -1,
|
|
665
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "w-3 h-3" })
|
|
666
|
+
}
|
|
667
|
+
),
|
|
668
|
+
/* @__PURE__ */ jsxs(
|
|
669
|
+
"button",
|
|
670
|
+
{
|
|
671
|
+
onClick: () => onSelect(index),
|
|
672
|
+
className: "flex-1 flex items-center gap-1.5 min-w-0 text-left",
|
|
673
|
+
"aria-label": `Play ${item.name}`,
|
|
674
|
+
children: [
|
|
675
|
+
item.type === "video" ? /* @__PURE__ */ jsx(ListVideo, { className: "w-3.5 h-3.5 shrink-0" }) : /* @__PURE__ */ jsx(Tv, { className: "w-3.5 h-3.5 shrink-0" }),
|
|
676
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: item.name }),
|
|
677
|
+
duration && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground ml-auto shrink-0 pl-1", children: duration })
|
|
678
|
+
]
|
|
679
|
+
}
|
|
680
|
+
),
|
|
681
|
+
/* @__PURE__ */ jsx(
|
|
682
|
+
"button",
|
|
683
|
+
{
|
|
684
|
+
onClick: (e) => {
|
|
685
|
+
e.stopPropagation();
|
|
686
|
+
onRemove(index);
|
|
687
|
+
},
|
|
688
|
+
className: "shrink-0 opacity-0 group-hover:opacity-100 transition-opacity p-0.5",
|
|
689
|
+
"aria-label": "Remove from playlist",
|
|
690
|
+
children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3 text-muted-foreground hover:text-destructive" })
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
]
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
var PlaylistPanel = ({
|
|
698
|
+
playlist,
|
|
699
|
+
currentVideoIndex,
|
|
700
|
+
onSelectVideo,
|
|
701
|
+
onFilesAdded,
|
|
702
|
+
onFolderFilesAdded,
|
|
703
|
+
onAddStream,
|
|
704
|
+
onRemoveItem,
|
|
705
|
+
onReorder,
|
|
706
|
+
onImportM3U,
|
|
707
|
+
isOpen,
|
|
708
|
+
isPinned,
|
|
709
|
+
size,
|
|
710
|
+
onToggle,
|
|
711
|
+
onTogglePin,
|
|
712
|
+
onSizeChange
|
|
713
|
+
}) => {
|
|
714
|
+
const fileInputRef = useRef(null);
|
|
715
|
+
const folderInputRef = useRef(null);
|
|
716
|
+
const m3uInputRef = useRef(null);
|
|
717
|
+
const [streamUrl, setStreamUrl] = useState("");
|
|
718
|
+
const [sortKey, setSortKey] = useState("");
|
|
719
|
+
const handleStreamUrlSubmit = (e) => {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
if (streamUrl) {
|
|
722
|
+
onAddStream(streamUrl);
|
|
723
|
+
setStreamUrl("");
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const handleFolderSelect = (e) => {
|
|
727
|
+
const files = Array.from(e.target.files ?? []).filter((f) => VIDEO_EXTENSIONS_RE.test(f.name)).sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
|
|
728
|
+
if (files.length > 0) onFolderFilesAdded(files);
|
|
729
|
+
e.target.value = "";
|
|
730
|
+
};
|
|
731
|
+
const handleM3USelect = async (e) => {
|
|
732
|
+
const file = e.target.files?.[0];
|
|
733
|
+
if (!file) return;
|
|
734
|
+
const text = await file.text();
|
|
735
|
+
const items = parseM3U8(text);
|
|
736
|
+
if (items.length > 0) onImportM3U(items);
|
|
737
|
+
e.target.value = "";
|
|
738
|
+
};
|
|
739
|
+
const handleDragEnd = (event) => {
|
|
740
|
+
const { active, over } = event;
|
|
741
|
+
if (!over || active.id === over.id) return;
|
|
742
|
+
const oldIndex = playlist.findIndex((item) => item.id === active.id);
|
|
743
|
+
const newIndex = playlist.findIndex((item) => item.id === over.id);
|
|
744
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
745
|
+
onReorder(arrayMove(playlist, oldIndex, newIndex));
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const handleSort = (value) => {
|
|
749
|
+
setSortKey(value);
|
|
750
|
+
const sorted = [...playlist].sort((a, b) => {
|
|
751
|
+
switch (value) {
|
|
752
|
+
case "name-asc":
|
|
753
|
+
return a.name.localeCompare(b.name, void 0, { numeric: true });
|
|
754
|
+
case "name-desc":
|
|
755
|
+
return b.name.localeCompare(a.name, void 0, { numeric: true });
|
|
756
|
+
case "duration-asc":
|
|
757
|
+
return (a.duration ?? 0) - (b.duration ?? 0);
|
|
758
|
+
case "duration-desc":
|
|
759
|
+
return (b.duration ?? 0) - (a.duration ?? 0);
|
|
760
|
+
default:
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
onReorder(sorted);
|
|
765
|
+
};
|
|
766
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: !isOpen ? (
|
|
767
|
+
/* ── Collapsed drawer strip ── */
|
|
768
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-11 h-full bg-card border-l border-border shrink-0", children: [
|
|
769
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
770
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
771
|
+
Button,
|
|
772
|
+
{
|
|
773
|
+
variant: "ghost",
|
|
774
|
+
size: "icon",
|
|
775
|
+
className: "h-9 w-9 mt-2 shrink-0",
|
|
776
|
+
onClick: onToggle,
|
|
777
|
+
"aria-label": "Expand Playlist",
|
|
778
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
|
|
779
|
+
}
|
|
780
|
+
) }),
|
|
781
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "left", children: /* @__PURE__ */ jsx("p", { children: "Expand Playlist" }) })
|
|
782
|
+
] }),
|
|
783
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
784
|
+
"span",
|
|
785
|
+
{
|
|
786
|
+
className: "text-[10px] font-semibold text-muted-foreground tracking-widest uppercase select-none",
|
|
787
|
+
style: { writingMode: "vertical-rl", transform: "rotate(180deg)" },
|
|
788
|
+
children: "Playlist"
|
|
789
|
+
}
|
|
790
|
+
) }),
|
|
791
|
+
playlist.length > 0 && /* @__PURE__ */ jsx("span", { className: "mb-3 text-[10px] font-bold text-primary bg-primary/10 rounded-full w-6 h-6 flex items-center justify-center shrink-0", children: playlist.length > 99 ? "99+" : playlist.length })
|
|
792
|
+
] })
|
|
793
|
+
) : (
|
|
794
|
+
/* ── Full panel ── */
|
|
795
|
+
/* @__PURE__ */ jsxs("div", { className: cn("h-full flex flex-col bg-card border-l border-border shrink-0 transition-[width] duration-200", SIZE_WIDTHS[size]), children: [
|
|
796
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b border-border shrink-0", children: [
|
|
797
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
|
|
798
|
+
/* @__PURE__ */ jsx(ListVideo, { className: "h-4 w-4 text-primary shrink-0" }),
|
|
799
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-sm truncate", children: "Playlist" }),
|
|
800
|
+
playlist.length > 0 && /* @__PURE__ */ jsx("span", { className: "text-[10px] font-bold text-muted-foreground bg-muted rounded-full px-1.5 py-0.5 shrink-0 leading-none", children: playlist.length })
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [
|
|
803
|
+
playlist.length > 0 && /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
804
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
805
|
+
Button,
|
|
806
|
+
{
|
|
807
|
+
variant: "ghost",
|
|
808
|
+
size: "icon",
|
|
809
|
+
className: "h-7 w-7",
|
|
810
|
+
onClick: () => exportPlaylist(playlist),
|
|
811
|
+
"aria-label": "Export Playlist",
|
|
812
|
+
children: /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" })
|
|
813
|
+
}
|
|
814
|
+
) }),
|
|
815
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Export as M3U8" }) })
|
|
816
|
+
] }),
|
|
817
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
818
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
819
|
+
Button,
|
|
820
|
+
{
|
|
821
|
+
variant: "ghost",
|
|
822
|
+
size: "icon",
|
|
823
|
+
className: "h-7 w-7",
|
|
824
|
+
onClick: () => m3uInputRef.current?.click(),
|
|
825
|
+
"aria-label": "Import Playlist",
|
|
826
|
+
children: /* @__PURE__ */ jsx(Upload, { className: "h-3.5 w-3.5" })
|
|
827
|
+
}
|
|
828
|
+
) }),
|
|
829
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Import M3U/M3U8" }) })
|
|
830
|
+
] }),
|
|
831
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
832
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
833
|
+
Button,
|
|
834
|
+
{
|
|
835
|
+
variant: "ghost",
|
|
836
|
+
size: "icon",
|
|
837
|
+
className: cn("h-7 w-7", isPinned && "text-primary bg-primary/10"),
|
|
838
|
+
onClick: onTogglePin,
|
|
839
|
+
"aria-label": isPinned ? "Unpin Playlist" : "Pin Playlist",
|
|
840
|
+
children: isPinned ? /* @__PURE__ */ jsx(Pin, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(PinOff, { className: "h-3.5 w-3.5" })
|
|
841
|
+
}
|
|
842
|
+
) }),
|
|
843
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: isPinned ? "Unpin (allow auto-hide on play)" : "Pin (keep open while playing)" }) })
|
|
844
|
+
] }),
|
|
845
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
846
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
847
|
+
Button,
|
|
848
|
+
{
|
|
849
|
+
variant: "ghost",
|
|
850
|
+
size: "icon",
|
|
851
|
+
className: "h-7 w-7",
|
|
852
|
+
onClick: () => onSizeChange(NEXT_SIZE[size]),
|
|
853
|
+
"aria-label": "Resize Playlist",
|
|
854
|
+
children: size === "lg" ? /* @__PURE__ */ jsx(Minimize2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Maximize2, { className: "h-3.5 w-3.5" })
|
|
855
|
+
}
|
|
856
|
+
) }),
|
|
857
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: size === "lg" ? "Make smaller" : "Make larger" }) })
|
|
858
|
+
] }),
|
|
859
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
860
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
861
|
+
Button,
|
|
862
|
+
{
|
|
863
|
+
variant: "ghost",
|
|
864
|
+
size: "icon",
|
|
865
|
+
className: "h-7 w-7",
|
|
866
|
+
onClick: onToggle,
|
|
867
|
+
"aria-label": "Collapse Playlist",
|
|
868
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
|
|
869
|
+
}
|
|
870
|
+
) }),
|
|
871
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Collapse" }) })
|
|
872
|
+
] })
|
|
873
|
+
] })
|
|
874
|
+
] }),
|
|
875
|
+
/* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2 border-b border-border shrink-0", children: [
|
|
876
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
|
|
877
|
+
/* @__PURE__ */ jsxs(Button, { onClick: () => fileInputRef.current?.click(), className: "flex-1 h-8 text-xs", children: [
|
|
878
|
+
/* @__PURE__ */ jsx(FilePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
|
|
879
|
+
" Add Files"
|
|
880
|
+
] }),
|
|
881
|
+
/* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
882
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
883
|
+
Button,
|
|
884
|
+
{
|
|
885
|
+
variant: "outline",
|
|
886
|
+
size: "icon",
|
|
887
|
+
className: "h-8 w-8 shrink-0",
|
|
888
|
+
onClick: () => folderInputRef.current?.click(),
|
|
889
|
+
"aria-label": "Open Folder",
|
|
890
|
+
children: /* @__PURE__ */ jsx(FolderOpen, { className: "h-3.5 w-3.5" })
|
|
891
|
+
}
|
|
892
|
+
) }),
|
|
893
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Open Folder" }) })
|
|
894
|
+
] })
|
|
895
|
+
] }),
|
|
896
|
+
/* @__PURE__ */ jsx(
|
|
897
|
+
"input",
|
|
898
|
+
{
|
|
899
|
+
type: "file",
|
|
900
|
+
ref: fileInputRef,
|
|
901
|
+
className: "hidden",
|
|
902
|
+
multiple: true,
|
|
903
|
+
accept: "video/*,.mkv,.avi,.mov,.wmv,.flv,.webm,.vtt,.srt",
|
|
904
|
+
onChange: (e) => e.target.files && onFilesAdded(e.target.files)
|
|
905
|
+
}
|
|
906
|
+
),
|
|
907
|
+
/* @__PURE__ */ jsx(
|
|
908
|
+
"input",
|
|
909
|
+
{
|
|
910
|
+
type: "file",
|
|
911
|
+
ref: folderInputRef,
|
|
912
|
+
className: "hidden",
|
|
913
|
+
multiple: true,
|
|
914
|
+
accept: "video/*",
|
|
915
|
+
webkitdirectory: "",
|
|
916
|
+
onChange: handleFolderSelect
|
|
917
|
+
}
|
|
918
|
+
),
|
|
919
|
+
/* @__PURE__ */ jsx(
|
|
920
|
+
"input",
|
|
921
|
+
{
|
|
922
|
+
type: "file",
|
|
923
|
+
ref: m3uInputRef,
|
|
924
|
+
className: "hidden",
|
|
925
|
+
accept: ".m3u,.m3u8",
|
|
926
|
+
onChange: handleM3USelect
|
|
927
|
+
}
|
|
928
|
+
),
|
|
929
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: handleStreamUrlSubmit, className: "flex gap-1.5", children: [
|
|
930
|
+
/* @__PURE__ */ jsx(
|
|
931
|
+
Input,
|
|
932
|
+
{
|
|
933
|
+
type: "url",
|
|
934
|
+
placeholder: "Enter stream URL",
|
|
935
|
+
value: streamUrl,
|
|
936
|
+
onChange: (e) => setStreamUrl(e.target.value),
|
|
937
|
+
className: "h-8 text-xs"
|
|
938
|
+
}
|
|
939
|
+
),
|
|
940
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", size: "icon", variant: "secondary", className: "h-8 w-8 shrink-0", children: /* @__PURE__ */ jsx(Link, { className: "h-3.5 w-3.5" }) })
|
|
941
|
+
] }),
|
|
942
|
+
playlist.length > 1 && /* @__PURE__ */ jsxs(Select, { value: sortKey, onValueChange: handleSort, children: [
|
|
943
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-7 text-xs", "aria-label": "Sort playlist", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort by\u2026" }) }),
|
|
944
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
945
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "name-asc", children: "Name A\u2013Z" }),
|
|
946
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "name-desc", children: "Name Z\u2013A" }),
|
|
947
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "duration-asc", children: "Shortest first" }),
|
|
948
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "duration-desc", children: "Longest first" })
|
|
949
|
+
] })
|
|
950
|
+
] })
|
|
951
|
+
] }),
|
|
952
|
+
/* @__PURE__ */ jsx(ScrollArea, { className: "flex-1", children: /* @__PURE__ */ jsx("div", { className: "p-2 space-y-0.5", children: playlist.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center text-xs text-muted-foreground py-10 px-2", children: [
|
|
953
|
+
/* @__PURE__ */ jsx("p", { children: "Your playlist is empty." }),
|
|
954
|
+
/* @__PURE__ */ jsx("p", { children: "Add files or a stream URL to get started." })
|
|
955
|
+
] }) : /* @__PURE__ */ jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(
|
|
956
|
+
SortableContext,
|
|
957
|
+
{
|
|
958
|
+
items: playlist.map((i) => i.id),
|
|
959
|
+
strategy: verticalListSortingStrategy,
|
|
960
|
+
children: playlist.map((item, index) => /* @__PURE__ */ jsx(
|
|
961
|
+
SortablePlaylistItem,
|
|
962
|
+
{
|
|
963
|
+
item,
|
|
964
|
+
index,
|
|
965
|
+
isActive: index === currentVideoIndex,
|
|
966
|
+
onSelect: onSelectVideo,
|
|
967
|
+
onRemove: onRemoveItem
|
|
968
|
+
},
|
|
969
|
+
item.id
|
|
970
|
+
))
|
|
971
|
+
}
|
|
972
|
+
) }) }) })
|
|
973
|
+
] })
|
|
974
|
+
) });
|
|
975
|
+
};
|
|
976
|
+
var playlist_panel_default = PlaylistPanel;
|
|
977
|
+
function VideoOverlay({ isLoading, loadingMessage, processingProgress = 0, eta, throughputMBs, onCancel }) {
|
|
978
|
+
if (!isLoading && !loadingMessage) return null;
|
|
979
|
+
return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 bg-black bg-opacity-70 flex flex-col items-center justify-center text-white z-10", children: [
|
|
980
|
+
/* @__PURE__ */ jsx("div", { className: "w-16 h-16 border-4 border-t-transparent border-primary rounded-full animate-spin" }),
|
|
981
|
+
/* @__PURE__ */ jsx("p", { className: "mt-4 text-lg max-w-sm text-center", children: loadingMessage || "Processing video..." }),
|
|
982
|
+
processingProgress > 0 && processingProgress < 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
983
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 w-64 bg-gray-700 rounded-full h-2", children: /* @__PURE__ */ jsx(
|
|
984
|
+
"div",
|
|
985
|
+
{
|
|
986
|
+
className: "bg-primary h-2 rounded-full transition-all duration-300",
|
|
987
|
+
style: { width: `${Math.round(processingProgress * 100)}%` }
|
|
988
|
+
}
|
|
989
|
+
) }),
|
|
990
|
+
throughputMBs !== null && throughputMBs !== void 0 && /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-white/60", children: [
|
|
991
|
+
throughputMBs,
|
|
992
|
+
" MB/s",
|
|
993
|
+
eta !== null && eta !== void 0 && ` \xB7 ~${eta}s left`
|
|
994
|
+
] })
|
|
995
|
+
] }),
|
|
996
|
+
onCancel && /* @__PURE__ */ jsx(
|
|
997
|
+
"button",
|
|
998
|
+
{
|
|
999
|
+
onClick: onCancel,
|
|
1000
|
+
className: "mt-3 px-4 py-1.5 text-sm rounded border border-white/30 text-white/80 hover:bg-white/10 transition-colors",
|
|
1001
|
+
children: "Cancel"
|
|
1002
|
+
}
|
|
1003
|
+
)
|
|
1004
|
+
] });
|
|
1005
|
+
}
|
|
1006
|
+
function PlayerErrorDisplay({ error, onRetry, onSkip, onDismiss }) {
|
|
1007
|
+
return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-black/80 z-10", children: [
|
|
1008
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "w-12 h-12 text-red-500 mb-4" }),
|
|
1009
|
+
/* @__PURE__ */ jsx("h3", { className: "text-white text-lg font-semibold mb-2", children: "Playback Error" }),
|
|
1010
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm text-center max-w-xs mb-6", children: error.message }),
|
|
1011
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
1012
|
+
error.retryable && onRetry && /* @__PURE__ */ jsx(Button, { onClick: onRetry, variant: "outline", children: "Retry" }),
|
|
1013
|
+
onSkip && /* @__PURE__ */ jsx(Button, { onClick: onSkip, variant: "outline", children: "Skip to Next" }),
|
|
1014
|
+
onDismiss && /* @__PURE__ */ jsx(Button, { onClick: onDismiss, children: "Dismiss" })
|
|
1015
|
+
] })
|
|
1016
|
+
] });
|
|
1017
|
+
}
|
|
1018
|
+
function formatTime3(seconds) {
|
|
1019
|
+
if (isNaN(seconds) || seconds === 0) return "\u2014";
|
|
1020
|
+
const h = Math.floor(seconds / 3600);
|
|
1021
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1022
|
+
const s = Math.floor(seconds % 60);
|
|
1023
|
+
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
1024
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1025
|
+
}
|
|
1026
|
+
function formatSize(bytes) {
|
|
1027
|
+
if (!bytes) return "\u2014";
|
|
1028
|
+
if (bytes > 1e9) return `${(bytes / 1e9).toFixed(2)} GB`;
|
|
1029
|
+
if (bytes > 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
|
|
1030
|
+
return `${(bytes / 1e3).toFixed(0)} KB`;
|
|
1031
|
+
}
|
|
1032
|
+
function formatBitrate(bps) {
|
|
1033
|
+
if (!bps) return "\u2014";
|
|
1034
|
+
return bps > 1e6 ? `${(bps / 1e6).toFixed(1)} Mbps` : `${(bps / 1e3).toFixed(0)} Kbps`;
|
|
1035
|
+
}
|
|
1036
|
+
function VideoInfoPanel({ metadata, onClose }) {
|
|
1037
|
+
if (!metadata) return null;
|
|
1038
|
+
const rows = [
|
|
1039
|
+
["File", metadata.filename ? metadata.filename.split("/").pop() ?? metadata.filename : "\u2014"],
|
|
1040
|
+
["Size", formatSize(metadata.fileSize)],
|
|
1041
|
+
["Duration", formatTime3(metadata.duration)],
|
|
1042
|
+
["Container", metadata.container || "\u2014"],
|
|
1043
|
+
["Resolution", metadata.width && metadata.height ? `${metadata.width} \xD7 ${metadata.height}` : "\u2014"],
|
|
1044
|
+
["Frame Rate", metadata.frameRate ? `${metadata.frameRate} fps` : "\u2014"],
|
|
1045
|
+
["Video Codec", metadata.videoCodec ?? "\u2014"],
|
|
1046
|
+
["Video Bitrate", formatBitrate(metadata.videoBitrate)],
|
|
1047
|
+
...metadata.audioTracks.map(
|
|
1048
|
+
(t, i) => [
|
|
1049
|
+
`Audio ${i + 1}`,
|
|
1050
|
+
[
|
|
1051
|
+
t.codec ?? "?",
|
|
1052
|
+
t.channels ? `${t.channels}ch` : null,
|
|
1053
|
+
t.sampleRate ? `${(t.sampleRate / 1e3).toFixed(1)} kHz` : null
|
|
1054
|
+
].filter(Boolean).join(" \xB7 ")
|
|
1055
|
+
]
|
|
1056
|
+
)
|
|
1057
|
+
];
|
|
1058
|
+
return /* @__PURE__ */ jsxs("div", { className: "absolute top-4 right-4 z-40 bg-black/85 text-white rounded-lg p-4 text-xs w-72", children: [
|
|
1059
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between mb-3", children: [
|
|
1060
|
+
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: "Video Information" }),
|
|
1061
|
+
/* @__PURE__ */ jsx(
|
|
1062
|
+
"button",
|
|
1063
|
+
{
|
|
1064
|
+
onClick: onClose,
|
|
1065
|
+
className: "text-muted-foreground hover:text-white",
|
|
1066
|
+
"aria-label": "Close",
|
|
1067
|
+
children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
|
|
1068
|
+
}
|
|
1069
|
+
)
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__PURE__ */ jsx("table", { className: "w-full", children: /* @__PURE__ */ jsx("tbody", { children: rows.map(([label, value]) => /* @__PURE__ */ jsxs("tr", { className: "border-b border-white/10 last:border-0", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-3 text-muted-foreground whitespace-nowrap", children: label }),
|
|
1073
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 text-right font-mono break-all", children: value || "\u2014" })
|
|
1074
|
+
] }, label)) }) })
|
|
1075
|
+
] });
|
|
1076
|
+
}
|
|
1077
|
+
var Dialog = DialogPrimitive.Root;
|
|
1078
|
+
var DialogPortal = DialogPrimitive.Portal;
|
|
1079
|
+
var DialogOverlay = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1080
|
+
DialogPrimitive.Overlay,
|
|
1081
|
+
{
|
|
1082
|
+
ref,
|
|
1083
|
+
className: cn(
|
|
1084
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
1085
|
+
className
|
|
1086
|
+
),
|
|
1087
|
+
...props
|
|
1088
|
+
}
|
|
1089
|
+
));
|
|
1090
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
1091
|
+
var DialogContent = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
|
|
1092
|
+
/* @__PURE__ */ jsx(DialogOverlay, {}),
|
|
1093
|
+
/* @__PURE__ */ jsxs(
|
|
1094
|
+
DialogPrimitive.Content,
|
|
1095
|
+
{
|
|
1096
|
+
ref,
|
|
1097
|
+
className: cn(
|
|
1098
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
1099
|
+
className
|
|
1100
|
+
),
|
|
1101
|
+
...props,
|
|
1102
|
+
children: [
|
|
1103
|
+
children,
|
|
1104
|
+
/* @__PURE__ */ jsxs(DialogPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground", children: [
|
|
1105
|
+
/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }),
|
|
1106
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
|
|
1107
|
+
] })
|
|
1108
|
+
]
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
] }));
|
|
1112
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
1113
|
+
var DialogHeader = ({
|
|
1114
|
+
className,
|
|
1115
|
+
...props
|
|
1116
|
+
}) => /* @__PURE__ */ jsx(
|
|
1117
|
+
"div",
|
|
1118
|
+
{
|
|
1119
|
+
className: cn(
|
|
1120
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
1121
|
+
className
|
|
1122
|
+
),
|
|
1123
|
+
...props
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
DialogHeader.displayName = "DialogHeader";
|
|
1127
|
+
var DialogTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1128
|
+
DialogPrimitive.Title,
|
|
1129
|
+
{
|
|
1130
|
+
ref,
|
|
1131
|
+
className: cn(
|
|
1132
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
1133
|
+
className
|
|
1134
|
+
),
|
|
1135
|
+
...props
|
|
1136
|
+
}
|
|
1137
|
+
));
|
|
1138
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
1139
|
+
var DialogDescription = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
1140
|
+
DialogPrimitive.Description,
|
|
1141
|
+
{
|
|
1142
|
+
ref,
|
|
1143
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
1144
|
+
...props
|
|
1145
|
+
}
|
|
1146
|
+
));
|
|
1147
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
1148
|
+
var TOAST_LIMIT = 1;
|
|
1149
|
+
var TOAST_REMOVE_DELAY = 1e6;
|
|
1150
|
+
var count = 0;
|
|
1151
|
+
function genId() {
|
|
1152
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
1153
|
+
return count.toString();
|
|
1154
|
+
}
|
|
1155
|
+
var toastTimeouts = /* @__PURE__ */ new Map();
|
|
1156
|
+
var addToRemoveQueue = (toastId) => {
|
|
1157
|
+
if (toastTimeouts.has(toastId)) {
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const timeout = setTimeout(() => {
|
|
1161
|
+
toastTimeouts.delete(toastId);
|
|
1162
|
+
dispatch({
|
|
1163
|
+
type: "REMOVE_TOAST",
|
|
1164
|
+
toastId
|
|
1165
|
+
});
|
|
1166
|
+
}, TOAST_REMOVE_DELAY);
|
|
1167
|
+
toastTimeouts.set(toastId, timeout);
|
|
1168
|
+
};
|
|
1169
|
+
var reducer = (state, action) => {
|
|
1170
|
+
switch (action.type) {
|
|
1171
|
+
case "ADD_TOAST":
|
|
1172
|
+
return {
|
|
1173
|
+
...state,
|
|
1174
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
|
|
1175
|
+
};
|
|
1176
|
+
case "UPDATE_TOAST":
|
|
1177
|
+
return {
|
|
1178
|
+
...state,
|
|
1179
|
+
toasts: state.toasts.map(
|
|
1180
|
+
(t) => t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
1181
|
+
)
|
|
1182
|
+
};
|
|
1183
|
+
case "DISMISS_TOAST": {
|
|
1184
|
+
const { toastId } = action;
|
|
1185
|
+
if (toastId) {
|
|
1186
|
+
addToRemoveQueue(toastId);
|
|
1187
|
+
} else {
|
|
1188
|
+
state.toasts.forEach((toast2) => {
|
|
1189
|
+
addToRemoveQueue(toast2.id);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
...state,
|
|
1194
|
+
toasts: state.toasts.map(
|
|
1195
|
+
(t) => t.id === toastId || toastId === void 0 ? {
|
|
1196
|
+
...t,
|
|
1197
|
+
open: false
|
|
1198
|
+
} : t
|
|
1199
|
+
)
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
case "REMOVE_TOAST":
|
|
1203
|
+
if (action.toastId === void 0) {
|
|
1204
|
+
return {
|
|
1205
|
+
...state,
|
|
1206
|
+
toasts: []
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
...state,
|
|
1211
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId)
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
var listeners = [];
|
|
1216
|
+
var memoryState = { toasts: [] };
|
|
1217
|
+
function dispatch(action) {
|
|
1218
|
+
memoryState = reducer(memoryState, action);
|
|
1219
|
+
listeners.forEach((listener) => {
|
|
1220
|
+
listener(memoryState);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function toast({ ...props }) {
|
|
1224
|
+
const id = genId();
|
|
1225
|
+
const update = (props2) => dispatch({
|
|
1226
|
+
type: "UPDATE_TOAST",
|
|
1227
|
+
toast: { ...props2, id }
|
|
1228
|
+
});
|
|
1229
|
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
1230
|
+
dispatch({
|
|
1231
|
+
type: "ADD_TOAST",
|
|
1232
|
+
toast: {
|
|
1233
|
+
...props,
|
|
1234
|
+
id,
|
|
1235
|
+
open: true,
|
|
1236
|
+
onOpenChange: (open) => {
|
|
1237
|
+
if (!open) dismiss();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
id,
|
|
1243
|
+
dismiss,
|
|
1244
|
+
update
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
function useToast() {
|
|
1248
|
+
const [state, setState] = React10.useState(memoryState);
|
|
1249
|
+
React10.useEffect(() => {
|
|
1250
|
+
listeners.push(setState);
|
|
1251
|
+
return () => {
|
|
1252
|
+
const index = listeners.indexOf(setState);
|
|
1253
|
+
if (index > -1) {
|
|
1254
|
+
listeners.splice(index, 1);
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
}, [state]);
|
|
1258
|
+
return {
|
|
1259
|
+
...state,
|
|
1260
|
+
toast,
|
|
1261
|
+
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId })
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function ShortcutSettingsDialog({
|
|
1265
|
+
shortcuts,
|
|
1266
|
+
onSave,
|
|
1267
|
+
onClose
|
|
1268
|
+
}) {
|
|
1269
|
+
const [editing, setEditing] = useState(shortcuts);
|
|
1270
|
+
const [capturing, setCapturing] = useState(null);
|
|
1271
|
+
const { toast: toast2 } = useToast();
|
|
1272
|
+
useEffect(() => {
|
|
1273
|
+
if (!capturing) return;
|
|
1274
|
+
const handler = (e) => {
|
|
1275
|
+
e.preventDefault();
|
|
1276
|
+
if (e.key === "Escape") {
|
|
1277
|
+
setCapturing(null);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const conflict = editing.find(
|
|
1281
|
+
(b) => b.action !== capturing && matchesShortcut(e, {
|
|
1282
|
+
...b,
|
|
1283
|
+
key: e.key,
|
|
1284
|
+
modifiers: {
|
|
1285
|
+
ctrl: e.ctrlKey,
|
|
1286
|
+
shift: e.shiftKey,
|
|
1287
|
+
alt: e.altKey
|
|
1288
|
+
}
|
|
1289
|
+
})
|
|
1290
|
+
);
|
|
1291
|
+
if (conflict) {
|
|
1292
|
+
toast2({
|
|
1293
|
+
title: `Conflicts with "${conflict.label}"`,
|
|
1294
|
+
variant: "destructive"
|
|
1295
|
+
});
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
setEditing(
|
|
1299
|
+
(prev) => prev.map(
|
|
1300
|
+
(b) => b.action === capturing ? {
|
|
1301
|
+
...b,
|
|
1302
|
+
key: e.key,
|
|
1303
|
+
modifiers: {
|
|
1304
|
+
ctrl: e.ctrlKey,
|
|
1305
|
+
shift: e.shiftKey,
|
|
1306
|
+
alt: e.altKey
|
|
1307
|
+
}
|
|
1308
|
+
} : b
|
|
1309
|
+
)
|
|
1310
|
+
);
|
|
1311
|
+
setCapturing(null);
|
|
1312
|
+
};
|
|
1313
|
+
window.addEventListener("keydown", handler);
|
|
1314
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1315
|
+
}, [capturing, editing, toast2]);
|
|
1316
|
+
const handleSave = () => {
|
|
1317
|
+
saveShortcuts(editing);
|
|
1318
|
+
onSave(editing);
|
|
1319
|
+
onClose();
|
|
1320
|
+
};
|
|
1321
|
+
const handleReset = () => {
|
|
1322
|
+
setEditing(DEFAULT_SHORTCUTS);
|
|
1323
|
+
};
|
|
1324
|
+
return /* @__PURE__ */ jsx(Dialog, { open: true, onOpenChange: onClose, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-md", children: [
|
|
1325
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "Keyboard Shortcuts" }) }),
|
|
1326
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1 max-h-96 overflow-y-auto", children: editing.map((binding) => /* @__PURE__ */ jsxs(
|
|
1327
|
+
"div",
|
|
1328
|
+
{
|
|
1329
|
+
className: "flex items-center justify-between py-1 px-2 rounded hover:bg-muted",
|
|
1330
|
+
children: [
|
|
1331
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: binding.label }),
|
|
1332
|
+
/* @__PURE__ */ jsx(
|
|
1333
|
+
"button",
|
|
1334
|
+
{
|
|
1335
|
+
onClick: () => setCapturing(binding.action),
|
|
1336
|
+
className: `font-mono text-xs px-2 py-1 rounded border ${capturing === binding.action ? "border-primary animate-pulse" : "border-muted-foreground"}`,
|
|
1337
|
+
children: capturing === binding.action ? "Press key..." : formatShortcutKey(binding)
|
|
1338
|
+
}
|
|
1339
|
+
)
|
|
1340
|
+
]
|
|
1341
|
+
},
|
|
1342
|
+
binding.action
|
|
1343
|
+
)) }),
|
|
1344
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end mt-4", children: [
|
|
1345
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: handleReset, children: "Reset to Defaults" }),
|
|
1346
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleSave, children: "Save" })
|
|
1347
|
+
] })
|
|
1348
|
+
] }) });
|
|
1349
|
+
}
|
|
1350
|
+
function SubtitleOverlay({ videoRef, activeSubtitle }) {
|
|
1351
|
+
const [cueText, setCueText] = useState("");
|
|
1352
|
+
useEffect(() => {
|
|
1353
|
+
const video = videoRef.current;
|
|
1354
|
+
if (!video || activeSubtitle === "-1") {
|
|
1355
|
+
setCueText("");
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
let cueChangeCleanup = null;
|
|
1359
|
+
function subscribeToTrack() {
|
|
1360
|
+
const trackElements = Array.from(video.querySelectorAll("track"));
|
|
1361
|
+
const targetIdx = trackElements.findIndex(
|
|
1362
|
+
(el) => el.getAttribute("data-id") === activeSubtitle
|
|
1363
|
+
);
|
|
1364
|
+
if (targetIdx === -1) return false;
|
|
1365
|
+
const textTrack = trackElements[targetIdx].track;
|
|
1366
|
+
if (!textTrack) return false;
|
|
1367
|
+
const handleCueChange = () => {
|
|
1368
|
+
const activeCues = textTrack.activeCues;
|
|
1369
|
+
if (!activeCues || activeCues.length === 0) {
|
|
1370
|
+
setCueText("");
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const texts = [];
|
|
1374
|
+
for (let i = 0; i < activeCues.length; i++) {
|
|
1375
|
+
const cue = activeCues[i];
|
|
1376
|
+
const cleaned = cue.text.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
|
|
1377
|
+
texts.push(cleaned);
|
|
1378
|
+
}
|
|
1379
|
+
setCueText(texts.join("\n"));
|
|
1380
|
+
};
|
|
1381
|
+
function seedFromCurrentTime() {
|
|
1382
|
+
const allCues = textTrack.cues;
|
|
1383
|
+
if (!allCues) return;
|
|
1384
|
+
const currentTime = video.currentTime;
|
|
1385
|
+
const texts = [];
|
|
1386
|
+
for (let i = 0; i < allCues.length; i++) {
|
|
1387
|
+
const cue = allCues[i];
|
|
1388
|
+
if (cue.startTime <= currentTime && cue.endTime > currentTime) {
|
|
1389
|
+
texts.push(
|
|
1390
|
+
cue.text.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "")
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
setCueText(texts.join("\n"));
|
|
1395
|
+
}
|
|
1396
|
+
textTrack.addEventListener("cuechange", handleCueChange);
|
|
1397
|
+
const trackEl = trackElements[targetIdx];
|
|
1398
|
+
if (trackEl.readyState === 2) {
|
|
1399
|
+
seedFromCurrentTime();
|
|
1400
|
+
} else {
|
|
1401
|
+
const onLoad = () => {
|
|
1402
|
+
seedFromCurrentTime();
|
|
1403
|
+
trackEl.removeEventListener("load", onLoad);
|
|
1404
|
+
};
|
|
1405
|
+
trackEl.addEventListener("load", onLoad);
|
|
1406
|
+
}
|
|
1407
|
+
cueChangeCleanup = () => {
|
|
1408
|
+
textTrack.removeEventListener("cuechange", handleCueChange);
|
|
1409
|
+
setCueText("");
|
|
1410
|
+
};
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
if (subscribeToTrack()) {
|
|
1414
|
+
return () => cueChangeCleanup?.();
|
|
1415
|
+
}
|
|
1416
|
+
const observer = new MutationObserver(() => {
|
|
1417
|
+
if (subscribeToTrack()) {
|
|
1418
|
+
observer.disconnect();
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
observer.observe(video, { childList: true });
|
|
1422
|
+
return () => {
|
|
1423
|
+
observer.disconnect();
|
|
1424
|
+
cueChangeCleanup?.();
|
|
1425
|
+
};
|
|
1426
|
+
}, [videoRef, activeSubtitle]);
|
|
1427
|
+
if (!cueText) return null;
|
|
1428
|
+
return /* @__PURE__ */ jsx("div", { className: "absolute left-0 right-0 bottom-4 flex justify-center pointer-events-none z-10 px-8 translate-y-0 group-hover:-translate-y-24 transition-transform duration-300 ease-in-out", children: /* @__PURE__ */ jsx(
|
|
1429
|
+
"div",
|
|
1430
|
+
{
|
|
1431
|
+
className: "text-white text-center whitespace-pre-line",
|
|
1432
|
+
style: {
|
|
1433
|
+
fontSize: "2em",
|
|
1434
|
+
lineHeight: "normal",
|
|
1435
|
+
fontWeight: "bolder",
|
|
1436
|
+
fontFamily: "Netflix Sans, Helvetica Neue, Helvetica, Arial, sans-serif",
|
|
1437
|
+
textShadow: "#000000 0px 0px 7px"
|
|
1438
|
+
},
|
|
1439
|
+
children: cueText
|
|
1440
|
+
}
|
|
1441
|
+
) });
|
|
1442
|
+
}
|
|
1443
|
+
var MAX_RETRIES = 3;
|
|
1444
|
+
var LightBirdPlayer = () => {
|
|
1445
|
+
const videoRef = useRef(null);
|
|
1446
|
+
const containerRef = useRef(null);
|
|
1447
|
+
const canvasRef = useRef(null);
|
|
1448
|
+
const subtitleInputRef = useRef(null);
|
|
1449
|
+
const playerRef = useRef(null);
|
|
1450
|
+
const subtitleFilesMapRef = useRef(/* @__PURE__ */ new Map());
|
|
1451
|
+
const retryCountRef = useRef(0);
|
|
1452
|
+
const retryTimerRef = useRef(null);
|
|
1453
|
+
const streamStallDetectorRef = useRef(null);
|
|
1454
|
+
const isStreamRef = useRef(false);
|
|
1455
|
+
const { toast: toast2 } = useToast();
|
|
1456
|
+
const playlist = usePlaylist();
|
|
1457
|
+
const playback = useVideoPlayback(videoRef);
|
|
1458
|
+
const filters = useVideoFilters(videoRef);
|
|
1459
|
+
const subtitles = useSubtitles({
|
|
1460
|
+
onError: (msg) => toast2({ title: msg, variant: "destructive" }),
|
|
1461
|
+
onSuccess: (msg) => toast2({ title: msg })
|
|
1462
|
+
});
|
|
1463
|
+
const fullscreen = useFullscreen(containerRef);
|
|
1464
|
+
const pip = usePictureInPicture(videoRef);
|
|
1465
|
+
const { metadata: videoMetadata } = useVideoInfo(videoRef, playlist.currentItem?.file ?? null);
|
|
1466
|
+
useProgressPersistence(videoRef, playlist.currentItem?.name ?? null);
|
|
1467
|
+
const { chapters, currentChapter, goToChapter } = useChapters(videoRef, playerRef);
|
|
1468
|
+
const [shortcuts, setShortcuts] = useState(() => loadShortcuts());
|
|
1469
|
+
const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
|
|
1470
|
+
const [showShortcutsDialog, setShowShortcutsDialog] = useState(false);
|
|
1471
|
+
const [showInfo, setShowInfo] = useState(false);
|
|
1472
|
+
const progressEstimatorRef = useRef(null);
|
|
1473
|
+
const [audioTracks, setAudioTracks] = useState([]);
|
|
1474
|
+
const [activeAudioTrack, setActiveAudioTrack] = useState("0");
|
|
1475
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1476
|
+
const [loadingMessage, setLoadingMessage] = useState("");
|
|
1477
|
+
const [processingProgress, setProcessingProgress] = useState(0);
|
|
1478
|
+
const [processingEta, setProcessingEta] = useState(null);
|
|
1479
|
+
const [processingThroughput, setProcessingThroughput] = useState(null);
|
|
1480
|
+
const [playerError, setPlayerError] = useState(null);
|
|
1481
|
+
const [cancellableProcessing, setCancellableProcessing] = useState(false);
|
|
1482
|
+
const [mediaThumbnail, setMediaThumbnail] = useState(null);
|
|
1483
|
+
const [tracksLoading, setTracksLoading] = useState(false);
|
|
1484
|
+
const shortcutHandlers = useMemo(() => ({
|
|
1485
|
+
"play-pause": () => playback.togglePlay(),
|
|
1486
|
+
"seek-forward-5": () => {
|
|
1487
|
+
const el = videoRef.current;
|
|
1488
|
+
if (el) playback.seek(el.currentTime + 5);
|
|
1489
|
+
},
|
|
1490
|
+
"seek-backward-5": () => {
|
|
1491
|
+
const el = videoRef.current;
|
|
1492
|
+
if (el) playback.seek(el.currentTime - 5);
|
|
1493
|
+
},
|
|
1494
|
+
"seek-forward-30": () => {
|
|
1495
|
+
const el = videoRef.current;
|
|
1496
|
+
if (el) playback.seek(el.currentTime + 30);
|
|
1497
|
+
},
|
|
1498
|
+
"seek-backward-30": () => {
|
|
1499
|
+
const el = videoRef.current;
|
|
1500
|
+
if (el) playback.seek(el.currentTime - 30);
|
|
1501
|
+
},
|
|
1502
|
+
"volume-up": () => {
|
|
1503
|
+
const el = videoRef.current;
|
|
1504
|
+
if (el) playback.setVolume(Math.min(1, el.volume + 0.05));
|
|
1505
|
+
},
|
|
1506
|
+
"volume-down": () => {
|
|
1507
|
+
const el = videoRef.current;
|
|
1508
|
+
if (el) playback.setVolume(Math.max(0, el.volume - 0.05));
|
|
1509
|
+
},
|
|
1510
|
+
"mute": () => playback.toggleMute(),
|
|
1511
|
+
"fullscreen": () => fullscreen.toggle(),
|
|
1512
|
+
"next-item": () => handleNext(),
|
|
1513
|
+
"prev-item": () => handlePrevious(),
|
|
1514
|
+
"screenshot": () => captureScreenshot(),
|
|
1515
|
+
"show-shortcuts": () => setShowShortcutsHelp((v) => !v),
|
|
1516
|
+
"next-chapter": () => {
|
|
1517
|
+
const el = videoRef.current;
|
|
1518
|
+
if (!el || chapters.length === 0) return;
|
|
1519
|
+
const next = chapters.find((c) => c.startTime > el.currentTime);
|
|
1520
|
+
if (next) el.currentTime = next.startTime;
|
|
1521
|
+
},
|
|
1522
|
+
"prev-chapter": () => {
|
|
1523
|
+
const el = videoRef.current;
|
|
1524
|
+
if (!el || chapters.length === 0) return;
|
|
1525
|
+
const cur = currentChapter;
|
|
1526
|
+
if (!cur) return;
|
|
1527
|
+
if (el.currentTime > cur.startTime + 3) {
|
|
1528
|
+
el.currentTime = cur.startTime;
|
|
1529
|
+
} else {
|
|
1530
|
+
const prev = chapters[cur.index - 1];
|
|
1531
|
+
if (prev) el.currentTime = prev.startTime;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1535
|
+
}), [playback.togglePlay, playback.seek, playback.setVolume, playback.toggleMute, fullscreen.toggle, chapters, currentChapter]);
|
|
1536
|
+
useKeyboardShortcuts(shortcuts, shortcutHandlers);
|
|
1537
|
+
const stopStallDetection = () => {
|
|
1538
|
+
if (streamStallDetectorRef.current) {
|
|
1539
|
+
clearInterval(streamStallDetectorRef.current);
|
|
1540
|
+
streamStallDetectorRef.current = null;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
const startStallDetection = () => {
|
|
1544
|
+
stopStallDetection();
|
|
1545
|
+
let lastTime = -1;
|
|
1546
|
+
streamStallDetectorRef.current = setInterval(() => {
|
|
1547
|
+
const el = videoRef.current;
|
|
1548
|
+
if (!el) return;
|
|
1549
|
+
const current = el.currentTime;
|
|
1550
|
+
if (!el.paused && current === lastTime) {
|
|
1551
|
+
const resumeAt = current;
|
|
1552
|
+
el.load();
|
|
1553
|
+
el.addEventListener(
|
|
1554
|
+
"canplay",
|
|
1555
|
+
() => {
|
|
1556
|
+
el.currentTime = resumeAt;
|
|
1557
|
+
el.play().catch(() => {
|
|
1558
|
+
});
|
|
1559
|
+
},
|
|
1560
|
+
{ once: true }
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
lastTime = current;
|
|
1564
|
+
}, 5e3);
|
|
1565
|
+
};
|
|
1566
|
+
const clearRetryTimer = () => {
|
|
1567
|
+
if (retryTimerRef.current) {
|
|
1568
|
+
clearTimeout(retryTimerRef.current);
|
|
1569
|
+
retryTimerRef.current = null;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
const [playlistOpen, setPlaylistOpen] = useState(true);
|
|
1573
|
+
const [playlistPinned, setPlaylistPinned] = useState(false);
|
|
1574
|
+
const [playlistSize, setPlaylistSize] = useState("md");
|
|
1575
|
+
const wasAutoHiddenRef = useRef(false);
|
|
1576
|
+
const processFile = useCallback(async (file, subtitleFiles = []) => {
|
|
1577
|
+
setIsLoading(true);
|
|
1578
|
+
setLoadingMessage("Initializing player...");
|
|
1579
|
+
setProcessingProgress(0);
|
|
1580
|
+
setPlayerError(null);
|
|
1581
|
+
setTracksLoading(false);
|
|
1582
|
+
progressEstimatorRef.current = new ProgressEstimator(file.size);
|
|
1583
|
+
setProcessingEta(null);
|
|
1584
|
+
setProcessingThroughput(null);
|
|
1585
|
+
retryCountRef.current = 0;
|
|
1586
|
+
isStreamRef.current = false;
|
|
1587
|
+
stopStallDetection();
|
|
1588
|
+
try {
|
|
1589
|
+
playerRef.current?.destroy();
|
|
1590
|
+
subtitles.reset();
|
|
1591
|
+
const player = createVideoPlayer(file, subtitleFiles, (progress) => {
|
|
1592
|
+
progressEstimatorRef.current?.update(progress);
|
|
1593
|
+
const est = progressEstimatorRef.current?.getEstimate();
|
|
1594
|
+
setProcessingProgress(progress);
|
|
1595
|
+
if (progress < 1) {
|
|
1596
|
+
setLoadingMessage(`Processing video\u2026 ${Math.round(progress * 100)}%`);
|
|
1597
|
+
setProcessingEta(est?.etaSeconds ?? null);
|
|
1598
|
+
setProcessingThroughput(est && est.speedMBps > 0 ? est.speedMBps : null);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
playerRef.current = player;
|
|
1602
|
+
setCancellableProcessing(true);
|
|
1603
|
+
try {
|
|
1604
|
+
if (!videoRef.current) throw new Error("Video element not available");
|
|
1605
|
+
setLoadingMessage("Loading video...");
|
|
1606
|
+
await player.initialize(videoRef.current);
|
|
1607
|
+
} finally {
|
|
1608
|
+
setCancellableProcessing(false);
|
|
1609
|
+
}
|
|
1610
|
+
subtitles.initManager(videoRef.current);
|
|
1611
|
+
subtitles.importSubtitles(player.getSubtitles());
|
|
1612
|
+
const newAudioTracks = player.getAudioTracks();
|
|
1613
|
+
setAudioTracks(newAudioTracks);
|
|
1614
|
+
setActiveAudioTrack(newAudioTracks[0]?.id || "0");
|
|
1615
|
+
setIsLoading(false);
|
|
1616
|
+
setLoadingMessage("");
|
|
1617
|
+
setProcessingProgress(0);
|
|
1618
|
+
if (player.tracksReady) {
|
|
1619
|
+
setTracksLoading(true);
|
|
1620
|
+
player.tracksReady.then(() => {
|
|
1621
|
+
if (playerRef.current !== player) {
|
|
1622
|
+
setTracksLoading(false);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
subtitles.importSubtitles(player.getSubtitles());
|
|
1626
|
+
const updatedTracks = player.getAudioTracks();
|
|
1627
|
+
setAudioTracks(updatedTracks);
|
|
1628
|
+
setActiveAudioTrack(updatedTracks[0]?.id || "0");
|
|
1629
|
+
setTracksLoading(false);
|
|
1630
|
+
}).catch(() => {
|
|
1631
|
+
setTracksLoading(false);
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
if (!(error instanceof CancellationError)) {
|
|
1636
|
+
console.error(error);
|
|
1637
|
+
toast2({
|
|
1638
|
+
title: "Failed to process video",
|
|
1639
|
+
description: "There was an error loading the video file. It might be an unsupported format.",
|
|
1640
|
+
variant: "destructive"
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
setIsLoading(false);
|
|
1644
|
+
setLoadingMessage("");
|
|
1645
|
+
setProcessingProgress(0);
|
|
1646
|
+
} finally {
|
|
1647
|
+
progressEstimatorRef.current = null;
|
|
1648
|
+
setProcessingEta(null);
|
|
1649
|
+
setProcessingThroughput(null);
|
|
1650
|
+
}
|
|
1651
|
+
}, [subtitles, toast2]);
|
|
1652
|
+
const handleCancelProcessing = useCallback(() => {
|
|
1653
|
+
playerRef.current?.cancel?.();
|
|
1654
|
+
playerRef.current = null;
|
|
1655
|
+
setCancellableProcessing(false);
|
|
1656
|
+
setIsLoading(false);
|
|
1657
|
+
setLoadingMessage("");
|
|
1658
|
+
setProcessingProgress(0);
|
|
1659
|
+
}, []);
|
|
1660
|
+
const loadVideo = useCallback((index) => {
|
|
1661
|
+
const item = playlist.playlist[index];
|
|
1662
|
+
if (!item) return;
|
|
1663
|
+
playlist.selectItem(index);
|
|
1664
|
+
setPlayerError(null);
|
|
1665
|
+
clearRetryTimer();
|
|
1666
|
+
retryCountRef.current = 0;
|
|
1667
|
+
if (item.type === "stream") {
|
|
1668
|
+
playerRef.current?.destroy();
|
|
1669
|
+
playerRef.current = null;
|
|
1670
|
+
if (videoRef.current) videoRef.current.src = item.url;
|
|
1671
|
+
subtitles.reset();
|
|
1672
|
+
setAudioTracks([]);
|
|
1673
|
+
setActiveAudioTrack("0");
|
|
1674
|
+
isStreamRef.current = true;
|
|
1675
|
+
startStallDetection();
|
|
1676
|
+
} else if (item.file) {
|
|
1677
|
+
isStreamRef.current = false;
|
|
1678
|
+
stopStallDetection();
|
|
1679
|
+
const subs = subtitleFilesMapRef.current.get(item.name) ?? [];
|
|
1680
|
+
processFile(item.file, subs);
|
|
1681
|
+
}
|
|
1682
|
+
}, [playlist.playlist, playlist.selectItem, subtitles, processFile]);
|
|
1683
|
+
const handleSkipToNext = useCallback(() => {
|
|
1684
|
+
setPlayerError(null);
|
|
1685
|
+
clearRetryTimer();
|
|
1686
|
+
if (playlist.currentIndex !== null && playlist.playlist.length > 1) {
|
|
1687
|
+
loadVideo((playlist.currentIndex + 1) % playlist.playlist.length);
|
|
1688
|
+
}
|
|
1689
|
+
}, [playlist.currentIndex, playlist.playlist.length, loadVideo]);
|
|
1690
|
+
const handleRetry = useCallback(() => {
|
|
1691
|
+
setPlayerError(null);
|
|
1692
|
+
clearRetryTimer();
|
|
1693
|
+
retryCountRef.current = 0;
|
|
1694
|
+
if (videoRef.current) {
|
|
1695
|
+
videoRef.current.load();
|
|
1696
|
+
}
|
|
1697
|
+
}, []);
|
|
1698
|
+
const handleDismissError = useCallback(() => {
|
|
1699
|
+
setPlayerError(null);
|
|
1700
|
+
clearRetryTimer();
|
|
1701
|
+
}, []);
|
|
1702
|
+
useEffect(() => {
|
|
1703
|
+
const el = videoRef.current;
|
|
1704
|
+
if (!el) return;
|
|
1705
|
+
const onError = () => {
|
|
1706
|
+
const parsed = parseMediaError(el.error ?? null);
|
|
1707
|
+
setPlayerError(parsed);
|
|
1708
|
+
if (parsed.retryable && retryCountRef.current < MAX_RETRIES) {
|
|
1709
|
+
const delay = Math.pow(2, retryCountRef.current) * 1e3;
|
|
1710
|
+
retryTimerRef.current = setTimeout(() => {
|
|
1711
|
+
retryCountRef.current += 1;
|
|
1712
|
+
el.load();
|
|
1713
|
+
}, delay);
|
|
1714
|
+
} else if (!parsed.recoverable) {
|
|
1715
|
+
toast2({ title: "Skipping unplayable file", description: parsed.message });
|
|
1716
|
+
if (playlist.currentIndex !== null && playlist.playlist.length > 1) {
|
|
1717
|
+
loadVideo((playlist.currentIndex + 1) % playlist.playlist.length);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
el.addEventListener("error", onError);
|
|
1722
|
+
return () => el.removeEventListener("error", onError);
|
|
1723
|
+
}, [playlist.currentIndex, playlist.playlist.length]);
|
|
1724
|
+
useEffect(() => {
|
|
1725
|
+
const el = videoRef.current;
|
|
1726
|
+
if (!el) return;
|
|
1727
|
+
const onEnded = () => {
|
|
1728
|
+
if (playback.loop) {
|
|
1729
|
+
el.currentTime = 0;
|
|
1730
|
+
el.play().catch(() => {
|
|
1731
|
+
});
|
|
1732
|
+
} else if (playlist.currentIndex !== null && playlist.playlist.length > 1) {
|
|
1733
|
+
loadVideo((playlist.currentIndex + 1) % playlist.playlist.length);
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
el.addEventListener("ended", onEnded);
|
|
1737
|
+
return () => el.removeEventListener("ended", onEnded);
|
|
1738
|
+
}, [playback.loop, playlist.currentIndex, playlist.playlist.length, loadVideo]);
|
|
1739
|
+
useEffect(() => {
|
|
1740
|
+
return () => {
|
|
1741
|
+
playerRef.current?.destroy();
|
|
1742
|
+
clearRetryTimer();
|
|
1743
|
+
stopStallDetection();
|
|
1744
|
+
};
|
|
1745
|
+
}, []);
|
|
1746
|
+
useEffect(() => {
|
|
1747
|
+
const el = videoRef.current;
|
|
1748
|
+
if (!el || !playlist.currentItem) {
|
|
1749
|
+
setMediaThumbnail(null);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
setMediaThumbnail(null);
|
|
1753
|
+
let cancelled = false;
|
|
1754
|
+
const onLoadedData = () => {
|
|
1755
|
+
captureVideoThumbnail(el).then((dataUrl) => {
|
|
1756
|
+
if (!cancelled) setMediaThumbnail(dataUrl);
|
|
1757
|
+
});
|
|
1758
|
+
};
|
|
1759
|
+
el.addEventListener("loadeddata", onLoadedData);
|
|
1760
|
+
return () => {
|
|
1761
|
+
cancelled = true;
|
|
1762
|
+
el.removeEventListener("loadeddata", onLoadedData);
|
|
1763
|
+
};
|
|
1764
|
+
}, [playlist.currentItem]);
|
|
1765
|
+
useEffect(() => {
|
|
1766
|
+
if (playback.isPlaying) {
|
|
1767
|
+
if (!playlistPinned) {
|
|
1768
|
+
setPlaylistOpen((current) => {
|
|
1769
|
+
if (current) wasAutoHiddenRef.current = true;
|
|
1770
|
+
return current ? false : current;
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
} else if (wasAutoHiddenRef.current) {
|
|
1774
|
+
setPlaylistOpen(true);
|
|
1775
|
+
wasAutoHiddenRef.current = false;
|
|
1776
|
+
}
|
|
1777
|
+
}, [playback.isPlaying, playlistPinned]);
|
|
1778
|
+
const handleFileChange = async (files) => {
|
|
1779
|
+
const { videoFiles, subtitleFiles } = playlist.parseFiles(files);
|
|
1780
|
+
if (videoFiles.length === 0) return;
|
|
1781
|
+
const validVideoFiles = [];
|
|
1782
|
+
for (const file of videoFiles) {
|
|
1783
|
+
const validation = validateFile(file);
|
|
1784
|
+
if (!validation.valid) {
|
|
1785
|
+
toast2({ title: "Cannot load file", description: validation.reason, variant: "destructive" });
|
|
1786
|
+
} else {
|
|
1787
|
+
validVideoFiles.push(file);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (validVideoFiles.length === 0) return;
|
|
1791
|
+
if (subtitleFiles.length > 0) {
|
|
1792
|
+
subtitleFilesMapRef.current.set(validVideoFiles[0].name, subtitleFiles);
|
|
1793
|
+
}
|
|
1794
|
+
const prevLen = playlist.playlist.length;
|
|
1795
|
+
await playlist.addFiles(validVideoFiles);
|
|
1796
|
+
if (playlist.currentIndex === null) {
|
|
1797
|
+
playlist.selectItem(prevLen);
|
|
1798
|
+
await processFile(validVideoFiles[0], subtitleFiles);
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
const handleFolderFilesAdded = useCallback(
|
|
1802
|
+
async (files) => {
|
|
1803
|
+
const prevLen = playlist.playlist.length;
|
|
1804
|
+
await playlist.addFiles(files);
|
|
1805
|
+
if (playlist.currentIndex === null && files.length > 0) {
|
|
1806
|
+
const newIndex = prevLen;
|
|
1807
|
+
playlist.selectItem(newIndex);
|
|
1808
|
+
await processFile(files[0]);
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
[playlist, processFile]
|
|
1812
|
+
);
|
|
1813
|
+
const handleRemoveItem = useCallback((index) => {
|
|
1814
|
+
playlist.removeItem(index);
|
|
1815
|
+
}, [playlist]);
|
|
1816
|
+
const handleReorder = useCallback(
|
|
1817
|
+
(newPlaylist) => {
|
|
1818
|
+
playlist.reorderItems(newPlaylist);
|
|
1819
|
+
},
|
|
1820
|
+
[playlist]
|
|
1821
|
+
);
|
|
1822
|
+
const handleImportM3U = useCallback(
|
|
1823
|
+
(items) => {
|
|
1824
|
+
items.forEach((item) => {
|
|
1825
|
+
playlist.appendItem({ ...item, id: crypto.randomUUID() });
|
|
1826
|
+
});
|
|
1827
|
+
if (playlist.currentIndex === null && items.length > 0) {
|
|
1828
|
+
const firstStream = items.find((i) => i.type === "stream");
|
|
1829
|
+
if (firstStream && videoRef.current) {
|
|
1830
|
+
playlist.selectItem(0);
|
|
1831
|
+
videoRef.current.src = firstStream.url;
|
|
1832
|
+
subtitles.reset();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
},
|
|
1836
|
+
[playlist, subtitles]
|
|
1837
|
+
);
|
|
1838
|
+
const handleAddStream = useCallback((url, name) => {
|
|
1839
|
+
const newIndex = playlist.playlist.length;
|
|
1840
|
+
const newItem = {
|
|
1841
|
+
id: crypto.randomUUID(),
|
|
1842
|
+
name: name || `Stream ${newIndex + 1}`,
|
|
1843
|
+
url,
|
|
1844
|
+
type: "stream"
|
|
1845
|
+
};
|
|
1846
|
+
playlist.appendItem(newItem);
|
|
1847
|
+
if (playlist.currentIndex === null) {
|
|
1848
|
+
playlist.selectItem(newIndex);
|
|
1849
|
+
if (videoRef.current) videoRef.current.src = url;
|
|
1850
|
+
subtitles.reset();
|
|
1851
|
+
setAudioTracks([]);
|
|
1852
|
+
setActiveAudioTrack("0");
|
|
1853
|
+
isStreamRef.current = true;
|
|
1854
|
+
startStallDetection();
|
|
1855
|
+
}
|
|
1856
|
+
}, [playlist, subtitles]);
|
|
1857
|
+
const handleSubtitleChange = useCallback(async (id) => {
|
|
1858
|
+
subtitles.switchSubtitle(id);
|
|
1859
|
+
if (playerRef.current) {
|
|
1860
|
+
try {
|
|
1861
|
+
await playerRef.current.switchSubtitle(id);
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
console.error("Player subtitle switch failed:", error);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}, [subtitles]);
|
|
1867
|
+
const handleAudioTrackChange = useCallback(async (id) => {
|
|
1868
|
+
if (!playerRef.current) return;
|
|
1869
|
+
try {
|
|
1870
|
+
setIsLoading(true);
|
|
1871
|
+
setLoadingMessage("Switching audio track...");
|
|
1872
|
+
await playerRef.current.switchAudioTrack(id);
|
|
1873
|
+
setActiveAudioTrack(id);
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
console.error("Failed to switch audio track:", error);
|
|
1876
|
+
toast2({ title: "Failed to switch audio track", variant: "destructive" });
|
|
1877
|
+
} finally {
|
|
1878
|
+
setIsLoading(false);
|
|
1879
|
+
setLoadingMessage("");
|
|
1880
|
+
}
|
|
1881
|
+
}, [toast2]);
|
|
1882
|
+
const captureScreenshot = useCallback(() => {
|
|
1883
|
+
const video = videoRef.current;
|
|
1884
|
+
const canvas = canvasRef.current;
|
|
1885
|
+
if (!video || !canvas) return;
|
|
1886
|
+
canvas.width = video.videoWidth;
|
|
1887
|
+
canvas.height = video.videoHeight;
|
|
1888
|
+
const ctx = canvas.getContext("2d");
|
|
1889
|
+
if (!ctx) return;
|
|
1890
|
+
ctx.filter = video.style.filter;
|
|
1891
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
1892
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
1893
|
+
const a = document.createElement("a");
|
|
1894
|
+
a.href = dataUrl;
|
|
1895
|
+
a.download = `lightbird-screenshot-${(/* @__PURE__ */ new Date()).toISOString()}.png`;
|
|
1896
|
+
a.click();
|
|
1897
|
+
toast2({ title: "Screenshot Saved" });
|
|
1898
|
+
}, [toast2]);
|
|
1899
|
+
const handleNext = useCallback(() => {
|
|
1900
|
+
if (playlist.currentIndex !== null && playlist.playlist.length > 1) {
|
|
1901
|
+
loadVideo((playlist.currentIndex + 1) % playlist.playlist.length);
|
|
1902
|
+
}
|
|
1903
|
+
}, [playlist.currentIndex, playlist.playlist.length, loadVideo]);
|
|
1904
|
+
const handlePrevious = useCallback(() => {
|
|
1905
|
+
if (playlist.currentIndex !== null && playlist.playlist.length > 1) {
|
|
1906
|
+
loadVideo((playlist.currentIndex - 1 + playlist.playlist.length) % playlist.playlist.length);
|
|
1907
|
+
}
|
|
1908
|
+
}, [playlist.currentIndex, playlist.playlist.length, loadVideo]);
|
|
1909
|
+
const handleSubtitleUpload = useCallback(() => {
|
|
1910
|
+
subtitleInputRef.current?.click();
|
|
1911
|
+
}, []);
|
|
1912
|
+
const handleSelectVideo = useCallback((index) => {
|
|
1913
|
+
loadVideo(index);
|
|
1914
|
+
}, [loadVideo]);
|
|
1915
|
+
const handlePlaylistToggle = () => {
|
|
1916
|
+
wasAutoHiddenRef.current = false;
|
|
1917
|
+
setPlaylistOpen((v) => !v);
|
|
1918
|
+
};
|
|
1919
|
+
const handleMediaPlay = useCallback(() => {
|
|
1920
|
+
const el = videoRef.current;
|
|
1921
|
+
if (el) el.play().catch(() => {
|
|
1922
|
+
});
|
|
1923
|
+
}, []);
|
|
1924
|
+
const handleMediaPause = useCallback(() => {
|
|
1925
|
+
const el = videoRef.current;
|
|
1926
|
+
if (el) el.pause();
|
|
1927
|
+
}, []);
|
|
1928
|
+
const handleMediaSeekForward = useCallback(() => {
|
|
1929
|
+
const el = videoRef.current;
|
|
1930
|
+
if (el) playback.seek(el.currentTime + 10);
|
|
1931
|
+
}, [playback.seek]);
|
|
1932
|
+
const handleMediaSeekBackward = useCallback(() => {
|
|
1933
|
+
const el = videoRef.current;
|
|
1934
|
+
if (el) playback.seek(el.currentTime - 10);
|
|
1935
|
+
}, [playback.seek]);
|
|
1936
|
+
useMediaSession({
|
|
1937
|
+
title: playlist.currentItem?.name ?? null,
|
|
1938
|
+
artwork: mediaThumbnail,
|
|
1939
|
+
onPlay: handleMediaPlay,
|
|
1940
|
+
onPause: handleMediaPause,
|
|
1941
|
+
onNext: handleNext,
|
|
1942
|
+
onPrev: handlePrevious,
|
|
1943
|
+
onSeekForward: handleMediaSeekForward,
|
|
1944
|
+
onSeekBackward: handleMediaSeekBackward
|
|
1945
|
+
});
|
|
1946
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 w-full h-full", children: [
|
|
1947
|
+
/* @__PURE__ */ jsxs(
|
|
1948
|
+
"div",
|
|
1949
|
+
{
|
|
1950
|
+
ref: containerRef,
|
|
1951
|
+
className: "flex-1 flex flex-col items-center justify-center bg-black relative group",
|
|
1952
|
+
children: [
|
|
1953
|
+
/* @__PURE__ */ jsx(
|
|
1954
|
+
"video",
|
|
1955
|
+
{
|
|
1956
|
+
ref: videoRef,
|
|
1957
|
+
className: cn("w-full h-full object-contain transition-all duration-300", isLoading && "invisible"),
|
|
1958
|
+
loop: playback.loop,
|
|
1959
|
+
onClick: playback.togglePlay,
|
|
1960
|
+
crossOrigin: "anonymous"
|
|
1961
|
+
}
|
|
1962
|
+
),
|
|
1963
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "hidden" }),
|
|
1964
|
+
/* @__PURE__ */ jsx(SubtitleOverlay, { videoRef, activeSubtitle: subtitles.activeSubtitle }),
|
|
1965
|
+
/* @__PURE__ */ jsx(
|
|
1966
|
+
VideoOverlay,
|
|
1967
|
+
{
|
|
1968
|
+
isLoading,
|
|
1969
|
+
loadingMessage,
|
|
1970
|
+
processingProgress,
|
|
1971
|
+
eta: processingEta,
|
|
1972
|
+
throughputMBs: processingThroughput,
|
|
1973
|
+
onCancel: cancellableProcessing ? handleCancelProcessing : void 0
|
|
1974
|
+
}
|
|
1975
|
+
),
|
|
1976
|
+
playerError && /* @__PURE__ */ jsx(
|
|
1977
|
+
PlayerErrorDisplay,
|
|
1978
|
+
{
|
|
1979
|
+
error: playerError,
|
|
1980
|
+
onRetry: playerError.retryable ? handleRetry : void 0,
|
|
1981
|
+
onSkip: handleSkipToNext,
|
|
1982
|
+
onDismiss: handleDismissError
|
|
1983
|
+
}
|
|
1984
|
+
),
|
|
1985
|
+
showInfo && /* @__PURE__ */ jsx(
|
|
1986
|
+
VideoInfoPanel,
|
|
1987
|
+
{
|
|
1988
|
+
metadata: videoMetadata,
|
|
1989
|
+
onClose: () => setShowInfo(false)
|
|
1990
|
+
}
|
|
1991
|
+
),
|
|
1992
|
+
showShortcutsHelp && /* @__PURE__ */ jsx(
|
|
1993
|
+
"div",
|
|
1994
|
+
{
|
|
1995
|
+
className: "absolute inset-0 bg-black/90 z-50 flex items-center justify-center",
|
|
1996
|
+
onClick: () => setShowShortcutsHelp(false),
|
|
1997
|
+
children: /* @__PURE__ */ jsxs(
|
|
1998
|
+
"div",
|
|
1999
|
+
{
|
|
2000
|
+
className: "bg-card rounded-lg p-6 max-w-sm w-full",
|
|
2001
|
+
onClick: (e) => e.stopPropagation(),
|
|
2002
|
+
children: [
|
|
2003
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold mb-4", children: "Keyboard Shortcuts" }),
|
|
2004
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1 max-h-80 overflow-y-auto", children: shortcuts.map((b) => {
|
|
2005
|
+
const mods = [];
|
|
2006
|
+
if (b.modifiers?.ctrl) mods.push("Ctrl");
|
|
2007
|
+
if (b.modifiers?.shift) mods.push("Shift");
|
|
2008
|
+
if (b.modifiers?.alt) mods.push("Alt");
|
|
2009
|
+
const keyLabel = b.key === " " ? "Space" : b.key;
|
|
2010
|
+
const formatted = [...mods, keyLabel].join(" + ");
|
|
2011
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
|
|
2012
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: b.label }),
|
|
2013
|
+
/* @__PURE__ */ jsx("kbd", { className: "font-mono bg-muted px-1.5 rounded text-xs", children: formatted })
|
|
2014
|
+
] }, b.action);
|
|
2015
|
+
}) }),
|
|
2016
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-4", children: "Press ? or click outside to close" })
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
)
|
|
2020
|
+
}
|
|
2021
|
+
),
|
|
2022
|
+
playlist.currentItem && /* @__PURE__ */ jsx(
|
|
2023
|
+
player_controls_default,
|
|
2024
|
+
{
|
|
2025
|
+
isPlaying: playback.isPlaying,
|
|
2026
|
+
progress: playback.progress,
|
|
2027
|
+
duration: playback.duration,
|
|
2028
|
+
volume: playback.volume,
|
|
2029
|
+
isMuted: playback.isMuted,
|
|
2030
|
+
playbackRate: playback.playbackRate,
|
|
2031
|
+
loop: playback.loop,
|
|
2032
|
+
isFullScreen: fullscreen.isFullscreen,
|
|
2033
|
+
filters: filters.filters,
|
|
2034
|
+
zoom: filters.zoom,
|
|
2035
|
+
subtitles: subtitles.subtitles,
|
|
2036
|
+
activeSubtitle: subtitles.activeSubtitle,
|
|
2037
|
+
audioTracks,
|
|
2038
|
+
activeAudioTrack,
|
|
2039
|
+
tracksLoading,
|
|
2040
|
+
onPlayPause: playback.togglePlay,
|
|
2041
|
+
onSeek: playback.seek,
|
|
2042
|
+
onVolumeChange: playback.setVolume,
|
|
2043
|
+
onMuteToggle: playback.toggleMute,
|
|
2044
|
+
onPlaybackRateChange: playback.setPlaybackRate,
|
|
2045
|
+
onLoopToggle: playback.toggleLoop,
|
|
2046
|
+
onFullScreenToggle: fullscreen.toggle,
|
|
2047
|
+
onFrameStep: playback.frameStep,
|
|
2048
|
+
onScreenshot: captureScreenshot,
|
|
2049
|
+
onNext: handleNext,
|
|
2050
|
+
onPrevious: handlePrevious,
|
|
2051
|
+
onFiltersChange: filters.setFilters,
|
|
2052
|
+
onZoomChange: filters.setZoom,
|
|
2053
|
+
onSubtitleChange: handleSubtitleChange,
|
|
2054
|
+
onAudioTrackChange: handleAudioTrackChange,
|
|
2055
|
+
onSubtitleUpload: handleSubtitleUpload,
|
|
2056
|
+
onSubtitleRemove: subtitles.removeSubtitle,
|
|
2057
|
+
onShowInfo: () => setShowInfo((v) => !v),
|
|
2058
|
+
onOpenShortcuts: () => setShowShortcutsDialog(true),
|
|
2059
|
+
chapters,
|
|
2060
|
+
currentChapter,
|
|
2061
|
+
onGoToChapter: goToChapter,
|
|
2062
|
+
onTogglePiP: pip.toggle,
|
|
2063
|
+
isPiP: pip.isPiP,
|
|
2064
|
+
pipSupported: !!pip.isSupported
|
|
2065
|
+
}
|
|
2066
|
+
),
|
|
2067
|
+
showShortcutsDialog && /* @__PURE__ */ jsx(
|
|
2068
|
+
ShortcutSettingsDialog,
|
|
2069
|
+
{
|
|
2070
|
+
shortcuts,
|
|
2071
|
+
onSave: (updated) => setShortcuts(updated),
|
|
2072
|
+
onClose: () => setShowShortcutsDialog(false)
|
|
2073
|
+
}
|
|
2074
|
+
),
|
|
2075
|
+
/* @__PURE__ */ jsx(
|
|
2076
|
+
"input",
|
|
2077
|
+
{
|
|
2078
|
+
type: "file",
|
|
2079
|
+
ref: subtitleInputRef,
|
|
2080
|
+
className: "hidden",
|
|
2081
|
+
multiple: true,
|
|
2082
|
+
accept: ".vtt,.srt,.ass,.ssa",
|
|
2083
|
+
onChange: (e) => {
|
|
2084
|
+
if (e.target.files) subtitles.addSubtitleFiles(Array.from(e.target.files));
|
|
2085
|
+
e.target.value = "";
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
),
|
|
2089
|
+
!playlist.currentItem && !isLoading && !loadingMessage && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center text-muted-foreground", children: [
|
|
2090
|
+
/* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children: "LightBird Player" }),
|
|
2091
|
+
/* @__PURE__ */ jsx("p", { children: "Add a local file or stream to begin." })
|
|
2092
|
+
] }) })
|
|
2093
|
+
]
|
|
2094
|
+
}
|
|
2095
|
+
),
|
|
2096
|
+
/* @__PURE__ */ jsx(
|
|
2097
|
+
playlist_panel_default,
|
|
2098
|
+
{
|
|
2099
|
+
playlist: playlist.playlist,
|
|
2100
|
+
currentVideoIndex: playlist.currentIndex,
|
|
2101
|
+
onSelectVideo: handleSelectVideo,
|
|
2102
|
+
onFilesAdded: handleFileChange,
|
|
2103
|
+
onFolderFilesAdded: handleFolderFilesAdded,
|
|
2104
|
+
onAddStream: handleAddStream,
|
|
2105
|
+
onRemoveItem: handleRemoveItem,
|
|
2106
|
+
onReorder: handleReorder,
|
|
2107
|
+
onImportM3U: handleImportM3U,
|
|
2108
|
+
isOpen: playlistOpen,
|
|
2109
|
+
isPinned: playlistPinned,
|
|
2110
|
+
size: playlistSize,
|
|
2111
|
+
onToggle: handlePlaylistToggle,
|
|
2112
|
+
onTogglePin: () => setPlaylistPinned((v) => !v),
|
|
2113
|
+
onSizeChange: setPlaylistSize
|
|
2114
|
+
}
|
|
2115
|
+
)
|
|
2116
|
+
] });
|
|
2117
|
+
};
|
|
2118
|
+
var lightbird_player_default = LightBirdPlayer;
|
|
2119
|
+
var PlayerErrorBoundary = class extends Component {
|
|
2120
|
+
constructor() {
|
|
2121
|
+
super(...arguments);
|
|
2122
|
+
this.state = { hasError: false };
|
|
2123
|
+
}
|
|
2124
|
+
static getDerivedStateFromError() {
|
|
2125
|
+
return { hasError: true };
|
|
2126
|
+
}
|
|
2127
|
+
componentDidCatch(error, info) {
|
|
2128
|
+
console.error("[LightBird] Render error:", error, info);
|
|
2129
|
+
}
|
|
2130
|
+
render() {
|
|
2131
|
+
if (this.state.hasError) {
|
|
2132
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-screen bg-black text-white", children: /* @__PURE__ */ jsxs("p", { children: [
|
|
2133
|
+
"Something went wrong with the player.",
|
|
2134
|
+
" ",
|
|
2135
|
+
/* @__PURE__ */ jsx(
|
|
2136
|
+
"button",
|
|
2137
|
+
{
|
|
2138
|
+
className: "underline hover:text-gray-300",
|
|
2139
|
+
onClick: () => this.setState({ hasError: false }),
|
|
2140
|
+
children: "Try again"
|
|
2141
|
+
}
|
|
2142
|
+
)
|
|
2143
|
+
] }) });
|
|
2144
|
+
}
|
|
2145
|
+
return this.props.children;
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
var ToastProvider = ToastPrimitives.Provider;
|
|
2149
|
+
var ToastViewport = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2150
|
+
ToastPrimitives.Viewport,
|
|
2151
|
+
{
|
|
2152
|
+
ref,
|
|
2153
|
+
className: cn(
|
|
2154
|
+
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
|
2155
|
+
className
|
|
2156
|
+
),
|
|
2157
|
+
...props
|
|
2158
|
+
}
|
|
2159
|
+
));
|
|
2160
|
+
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
|
2161
|
+
var toastVariants = cva(
|
|
2162
|
+
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
|
2163
|
+
{
|
|
2164
|
+
variants: {
|
|
2165
|
+
variant: {
|
|
2166
|
+
default: "border bg-background text-foreground",
|
|
2167
|
+
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground"
|
|
2168
|
+
}
|
|
2169
|
+
},
|
|
2170
|
+
defaultVariants: {
|
|
2171
|
+
variant: "default"
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
);
|
|
2175
|
+
var Toast = React10.forwardRef(({ className, variant, ...props }, ref) => {
|
|
2176
|
+
return /* @__PURE__ */ jsx(
|
|
2177
|
+
ToastPrimitives.Root,
|
|
2178
|
+
{
|
|
2179
|
+
ref,
|
|
2180
|
+
className: cn(toastVariants({ variant }), className),
|
|
2181
|
+
...props
|
|
2182
|
+
}
|
|
2183
|
+
);
|
|
2184
|
+
});
|
|
2185
|
+
Toast.displayName = ToastPrimitives.Root.displayName;
|
|
2186
|
+
var ToastAction = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2187
|
+
ToastPrimitives.Action,
|
|
2188
|
+
{
|
|
2189
|
+
ref,
|
|
2190
|
+
className: cn(
|
|
2191
|
+
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
|
2192
|
+
className
|
|
2193
|
+
),
|
|
2194
|
+
...props
|
|
2195
|
+
}
|
|
2196
|
+
));
|
|
2197
|
+
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
|
2198
|
+
var ToastClose = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2199
|
+
ToastPrimitives.Close,
|
|
2200
|
+
{
|
|
2201
|
+
ref,
|
|
2202
|
+
className: cn(
|
|
2203
|
+
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
|
2204
|
+
className
|
|
2205
|
+
),
|
|
2206
|
+
"toast-close": "",
|
|
2207
|
+
...props,
|
|
2208
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
2209
|
+
}
|
|
2210
|
+
));
|
|
2211
|
+
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
2212
|
+
var ToastTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2213
|
+
ToastPrimitives.Title,
|
|
2214
|
+
{
|
|
2215
|
+
ref,
|
|
2216
|
+
className: cn("text-sm font-semibold", className),
|
|
2217
|
+
...props
|
|
2218
|
+
}
|
|
2219
|
+
));
|
|
2220
|
+
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
|
2221
|
+
var ToastDescription = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
2222
|
+
ToastPrimitives.Description,
|
|
2223
|
+
{
|
|
2224
|
+
ref,
|
|
2225
|
+
className: cn("text-sm opacity-90", className),
|
|
2226
|
+
...props
|
|
2227
|
+
}
|
|
2228
|
+
));
|
|
2229
|
+
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
|
2230
|
+
function Toaster() {
|
|
2231
|
+
const { toasts } = useToast();
|
|
2232
|
+
return /* @__PURE__ */ jsxs(ToastProvider, { children: [
|
|
2233
|
+
toasts.map(function({ id, title, description, action, ...props }) {
|
|
2234
|
+
return /* @__PURE__ */ jsxs(Toast, { ...props, children: [
|
|
2235
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
2236
|
+
title && /* @__PURE__ */ jsx(ToastTitle, { children: title }),
|
|
2237
|
+
description && /* @__PURE__ */ jsx(ToastDescription, { children: description })
|
|
2238
|
+
] }),
|
|
2239
|
+
action,
|
|
2240
|
+
/* @__PURE__ */ jsx(ToastClose, {})
|
|
2241
|
+
] }, id);
|
|
2242
|
+
}),
|
|
2243
|
+
/* @__PURE__ */ jsx(ToastViewport, {})
|
|
2244
|
+
] });
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
export { lightbird_player_default as LightBirdPlayer, player_controls_default as PlayerControls, PlayerErrorBoundary, PlayerErrorDisplay, playlist_panel_default as PlaylistPanel, ShortcutSettingsDialog, SubtitleOverlay, Toaster, VideoInfoPanel, VideoOverlay };
|