@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/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 };