@lightbird/ui 0.6.0 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import * as React10 from 'react';
3
- import React10__default, { useMemo, useState, useRef, useEffect, useCallback, Component } from 'react';
2
+ import * as React11 from 'react';
3
+ import React11__default, { useRef, useState, useMemo, useEffect, useCallback, Component } from 'react';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import * as SliderPrimitive from '@radix-ui/react-slider';
@@ -10,7 +10,8 @@ import { cva } from 'class-variance-authority';
10
10
  import * as PopoverPrimitive from '@radix-ui/react-popover';
11
11
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
12
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, Link2, Globe, AlertCircle, GripVertical, Tv, Sun } from 'lucide-react';
13
+ import { useSmoothProgress, usePlaylist, useVideoPlayback, useVideoFilters, useSubtitles, useFullscreen, usePictureInPicture, useSeekPreview, useABLoop, useTouchGestures, useVideoInfo, useProgressPersistence, useChapters, useMagnet, useKeyboardShortcuts, useMediaSession } from '@lightbird/core/react';
14
+ import { Circle, SkipBack, Pause, Play, SkipForward, VolumeX, Volume2, AudioLines, Loader2, Subtitles, Plus, X, Settings2, PictureInPicture2, Camera, Info, Keyboard, List, Minimize, Maximize, ChevronDown, ChevronUp, Check, ChevronLeft, ListVideo, Download, Upload, Pin, PinOff, Minimize2, Maximize2, ChevronRight, FilePlus, FolderOpen, Link, Link2, Globe, AlertCircle, Film, GripVertical, Tv, FastForward, Rewind, Sun } from 'lucide-react';
14
15
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
15
16
  import { DndContext, closestCenter } from '@dnd-kit/core';
16
17
  import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
@@ -19,7 +20,6 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
19
20
  import * as SelectPrimitive from '@radix-ui/react-select';
20
21
  import { exportPlaylist, formatShortcutKey, FLAG_MAGNET_LINK, loadShortcuts, ProgressEstimator, createVideoPlayer, CancellationError, hasAcceptedDisclaimer, acceptDisclaimer, initFeatureFlags, parseMediaError, captureVideoThumbnail, validateFile, parseM3U8, matchesShortcut, saveShortcuts, DEFAULT_SHORTCUTS } from '@lightbird/core';
21
22
  import * as DialogPrimitive from '@radix-ui/react-dialog';
22
- import { usePlaylist, useVideoPlayback, useVideoFilters, useSubtitles, useFullscreen, usePictureInPicture, useSeekPreview, useABLoop, useTouchGestures, useVideoInfo, useProgressPersistence, useChapters, useMagnet, useKeyboardShortcuts, useMediaSession } from '@lightbird/core/react';
23
23
  import { useBooleanFlagValue, OpenFeatureProvider } from '@openfeature/react-sdk';
24
24
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
25
25
  import * as ToastPrimitives from '@radix-ui/react-toast';
@@ -27,7 +27,7 @@ import * as ToastPrimitives from '@radix-ui/react-toast';
27
27
  function cn(...inputs) {
28
28
  return twMerge(clsx(inputs));
29
29
  }
30
- var Slider = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(
30
+ var Slider = React11.forwardRef(({ className, trackClassName, rangeClassName, thumbClassName, ...props }, ref) => /* @__PURE__ */ jsxs(
31
31
  SliderPrimitive.Root,
32
32
  {
33
33
  ref,
@@ -37,8 +37,33 @@ var Slider = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ *
37
37
  ),
38
38
  ...props,
39
39
  children: [
40
- /* @__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" }) }),
41
- /* @__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
+ /* @__PURE__ */ jsx(
41
+ SliderPrimitive.Track,
42
+ {
43
+ className: cn(
44
+ "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary transition-all duration-150 ease-out",
45
+ trackClassName
46
+ ),
47
+ children: /* @__PURE__ */ jsx(
48
+ SliderPrimitive.Range,
49
+ {
50
+ className: cn(
51
+ "absolute h-full bg-primary transition-[box-shadow] duration-150",
52
+ rangeClassName
53
+ )
54
+ }
55
+ )
56
+ }
57
+ ),
58
+ /* @__PURE__ */ jsx(
59
+ SliderPrimitive.Thumb,
60
+ {
61
+ className: cn(
62
+ "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-[transform,box-shadow] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
63
+ thumbClassName
64
+ )
65
+ }
66
+ )
42
67
  ]
43
68
  }
44
69
  ));
@@ -68,7 +93,7 @@ var buttonVariants = cva(
68
93
  }
69
94
  }
70
95
  );
71
- var Button = React10.forwardRef(
96
+ var Button = React11.forwardRef(
72
97
  ({ className, variant, size, asChild = false, ...props }, ref) => {
73
98
  const Comp = asChild ? Slot : "button";
74
99
  return /* @__PURE__ */ jsx(
@@ -84,7 +109,7 @@ var Button = React10.forwardRef(
84
109
  Button.displayName = "Button";
85
110
  var Popover = PopoverPrimitive.Root;
86
111
  var PopoverTrigger = PopoverPrimitive.Trigger;
87
- var PopoverContent = React10.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
112
+ var PopoverContent = React11.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
88
113
  PopoverPrimitive.Content,
89
114
  {
90
115
  ref,
@@ -101,7 +126,7 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
101
126
  var TooltipProvider = TooltipPrimitive.Provider;
102
127
  var Tooltip = TooltipPrimitive.Root;
103
128
  var TooltipTrigger = TooltipPrimitive.Trigger;
104
- var TooltipContent = React10.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(
129
+ var TooltipContent = React11.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(
105
130
  TooltipPrimitive.Content,
106
131
  {
107
132
  ref,
@@ -117,7 +142,7 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
117
142
  var labelVariants = cva(
118
143
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
119
144
  );
120
- var Label = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
145
+ var Label = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
121
146
  LabelPrimitive.Root,
122
147
  {
123
148
  ref,
@@ -126,7 +151,164 @@ var Label = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
126
151
  }
127
152
  ));
128
153
  Label.displayName = LabelPrimitive.Root.displayName;
129
- var RadioGroup = React10.forwardRef(({ className, ...props }, ref) => {
154
+ var formatTime = (time) => {
155
+ if (isNaN(time)) return "00:00";
156
+ const date = /* @__PURE__ */ new Date(0);
157
+ date.setSeconds(time);
158
+ const timeString = date.toISOString().substr(11, 8);
159
+ return timeString.startsWith("00:") ? timeString.substr(3) : timeString;
160
+ };
161
+ var SeekBar = React11__default.memo(function SeekBar2({
162
+ progress,
163
+ duration,
164
+ isPlaying,
165
+ onSeek,
166
+ videoRef,
167
+ chapters = [],
168
+ abLoop = { pointA: null, pointB: null, isLooping: false },
169
+ onSeekHover,
170
+ seekPreviewThumbnail = null
171
+ }) {
172
+ const noopRef = useRef(null);
173
+ const smooth = useSmoothProgress(videoRef ?? noopRef, {
174
+ isPlaying: !!videoRef && isPlaying,
175
+ fallback: progress
176
+ });
177
+ const displayProgress = videoRef ? smooth : progress;
178
+ const [seekHover, setSeekHover] = useState(null);
179
+ const [isHovering, setIsHovering] = useState(false);
180
+ const [isScrubbing, setIsScrubbing] = useState(false);
181
+ const active = isHovering || isScrubbing;
182
+ const handleSeekHover = (e) => {
183
+ if (duration <= 0) return;
184
+ const rect = e.currentTarget.getBoundingClientRect();
185
+ if (rect.width <= 0) return;
186
+ const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
187
+ const time = ratio * duration;
188
+ setSeekHover({ ratio, time });
189
+ onSeekHover?.(time);
190
+ };
191
+ const handleSeekLeave = () => {
192
+ setSeekHover(null);
193
+ setIsHovering(false);
194
+ onSeekHover?.(null);
195
+ };
196
+ return /* @__PURE__ */ jsxs(
197
+ "div",
198
+ {
199
+ className: "relative w-full py-2",
200
+ "data-testid": "seek-bar",
201
+ "data-scrubbing": isScrubbing || void 0,
202
+ "data-hover": isHovering || void 0,
203
+ onMouseEnter: () => setIsHovering(true),
204
+ onMouseMove: handleSeekHover,
205
+ onMouseLeave: handleSeekLeave,
206
+ children: [
207
+ seekHover && /* @__PURE__ */ jsxs(
208
+ "div",
209
+ {
210
+ "data-testid": "seek-preview",
211
+ className: "absolute bottom-full mb-3 -translate-x-1/2 pointer-events-none flex flex-col items-center z-20",
212
+ style: { left: `${seekHover.ratio * 100}%` },
213
+ children: [
214
+ seekPreviewThumbnail ? /* @__PURE__ */ jsx(
215
+ "img",
216
+ {
217
+ src: seekPreviewThumbnail,
218
+ alt: "",
219
+ className: "w-40 h-[90px] rounded border border-white/20 bg-black object-cover shadow-lg"
220
+ }
221
+ ) : /* @__PURE__ */ jsx("div", { className: "w-40 h-[90px] rounded border border-white/20 bg-black/80 shadow-lg" }),
222
+ /* @__PURE__ */ jsx("span", { className: "mt-1 rounded bg-black/80 px-1.5 py-0.5 font-mono text-xs text-white", children: formatTime(seekHover.time) })
223
+ ]
224
+ }
225
+ ),
226
+ /* @__PURE__ */ jsx(
227
+ Slider,
228
+ {
229
+ value: [displayProgress],
230
+ max: duration || 0,
231
+ step: 0.05,
232
+ onValueChange: ([val]) => onSeek(val),
233
+ onPointerDown: () => setIsScrubbing(true),
234
+ onPointerUp: () => setIsScrubbing(false),
235
+ onLostPointerCapture: () => setIsScrubbing(false),
236
+ className: "w-full",
237
+ trackClassName: cn(
238
+ "rounded-none transition-all duration-150 ease-out",
239
+ active ? "h-1.5" : "h-[3px]"
240
+ ),
241
+ rangeClassName: cn(
242
+ "rounded-none",
243
+ active && "shadow-[0_0_6px_hsl(var(--primary)/0.6)]"
244
+ ),
245
+ thumbClassName: cn(
246
+ "h-3 w-3 border transition-[transform,opacity] duration-150",
247
+ active ? "scale-100 opacity-100" : "scale-0 opacity-0",
248
+ isScrubbing && "scale-125"
249
+ )
250
+ }
251
+ ),
252
+ chapters.length > 0 && duration > 0 && chapters.slice(1).map((chapter) => /* @__PURE__ */ jsxs(Tooltip, { children: [
253
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
254
+ "div",
255
+ {
256
+ "data-testid": "chapter-tick",
257
+ style: {
258
+ position: "absolute",
259
+ left: `${chapter.startTime / duration * 100}%`,
260
+ top: "50%",
261
+ width: "2px",
262
+ height: active ? "10px" : "6px",
263
+ background: "white",
264
+ opacity: 0.55,
265
+ pointerEvents: "none",
266
+ transform: "translate(-1px, -50%)",
267
+ transition: "height 150ms ease-out"
268
+ }
269
+ }
270
+ ) }),
271
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
272
+ chapter.title,
273
+ " \u2014 ",
274
+ formatTime(chapter.startTime)
275
+ ] }) })
276
+ ] }, chapter.index)),
277
+ duration > 0 && abLoop.pointA !== null && abLoop.pointB !== null && /* @__PURE__ */ jsx(
278
+ "div",
279
+ {
280
+ "data-testid": "ab-loop-region",
281
+ className: cn(
282
+ "pointer-events-none absolute top-1/2 -translate-y-1/2 bg-primary/40 transition-[height] duration-150",
283
+ active ? "h-1.5" : "h-[3px]"
284
+ ),
285
+ style: {
286
+ left: `${abLoop.pointA / duration * 100}%`,
287
+ width: `${(abLoop.pointB - abLoop.pointA) / duration * 100}%`
288
+ }
289
+ }
290
+ ),
291
+ duration > 0 && abLoop.pointA !== null && /* @__PURE__ */ jsx(
292
+ "div",
293
+ {
294
+ "data-testid": "ab-marker-a",
295
+ className: "pointer-events-none absolute top-1/2 -translate-y-1/2 h-4 w-0.5 -translate-x-1/2 bg-primary",
296
+ style: { left: `${abLoop.pointA / duration * 100}%` }
297
+ }
298
+ ),
299
+ duration > 0 && abLoop.pointB !== null && /* @__PURE__ */ jsx(
300
+ "div",
301
+ {
302
+ "data-testid": "ab-marker-b",
303
+ className: "pointer-events-none absolute top-1/2 -translate-y-1/2 h-4 w-0.5 -translate-x-1/2 bg-primary",
304
+ style: { left: `${abLoop.pointB / duration * 100}%` }
305
+ }
306
+ )
307
+ ]
308
+ }
309
+ );
310
+ });
311
+ var RadioGroup = React11.forwardRef(({ className, ...props }, ref) => {
130
312
  return /* @__PURE__ */ jsx(
131
313
  RadioGroupPrimitive.Root,
132
314
  {
@@ -137,7 +319,7 @@ var RadioGroup = React10.forwardRef(({ className, ...props }, ref) => {
137
319
  );
138
320
  });
139
321
  RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
140
- var RadioGroupItem = React10.forwardRef(({ className, ...props }, ref) => {
322
+ var RadioGroupItem = React11.forwardRef(({ className, ...props }, ref) => {
141
323
  return /* @__PURE__ */ jsx(
142
324
  RadioGroupPrimitive.Item,
143
325
  {
@@ -152,14 +334,14 @@ var RadioGroupItem = React10.forwardRef(({ className, ...props }, ref) => {
152
334
  );
153
335
  });
154
336
  RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
155
- var formatTime = (time) => {
337
+ var formatTime2 = (time) => {
156
338
  if (isNaN(time)) return "00:00";
157
339
  const date = /* @__PURE__ */ new Date(0);
158
340
  date.setSeconds(time);
159
341
  const timeString = date.toISOString().substr(11, 8);
160
342
  return timeString.startsWith("00:") ? timeString.substr(3) : timeString;
161
343
  };
162
- var PlayerControls = React10__default.memo(function PlayerControls2({
344
+ var PlayerControls = React11__default.memo(function PlayerControls2({
163
345
  isPlaying,
164
346
  progress,
165
347
  duration,
@@ -203,355 +385,260 @@ var PlayerControls = React10__default.memo(function PlayerControls2({
203
385
  onSeekHover,
204
386
  seekPreviewThumbnail = null,
205
387
  abLoop = { pointA: null, pointB: null, isLooping: false },
206
- onABLoopCycle
388
+ onABLoopCycle,
389
+ videoRef,
390
+ isDisabled = false
207
391
  }) {
208
- const formattedProgress = useMemo(() => formatTime(progress), [progress]);
209
- const formattedDuration = useMemo(() => formatTime(duration), [duration]);
392
+ const formattedProgress = useMemo(() => formatTime2(progress), [progress]);
393
+ const formattedDuration = useMemo(() => formatTime2(duration), [duration]);
210
394
  const [chaptersMenuOpen, setChaptersMenuOpen] = useState(false);
211
- const [seekHover, setSeekHover] = useState(null);
212
- const handleSeekHover = (e) => {
213
- if (duration <= 0) return;
214
- const rect = e.currentTarget.getBoundingClientRect();
215
- if (rect.width <= 0) return;
216
- const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
217
- const time = ratio * duration;
218
- setSeekHover({ ratio, time });
219
- onSeekHover?.(time);
220
- };
221
- const handleSeekLeave = () => {
222
- setSeekHover(null);
223
- onSeekHover?.(null);
224
- };
225
- 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: [
226
- /* @__PURE__ */ jsxs(
227
- "div",
228
- {
229
- className: "relative w-full",
230
- "data-testid": "seek-bar",
231
- onMouseMove: handleSeekHover,
232
- onMouseLeave: handleSeekLeave,
233
- children: [
234
- seekHover && /* @__PURE__ */ jsxs(
235
- "div",
236
- {
237
- "data-testid": "seek-preview",
238
- className: "absolute bottom-full mb-3 -translate-x-1/2 pointer-events-none flex flex-col items-center z-20",
239
- style: { left: `${seekHover.ratio * 100}%` },
240
- children: [
241
- seekPreviewThumbnail ? /* @__PURE__ */ jsx(
242
- "img",
243
- {
244
- src: seekPreviewThumbnail,
245
- alt: "",
246
- className: "w-40 h-[90px] rounded border border-white/20 bg-black object-cover shadow-lg"
247
- }
248
- ) : /* @__PURE__ */ jsx("div", { className: "w-40 h-[90px] rounded border border-white/20 bg-black/80 shadow-lg" }),
249
- /* @__PURE__ */ jsx("span", { className: "mt-1 rounded bg-black/80 px-1.5 py-0.5 font-mono text-xs text-white", children: formatTime(seekHover.time) })
250
- ]
251
- }
252
- ),
253
- /* @__PURE__ */ jsx(
254
- Slider,
255
- {
256
- value: [progress],
257
- max: duration,
258
- step: 1,
259
- onValueChange: ([val]) => onSeek(val),
260
- className: "w-full h-2"
261
- }
262
- ),
263
- chapters.length > 0 && duration > 0 && chapters.slice(1).map((chapter) => /* @__PURE__ */ jsxs(Tooltip, { children: [
264
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
265
- "div",
266
- {
267
- "data-testid": "chapter-tick",
268
- style: {
269
- position: "absolute",
270
- left: `${chapter.startTime / duration * 100}%`,
271
- top: 0,
272
- width: "2px",
273
- height: "100%",
274
- background: "white",
275
- opacity: 0.5,
276
- pointerEvents: "none",
277
- transform: "translateX(-1px)"
278
- }
279
- }
280
- ) }),
281
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
282
- chapter.title,
283
- " \u2014 ",
284
- formatTime(chapter.startTime)
285
- ] }) })
286
- ] }, chapter.index)),
287
- duration > 0 && abLoop.pointA !== null && abLoop.pointB !== null && /* @__PURE__ */ jsx(
288
- "div",
289
- {
290
- "data-testid": "ab-loop-region",
291
- className: "pointer-events-none absolute top-0 h-full bg-primary/30",
292
- style: {
293
- left: `${abLoop.pointA / duration * 100}%`,
294
- width: `${(abLoop.pointB - abLoop.pointA) / duration * 100}%`
295
- }
296
- }
297
- ),
298
- duration > 0 && abLoop.pointA !== null && /* @__PURE__ */ jsx(
299
- "div",
300
- {
301
- "data-testid": "ab-marker-a",
302
- className: "pointer-events-none absolute top-0 h-full w-0.5 -translate-x-1/2 bg-primary",
303
- style: { left: `${abLoop.pointA / duration * 100}%` }
304
- }
305
- ),
306
- duration > 0 && abLoop.pointB !== null && /* @__PURE__ */ jsx(
307
- "div",
308
- {
309
- "data-testid": "ab-marker-b",
310
- className: "pointer-events-none absolute top-0 h-full w-0.5 -translate-x-1/2 bg-primary",
311
- style: { left: `${abLoop.pointB / duration * 100}%` }
312
- }
313
- )
314
- ]
315
- }
316
- ),
317
- currentChapter && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: currentChapter.title }),
318
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-white", children: [
319
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
320
- /* @__PURE__ */ jsxs(Tooltip, { children: [
321
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPrevious, children: /* @__PURE__ */ jsx(SkipBack, {}) }) }),
322
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Previous (N)" }) })
323
- ] }),
324
- /* @__PURE__ */ jsxs(Tooltip, { children: [
325
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPlayPause, children: isPlaying ? /* @__PURE__ */ jsx(Pause, {}) : /* @__PURE__ */ jsx(Play, {}) }) }),
326
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
327
- isPlaying ? "Pause" : "Play",
328
- " (Space)"
329
- ] }) })
330
- ] }),
331
- /* @__PURE__ */ jsxs(Tooltip, { children: [
332
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onNext, children: /* @__PURE__ */ jsx(SkipForward, {}) }) }),
333
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Next (P)" }) })
334
- ] }),
335
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
336
- /* @__PURE__ */ jsxs(Tooltip, { children: [
337
- /* @__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, {}) }) }),
338
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Mute (M)" }) })
339
- ] }),
340
- /* @__PURE__ */ jsx(Slider, { value: [isMuted ? 0 : volume], max: 1, step: 0.05, onValueChange: ([val]) => onVolumeChange(val), className: "w-24" })
341
- ] }),
342
- /* @__PURE__ */ jsxs("span", { className: "font-mono text-sm", children: [
343
- formattedProgress,
344
- " / ",
345
- formattedDuration
346
- ] })
347
- ] }),
348
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
349
- /* @__PURE__ */ jsxs(Tooltip, { children: [
350
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: () => onFrameStep("backward"), children: /* @__PURE__ */ jsx(Rewind, { size: 18 }) }) }),
351
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Frame Backward" }) })
352
- ] }),
353
- /* @__PURE__ */ jsxs(Tooltip, { children: [
354
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: () => onFrameStep("forward"), children: /* @__PURE__ */ jsx(FastForward, { size: 18 }) }) }),
355
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Frame Forward" }) })
356
- ] }),
357
- /* @__PURE__ */ jsxs(Popover, { children: [
358
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "font-mono w-16", children: [
359
- playbackRate,
360
- "x"
361
- ] }) }),
362
- /* @__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: [
363
- /* @__PURE__ */ jsx(RadioGroupItem, { value: String(rate), id: `rate-${rate}` }),
364
- /* @__PURE__ */ jsxs(Label, { htmlFor: `rate-${rate}`, children: [
365
- rate,
366
- "x"
367
- ] })
368
- ] }, rate)) }) })
369
- ] }),
370
- audioTracks.length > 0 && /* @__PURE__ */ jsxs(Popover, { children: [
371
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", children: [
372
- /* @__PURE__ */ jsx(AudioLines, {}),
373
- tracksLoading && /* @__PURE__ */ jsx(Loader2, { className: "absolute top-0 right-0 h-2.5 w-2.5 animate-spin text-primary" })
374
- ] }) }),
375
- /* @__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: [
376
- /* @__PURE__ */ jsx(RadioGroupItem, { value: track.id, id: `audio-${track.id}` }),
377
- /* @__PURE__ */ jsx(Label, { htmlFor: `audio-${track.id}`, children: track.name })
378
- ] }, track.id)) }) }) })
379
- ] }),
380
- /* @__PURE__ */ jsxs(Popover, { children: [
381
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", children: [
382
- /* @__PURE__ */ jsx(Subtitles, {}),
383
- 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" })
384
- ] }) }),
385
- /* @__PURE__ */ jsx(PopoverContent, { className: "w-64", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
386
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
387
- /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: "Subtitles" }),
388
- /* @__PURE__ */ jsxs(
389
- Button,
395
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
396
+ "div",
397
+ {
398
+ "aria-disabled": isDisabled || void 0,
399
+ className: cn(
400
+ "absolute bottom-0 left-0 right-0 px-4 pb-3 pt-10 flex flex-col gap-1.5",
401
+ "bg-gradient-to-t from-black/80 via-black/40 to-transparent transition-opacity duration-300 ease-in-out",
402
+ isDisabled ? "opacity-60 pointer-events-none" : "opacity-0 group-hover:opacity-100"
403
+ ),
404
+ children: [
405
+ /* @__PURE__ */ jsx(
406
+ SeekBar,
407
+ {
408
+ progress,
409
+ duration,
410
+ isPlaying,
411
+ onSeek,
412
+ videoRef,
413
+ chapters,
414
+ abLoop,
415
+ onSeekHover,
416
+ seekPreviewThumbnail
417
+ }
418
+ ),
419
+ currentChapter && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground -mt-0.5", children: currentChapter.title }),
420
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-white", children: [
421
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
422
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
423
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPrevious, children: /* @__PURE__ */ jsx(SkipBack, {}) }) }),
424
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Previous (N)" }) })
425
+ ] }),
426
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
427
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onPlayPause, children: isPlaying ? /* @__PURE__ */ jsx(Pause, {}) : /* @__PURE__ */ jsx(Play, {}) }) }),
428
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("p", { children: [
429
+ isPlaying ? "Pause" : "Play",
430
+ " (Space)"
431
+ ] }) })
432
+ ] }),
433
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
434
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onNext, children: /* @__PURE__ */ jsx(SkipForward, {}) }) }),
435
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Next (P)" }) })
436
+ ] }),
437
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 ml-1", children: [
438
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
439
+ /* @__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, {}) }) }),
440
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Mute (M)" }) })
441
+ ] }),
442
+ /* @__PURE__ */ jsx(
443
+ Slider,
390
444
  {
391
- variant: "outline",
392
- size: "sm",
393
- onClick: onSubtitleUpload,
394
- className: "h-7 px-2",
395
- children: [
396
- /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3 mr-1" }),
397
- "Add"
398
- ]
445
+ value: [isMuted ? 0 : volume],
446
+ max: 1,
447
+ step: 0.05,
448
+ onValueChange: ([val]) => onVolumeChange(val),
449
+ "aria-label": "Volume",
450
+ className: "w-24",
451
+ trackClassName: "h-[3px]",
452
+ thumbClassName: "h-3 w-3 border"
399
453
  }
400
454
  )
401
455
  ] }),
402
- 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: [
403
- /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
404
- /* @__PURE__ */ jsx(RadioGroupItem, { value: "-1", id: "sub-off" }),
405
- /* @__PURE__ */ jsx(Label, { htmlFor: "sub-off", children: "Off" })
406
- ] }),
407
- subtitles.map((sub) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between space-x-2", children: [
408
- /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 flex-1", children: [
409
- /* @__PURE__ */ jsx(RadioGroupItem, { value: sub.id, id: `sub-${sub.id}` }),
410
- /* @__PURE__ */ jsx(Label, { htmlFor: `sub-${sub.id}`, className: "truncate", children: sub.name })
411
- ] }),
412
- sub.type === "external" && onSubtitleRemove && /* @__PURE__ */ jsx(
413
- Button,
414
- {
415
- variant: "ghost",
416
- size: "sm",
417
- onClick: () => onSubtitleRemove(sub.id),
418
- className: "h-6 w-6 p-0",
419
- children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
420
- }
421
- )
422
- ] }, sub.id))
423
- ] }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground text-center py-2", children: "No subtitles available" })
424
- ] }) })
425
- ] }),
426
- chapters.length > 0 && /* @__PURE__ */ jsxs(Popover, { open: chaptersMenuOpen, onOpenChange: setChaptersMenuOpen, children: [
427
- /* @__PURE__ */ jsxs(Tooltip, { children: [
428
- /* @__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" }) }) }) }),
429
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Chapters" }) })
456
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs tabular-nums ml-2 text-white/90", children: [
457
+ formattedProgress,
458
+ " ",
459
+ /* @__PURE__ */ jsxs("span", { className: "text-white/50", children: [
460
+ "/ ",
461
+ formattedDuration
462
+ ] })
463
+ ] })
430
464
  ] }),
431
- /* @__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(
432
- "button",
433
- {
434
- className: cn(
435
- "flex items-center justify-between px-4 py-2 text-sm hover:bg-accent transition-colors text-left",
436
- currentChapter?.index === chapter.index && "bg-accent font-medium"
437
- ),
438
- onClick: () => {
439
- onGoToChapter?.(chapter.index);
440
- setChaptersMenuOpen(false);
441
- },
442
- children: [
443
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: chapter.title }),
444
- /* @__PURE__ */ jsx("span", { className: "ml-4 font-mono text-xs text-muted-foreground shrink-0", children: formatTime(chapter.startTime) })
445
- ]
446
- },
447
- chapter.index
448
- )) }) })
449
- ] }),
450
- /* @__PURE__ */ jsxs(Popover, { children: [
451
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx(Settings2, {}) }) }),
452
- /* @__PURE__ */ jsxs(PopoverContent, { className: "w-64 space-y-4", children: [
453
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
454
- /* @__PURE__ */ jsxs(Label, { children: [
455
- "Brightness: ",
456
- filters.brightness,
457
- "%"
465
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
466
+ audioTracks.length > 0 && /* @__PURE__ */ jsxs(Popover, { children: [
467
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
468
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", "aria-label": "Audio track", children: [
469
+ /* @__PURE__ */ jsx(AudioLines, {}),
470
+ tracksLoading && /* @__PURE__ */ jsx(Loader2, { className: "absolute top-0 right-0 h-2.5 w-2.5 animate-spin text-primary" })
471
+ ] }) }) }),
472
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Audio track" }) })
458
473
  ] }),
459
- /* @__PURE__ */ jsx(Slider, { value: [filters.brightness], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, brightness: val }) })
474
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-56", 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: [
475
+ /* @__PURE__ */ jsx(RadioGroupItem, { value: track.id, id: `audio-${track.id}` }),
476
+ /* @__PURE__ */ jsx(Label, { htmlFor: `audio-${track.id}`, children: track.name })
477
+ ] }, track.id)) }) }) })
460
478
  ] }),
461
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
462
- /* @__PURE__ */ jsxs(Label, { children: [
463
- "Contrast: ",
464
- filters.contrast,
465
- "%"
479
+ /* @__PURE__ */ jsxs(Popover, { children: [
480
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
481
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "icon", className: "relative", "aria-label": "Subtitles", children: [
482
+ /* @__PURE__ */ jsx(Subtitles, {}),
483
+ 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" })
484
+ ] }) }) }),
485
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Subtitles" }) })
466
486
  ] }),
467
- /* @__PURE__ */ jsx(Slider, { value: [filters.contrast], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, contrast: val }) })
487
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-64", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
488
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
489
+ /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: "Subtitles" }),
490
+ onSubtitleUpload && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onSubtitleUpload, className: "h-7 px-2", children: [
491
+ /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3 mr-1" }),
492
+ "Add"
493
+ ] })
494
+ ] }),
495
+ 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: [
496
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
497
+ /* @__PURE__ */ jsx(RadioGroupItem, { value: "-1", id: "sub-off" }),
498
+ /* @__PURE__ */ jsx(Label, { htmlFor: "sub-off", children: "Off" })
499
+ ] }),
500
+ subtitles.map((sub) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between space-x-2", children: [
501
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 flex-1", children: [
502
+ /* @__PURE__ */ jsx(RadioGroupItem, { value: sub.id, id: `sub-${sub.id}` }),
503
+ /* @__PURE__ */ jsx(Label, { htmlFor: `sub-${sub.id}`, className: "truncate", children: sub.name })
504
+ ] }),
505
+ sub.type === "external" && onSubtitleRemove && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => onSubtitleRemove(sub.id), className: "h-6 w-6 p-0", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
506
+ ] }, sub.id))
507
+ ] }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground text-center py-2", children: "No subtitles available" })
508
+ ] }) })
468
509
  ] }),
469
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
470
- /* @__PURE__ */ jsxs(Label, { children: [
471
- "Saturation: ",
472
- filters.saturate,
473
- "%"
510
+ /* @__PURE__ */ jsxs(Popover, { children: [
511
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
512
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "font-mono w-12 h-9 px-2 text-xs", children: [
513
+ playbackRate,
514
+ "x"
515
+ ] }) }) }),
516
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Playback speed" }) })
474
517
  ] }),
475
- /* @__PURE__ */ jsx(Slider, { value: [filters.saturate], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, saturate: val }) })
518
+ /* @__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: [
519
+ /* @__PURE__ */ jsx(RadioGroupItem, { value: String(rate), id: `rate-${rate}` }),
520
+ /* @__PURE__ */ jsxs(Label, { htmlFor: `rate-${rate}`, children: [
521
+ rate,
522
+ "x"
523
+ ] })
524
+ ] }, rate)) }) })
476
525
  ] }),
477
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
478
- /* @__PURE__ */ jsxs(Label, { children: [
479
- "Hue: ",
480
- filters.hue,
481
- "\xB0"
526
+ /* @__PURE__ */ jsxs(Popover, { children: [
527
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
528
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Settings", children: /* @__PURE__ */ jsx(Settings2, {}) }) }) }),
529
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Settings" }) })
482
530
  ] }),
483
- /* @__PURE__ */ jsx(Slider, { value: [filters.hue], max: 360, onValueChange: ([val]) => onFiltersChange({ ...filters, hue: val }) })
531
+ /* @__PURE__ */ jsxs(PopoverContent, { className: "w-72 p-2 space-y-1", align: "end", children: [
532
+ /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
533
+ pipSupported && /* @__PURE__ */ jsxs(
534
+ "button",
535
+ {
536
+ onClick: onTogglePiP,
537
+ "aria-label": isPiP ? "Exit picture-in-picture" : "Enter picture-in-picture",
538
+ className: cn(
539
+ "flex items-center gap-2 w-full px-2 py-1.5 rounded text-sm hover:bg-accent text-left",
540
+ isPiP && "text-primary"
541
+ ),
542
+ children: [
543
+ /* @__PURE__ */ jsx(PictureInPicture2, { className: "h-4 w-4" }),
544
+ "Picture-in-Picture"
545
+ ]
546
+ }
547
+ ),
548
+ /* @__PURE__ */ jsxs("button", { onClick: onScreenshot, className: "flex items-center gap-2 w-full px-2 py-1.5 rounded text-sm hover:bg-accent text-left", children: [
549
+ /* @__PURE__ */ jsx(Camera, { className: "h-4 w-4" }),
550
+ "Screenshot"
551
+ ] }),
552
+ onShowInfo && /* @__PURE__ */ jsxs("button", { onClick: onShowInfo, className: "flex items-center gap-2 w-full px-2 py-1.5 rounded text-sm hover:bg-accent text-left", children: [
553
+ /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
554
+ "Video information"
555
+ ] }),
556
+ onOpenShortcuts && /* @__PURE__ */ jsxs("button", { onClick: onOpenShortcuts, className: "flex items-center gap-2 w-full px-2 py-1.5 rounded text-sm hover:bg-accent text-left", children: [
557
+ /* @__PURE__ */ jsx(Keyboard, { className: "h-4 w-4" }),
558
+ "Keyboard shortcuts"
559
+ ] })
560
+ ] }),
561
+ /* @__PURE__ */ jsxs("div", { className: "border-t border-border/40 pt-2 space-y-2 px-1", children: [
562
+ /* @__PURE__ */ jsx(Label, { className: "text-[10px] uppercase tracking-wider text-muted-foreground", children: "Display" }),
563
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
564
+ /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground", children: [
565
+ "Brightness: ",
566
+ filters.brightness,
567
+ "%"
568
+ ] }),
569
+ /* @__PURE__ */ jsx(Slider, { value: [filters.brightness], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, brightness: val }), trackClassName: "h-[3px]", thumbClassName: "h-3 w-3 border" })
570
+ ] }),
571
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
572
+ /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground", children: [
573
+ "Contrast: ",
574
+ filters.contrast,
575
+ "%"
576
+ ] }),
577
+ /* @__PURE__ */ jsx(Slider, { value: [filters.contrast], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, contrast: val }), trackClassName: "h-[3px]", thumbClassName: "h-3 w-3 border" })
578
+ ] }),
579
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
580
+ /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground", children: [
581
+ "Saturation: ",
582
+ filters.saturate,
583
+ "%"
584
+ ] }),
585
+ /* @__PURE__ */ jsx(Slider, { value: [filters.saturate], max: 200, onValueChange: ([val]) => onFiltersChange({ ...filters, saturate: val }), trackClassName: "h-[3px]", thumbClassName: "h-3 w-3 border" })
586
+ ] }),
587
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
588
+ /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground", children: [
589
+ "Hue: ",
590
+ filters.hue,
591
+ "\xB0"
592
+ ] }),
593
+ /* @__PURE__ */ jsx(Slider, { value: [filters.hue], max: 360, onValueChange: ([val]) => onFiltersChange({ ...filters, hue: val }), trackClassName: "h-[3px]", thumbClassName: "h-3 w-3 border" })
594
+ ] }),
595
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
596
+ /* @__PURE__ */ jsxs(Label, { className: "text-xs text-muted-foreground", children: [
597
+ "Zoom: ",
598
+ Math.round(zoom * 100),
599
+ "%"
600
+ ] }),
601
+ /* @__PURE__ */ jsx(Slider, { value: [zoom], min: 1, max: 3, step: 0.1, onValueChange: ([val]) => onZoomChange(val), trackClassName: "h-[3px]", thumbClassName: "h-3 w-3 border" })
602
+ ] })
603
+ ] })
604
+ ] })
484
605
  ] }),
485
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
486
- /* @__PURE__ */ jsxs(Label, { children: [
487
- "Zoom: ",
488
- Math.round(zoom * 100),
489
- "%"
606
+ chapters.length > 0 && /* @__PURE__ */ jsxs(Popover, { open: chaptersMenuOpen, onOpenChange: setChaptersMenuOpen, children: [
607
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
608
+ /* @__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" }) }) }) }),
609
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Chapters" }) })
490
610
  ] }),
491
- /* @__PURE__ */ jsx(Slider, { value: [zoom], min: 1, max: 3, step: 0.1, onValueChange: ([val]) => onZoomChange(val) })
611
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-72 p-0", align: "end", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col max-h-64 overflow-y-auto", children: chapters.map((chapter) => /* @__PURE__ */ jsxs(
612
+ "button",
613
+ {
614
+ className: cn(
615
+ "flex items-center justify-between px-4 py-2 text-sm hover:bg-accent transition-colors text-left",
616
+ currentChapter?.index === chapter.index && "bg-accent font-medium"
617
+ ),
618
+ onClick: () => {
619
+ onGoToChapter?.(chapter.index);
620
+ setChaptersMenuOpen(false);
621
+ },
622
+ children: [
623
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: chapter.title }),
624
+ /* @__PURE__ */ jsx("span", { className: "ml-4 font-mono text-xs text-muted-foreground shrink-0", children: formatTime2(chapter.startTime) })
625
+ ]
626
+ },
627
+ chapter.index
628
+ )) }) })
629
+ ] }),
630
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
631
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onFullScreenToggle, children: isFullScreen ? /* @__PURE__ */ jsx(Minimize, {}) : /* @__PURE__ */ jsx(Maximize, {}) }) }),
632
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Fullscreen (F)" }) })
492
633
  ] })
493
634
  ] })
494
- ] }),
495
- onShowInfo && /* @__PURE__ */ jsxs(Tooltip, { children: [
496
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onShowInfo, children: /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }) }) }),
497
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Video Information" }) })
498
- ] }),
499
- onOpenShortcuts && /* @__PURE__ */ jsxs(Tooltip, { children: [
500
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onOpenShortcuts, children: /* @__PURE__ */ jsx(Keyboard, { className: "h-4 w-4" }) }) }),
501
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Keyboard Shortcuts" }) })
502
- ] }),
503
- /* @__PURE__ */ jsxs(Tooltip, { children: [
504
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onScreenshot, children: /* @__PURE__ */ jsx(Camera, {}) }) }),
505
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Screenshot" }) })
506
- ] }),
507
- onABLoopCycle && /* @__PURE__ */ jsxs(Tooltip, { children: [
508
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
509
- Button,
510
- {
511
- variant: "ghost",
512
- size: "icon",
513
- onClick: onABLoopCycle,
514
- "aria-label": "A-B loop",
515
- "data-testid": "ab-loop-button",
516
- "data-active": abLoop.isLooping,
517
- className: cn("font-mono text-xs font-bold", abLoop.isLooping && "text-primary"),
518
- children: [
519
- /* @__PURE__ */ jsx("span", { className: cn(abLoop.pointA !== null && "text-primary"), children: "A" }),
520
- /* @__PURE__ */ jsx("span", { className: "opacity-50", children: "-" }),
521
- /* @__PURE__ */ jsx("span", { className: cn(abLoop.pointB !== null && "text-primary"), children: "B" })
522
- ]
523
- }
524
- ) }),
525
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: abLoop.pointA === null ? "Set loop start (A)" : abLoop.pointB === null ? "Set loop end (B)" : "Clear A-B loop" }) })
526
- ] }),
527
- /* @__PURE__ */ jsxs(Tooltip, { children: [
528
- /* @__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, {}) }) }),
529
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Loop" }) })
530
- ] }),
531
- pipSupported && /* @__PURE__ */ jsxs(Tooltip, { children: [
532
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
533
- Button,
534
- {
535
- variant: "ghost",
536
- size: "icon",
537
- onClick: onTogglePiP,
538
- "aria-label": isPiP ? "Exit picture-in-picture" : "Enter picture-in-picture",
539
- className: isPiP ? "text-primary" : "",
540
- children: /* @__PURE__ */ jsx(PictureInPicture2, {})
541
- }
542
- ) }),
543
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: isPiP ? "Exit picture-in-picture" : "Enter picture-in-picture" }) })
544
- ] }),
545
- /* @__PURE__ */ jsxs(Tooltip, { children: [
546
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", onClick: onFullScreenToggle, children: isFullScreen ? /* @__PURE__ */ jsx(Minimize, {}) : /* @__PURE__ */ jsx(Maximize, {}) }) }),
547
- /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsx("p", { children: "Fullscreen (F)" }) })
548
635
  ] })
549
- ] })
550
- ] })
551
- ] }) });
636
+ ]
637
+ }
638
+ ) });
552
639
  });
553
640
  var player_controls_default = PlayerControls;
554
- var ScrollArea = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
641
+ var ScrollArea = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
555
642
  ScrollAreaPrimitive.Root,
556
643
  {
557
644
  ref,
@@ -565,23 +652,23 @@ var ScrollArea = React10.forwardRef(({ className, children, ...props }, ref) =>
565
652
  }
566
653
  ));
567
654
  ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
568
- var ScrollBar = React10.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx(
655
+ var ScrollBar = React11.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx(
569
656
  ScrollAreaPrimitive.ScrollAreaScrollbar,
570
657
  {
571
658
  ref,
572
659
  orientation,
573
660
  className: cn(
574
661
  "flex touch-none select-none transition-colors",
575
- orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
576
- orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
662
+ orientation === "vertical" && "h-full w-2 border-l border-l-transparent p-[1px]",
663
+ orientation === "horizontal" && "h-2 flex-col border-t border-t-transparent p-[1px]",
577
664
  className
578
665
  ),
579
666
  ...props,
580
- children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
667
+ children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-muted-foreground/30 hover:bg-muted-foreground/55 active:bg-muted-foreground/75 transition-colors" })
581
668
  }
582
669
  ));
583
670
  ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
584
- var Input = React10.forwardRef(
671
+ var Input = React11.forwardRef(
585
672
  ({ className, type, ...props }, ref) => {
586
673
  return /* @__PURE__ */ jsx(
587
674
  "input",
@@ -600,7 +687,7 @@ var Input = React10.forwardRef(
600
687
  Input.displayName = "Input";
601
688
  var Select = SelectPrimitive.Root;
602
689
  var SelectValue = SelectPrimitive.Value;
603
- var SelectTrigger = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
690
+ var SelectTrigger = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
604
691
  SelectPrimitive.Trigger,
605
692
  {
606
693
  ref,
@@ -616,7 +703,7 @@ var SelectTrigger = React10.forwardRef(({ className, children, ...props }, ref)
616
703
  }
617
704
  ));
618
705
  SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
619
- var SelectScrollUpButton = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
706
+ var SelectScrollUpButton = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
620
707
  SelectPrimitive.ScrollUpButton,
621
708
  {
622
709
  ref,
@@ -629,7 +716,7 @@ var SelectScrollUpButton = React10.forwardRef(({ className, ...props }, ref) =>
629
716
  }
630
717
  ));
631
718
  SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
632
- var SelectScrollDownButton = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
719
+ var SelectScrollDownButton = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
633
720
  SelectPrimitive.ScrollDownButton,
634
721
  {
635
722
  ref,
@@ -642,7 +729,7 @@ var SelectScrollDownButton = React10.forwardRef(({ className, ...props }, ref) =
642
729
  }
643
730
  ));
644
731
  SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
645
- var SelectContent = React10.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
732
+ var SelectContent = React11.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
646
733
  SelectPrimitive.Content,
647
734
  {
648
735
  ref,
@@ -670,7 +757,7 @@ var SelectContent = React10.forwardRef(({ className, children, position = "poppe
670
757
  }
671
758
  ) }));
672
759
  SelectContent.displayName = SelectPrimitive.Content.displayName;
673
- var SelectLabel = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
760
+ var SelectLabel = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
674
761
  SelectPrimitive.Label,
675
762
  {
676
763
  ref,
@@ -679,7 +766,7 @@ var SelectLabel = React10.forwardRef(({ className, ...props }, ref) => /* @__PUR
679
766
  }
680
767
  ));
681
768
  SelectLabel.displayName = SelectPrimitive.Label.displayName;
682
- var SelectItem = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
769
+ var SelectItem = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
683
770
  SelectPrimitive.Item,
684
771
  {
685
772
  ref,
@@ -695,7 +782,7 @@ var SelectItem = React10.forwardRef(({ className, children, ...props }, ref) =>
695
782
  }
696
783
  ));
697
784
  SelectItem.displayName = SelectPrimitive.Item.displayName;
698
- var SelectSeparator = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
785
+ var SelectSeparator = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
699
786
  SelectPrimitive.Separator,
700
787
  {
701
788
  ref,
@@ -714,7 +801,7 @@ var NEXT_SIZE = {
714
801
  md: "lg",
715
802
  lg: "sm"
716
803
  };
717
- function formatTime2(seconds) {
804
+ function formatTime3(seconds) {
718
805
  if (!seconds || !isFinite(seconds)) return "";
719
806
  const h = Math.floor(seconds / 3600);
720
807
  const m = Math.floor(seconds % 3600 / 60);
@@ -743,7 +830,7 @@ function SortablePlaylistItem({ item, index, isActive, downloadReady, onSelect,
743
830
  transition,
744
831
  opacity: isDragging ? 0.5 : 1
745
832
  };
746
- const duration = formatTime2(item.duration ?? 0);
833
+ const duration = formatTime3(item.duration ?? 0);
747
834
  return /* @__PURE__ */ jsxs(
748
835
  "div",
749
836
  {
@@ -897,296 +984,306 @@ var PlaylistPanel = ({
897
984
  });
898
985
  onReorder(sorted);
899
986
  };
900
- return /* @__PURE__ */ jsx(TooltipProvider, { children: !isOpen ? (
901
- /* ── Collapsed drawer strip ── */
902
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-11 h-full bg-card border-l border-border shrink-0", children: [
903
- /* @__PURE__ */ jsxs(Tooltip, { children: [
904
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
905
- Button,
906
- {
907
- variant: "ghost",
908
- size: "icon",
909
- className: "h-9 w-9 mt-2 shrink-0",
910
- onClick: onToggle,
911
- "aria-label": "Expand Playlist",
912
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
913
- }
914
- ) }),
915
- /* @__PURE__ */ jsx(TooltipContent, { side: "left", children: /* @__PURE__ */ jsx("p", { children: "Expand Playlist" }) })
916
- ] }),
917
- /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsx(
918
- "span",
919
- {
920
- className: "text-[10px] font-semibold text-muted-foreground tracking-widest uppercase select-none",
921
- style: { writingMode: "vertical-rl", transform: "rotate(180deg)" },
922
- children: "Playlist"
923
- }
924
- ) }),
925
- 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 })
926
- ] })
927
- ) : (
928
- /* ── Full panel ── */
929
- /* @__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: [
930
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b border-border shrink-0", children: [
931
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
932
- /* @__PURE__ */ jsx(ListVideo, { className: "h-4 w-4 text-primary shrink-0" }),
933
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-sm truncate", children: "Playlist" }),
934
- 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 })
935
- ] }),
936
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [
937
- playlist.length > 0 && /* @__PURE__ */ jsxs(Tooltip, { children: [
938
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
939
- Button,
940
- {
941
- variant: "ghost",
942
- size: "icon",
943
- className: "h-7 w-7",
944
- onClick: () => exportPlaylist(playlist),
945
- "aria-label": "Export Playlist",
946
- children: /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" })
947
- }
948
- ) }),
949
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Export as M3U8" }) })
950
- ] }),
951
- /* @__PURE__ */ jsxs(Tooltip, { children: [
952
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
953
- Button,
954
- {
955
- variant: "ghost",
956
- size: "icon",
957
- className: "h-7 w-7",
958
- onClick: () => m3uInputRef.current?.click(),
959
- "aria-label": "Import Playlist",
960
- children: /* @__PURE__ */ jsx(Upload, { className: "h-3.5 w-3.5" })
961
- }
962
- ) }),
963
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Import M3U/M3U8" }) })
964
- ] }),
965
- /* @__PURE__ */ jsxs(Tooltip, { children: [
966
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
967
- Button,
968
- {
969
- variant: "ghost",
970
- size: "icon",
971
- className: cn("h-7 w-7", isPinned && "text-primary bg-primary/10"),
972
- onClick: onTogglePin,
973
- "aria-label": isPinned ? "Unpin Playlist" : "Pin Playlist",
974
- children: isPinned ? /* @__PURE__ */ jsx(Pin, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(PinOff, { className: "h-3.5 w-3.5" })
975
- }
976
- ) }),
977
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: isPinned ? "Unpin (allow auto-hide on play)" : "Pin (keep open while playing)" }) })
978
- ] }),
979
- /* @__PURE__ */ jsxs(Tooltip, { children: [
980
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
981
- Button,
982
- {
983
- variant: "ghost",
984
- size: "icon",
985
- className: "h-7 w-7",
986
- onClick: () => onSizeChange(NEXT_SIZE[size]),
987
- "aria-label": "Resize Playlist",
988
- children: size === "lg" ? /* @__PURE__ */ jsx(Minimize2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Maximize2, { className: "h-3.5 w-3.5" })
989
- }
990
- ) }),
991
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: size === "lg" ? "Make smaller" : "Make larger" }) })
992
- ] }),
987
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(
988
+ "div",
989
+ {
990
+ className: cn(
991
+ "h-full flex flex-col bg-card border-l border-border shrink-0 overflow-hidden",
992
+ "transition-[width] duration-300 ease-in-out motion-reduce:transition-none",
993
+ isOpen ? SIZE_WIDTHS[size] : "w-11"
994
+ ),
995
+ children: !isOpen ? (
996
+ /* ── Collapsed drawer strip ── */
997
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center w-11 h-full shrink-0", children: [
993
998
  /* @__PURE__ */ jsxs(Tooltip, { children: [
994
999
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
995
1000
  Button,
996
1001
  {
997
1002
  variant: "ghost",
998
1003
  size: "icon",
999
- className: "h-7 w-7",
1004
+ className: "h-9 w-9 mt-2 shrink-0",
1000
1005
  onClick: onToggle,
1001
- "aria-label": "Collapse Playlist",
1002
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
1006
+ "aria-label": "Expand Playlist",
1007
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
1003
1008
  }
1004
1009
  ) }),
1005
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Collapse" }) })
1006
- ] })
1007
- ] })
1008
- ] }),
1009
- /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2 border-b border-border shrink-0", children: [
1010
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1011
- /* @__PURE__ */ jsxs(Button, { onClick: () => fileInputRef.current?.click(), className: "flex-1 h-8 text-xs", children: [
1012
- /* @__PURE__ */ jsx(FilePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
1013
- " Add Files"
1010
+ /* @__PURE__ */ jsx(TooltipContent, { side: "left", children: /* @__PURE__ */ jsx("p", { children: "Expand Playlist" }) })
1014
1011
  ] }),
1015
- /* @__PURE__ */ jsxs(Tooltip, { children: [
1016
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1017
- Button,
1018
- {
1019
- variant: "outline",
1020
- size: "icon",
1021
- className: "h-8 w-8 shrink-0",
1022
- onClick: () => folderInputRef.current?.click(),
1023
- "aria-label": "Open Folder",
1024
- children: /* @__PURE__ */ jsx(FolderOpen, { className: "h-3.5 w-3.5" })
1025
- }
1026
- ) }),
1027
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Open Folder" }) })
1028
- ] })
1029
- ] }),
1030
- /* @__PURE__ */ jsx(
1031
- "input",
1032
- {
1033
- type: "file",
1034
- ref: fileInputRef,
1035
- className: "hidden",
1036
- multiple: true,
1037
- accept: "video/*,.mkv,.avi,.mov,.wmv,.flv,.webm,.vtt,.srt",
1038
- onChange: (e) => e.target.files && onFilesAdded(e.target.files)
1039
- }
1040
- ),
1041
- /* @__PURE__ */ jsx(
1042
- "input",
1043
- {
1044
- type: "file",
1045
- ref: folderInputRef,
1046
- className: "hidden",
1047
- multiple: true,
1048
- accept: "video/*",
1049
- webkitdirectory: "",
1050
- onChange: handleFolderSelect
1051
- }
1052
- ),
1053
- /* @__PURE__ */ jsx(
1054
- "input",
1055
- {
1056
- type: "file",
1057
- ref: m3uInputRef,
1058
- className: "hidden",
1059
- accept: ".m3u,.m3u8",
1060
- onChange: handleM3USelect
1061
- }
1062
- ),
1063
- /* @__PURE__ */ jsxs("form", { onSubmit: handleStreamUrlSubmit, className: "flex gap-1.5", children: [
1064
- /* @__PURE__ */ jsx(
1065
- Input,
1012
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsx(
1013
+ "span",
1066
1014
  {
1067
- type: "url",
1068
- placeholder: "Enter stream URL",
1069
- value: streamUrl,
1070
- onChange: (e) => setStreamUrl(e.target.value),
1071
- className: "h-8 text-xs"
1015
+ className: "text-[10px] font-semibold text-muted-foreground tracking-widest uppercase select-none",
1016
+ style: { writingMode: "vertical-rl", transform: "rotate(180deg)" },
1017
+ children: "Playlist"
1072
1018
  }
1073
- ),
1074
- /* @__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" }) }),
1075
- showMagnet && /* @__PURE__ */ jsxs(Tooltip, { children: [
1076
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1077
- Button,
1019
+ ) }),
1020
+ 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 })
1021
+ ] })
1022
+ ) : (
1023
+ /* ── Full panel ── */
1024
+ /* @__PURE__ */ jsxs("div", { className: cn("h-full flex flex-col shrink-0 transition-[width] duration-200", SIZE_WIDTHS[size]), children: [
1025
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b border-border shrink-0", children: [
1026
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
1027
+ /* @__PURE__ */ jsx(ListVideo, { className: "h-4 w-4 text-primary shrink-0" }),
1028
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-sm truncate", children: "Playlist" }),
1029
+ 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 })
1030
+ ] }),
1031
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 shrink-0", children: [
1032
+ playlist.length > 0 && /* @__PURE__ */ jsxs(Tooltip, { children: [
1033
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1034
+ Button,
1035
+ {
1036
+ variant: "ghost",
1037
+ size: "icon",
1038
+ className: "h-7 w-7",
1039
+ onClick: () => exportPlaylist(playlist),
1040
+ "aria-label": "Export Playlist",
1041
+ children: /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" })
1042
+ }
1043
+ ) }),
1044
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Export as M3U8" }) })
1045
+ ] }),
1046
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1047
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1048
+ Button,
1049
+ {
1050
+ variant: "ghost",
1051
+ size: "icon",
1052
+ className: "h-7 w-7",
1053
+ onClick: () => m3uInputRef.current?.click(),
1054
+ "aria-label": "Import Playlist",
1055
+ children: /* @__PURE__ */ jsx(Upload, { className: "h-3.5 w-3.5" })
1056
+ }
1057
+ ) }),
1058
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Import M3U/M3U8" }) })
1059
+ ] }),
1060
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1061
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1062
+ Button,
1063
+ {
1064
+ variant: "ghost",
1065
+ size: "icon",
1066
+ className: cn("h-7 w-7", isPinned && "text-primary bg-primary/10"),
1067
+ onClick: onTogglePin,
1068
+ "aria-label": isPinned ? "Unpin Playlist" : "Pin Playlist",
1069
+ children: isPinned ? /* @__PURE__ */ jsx(Pin, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(PinOff, { className: "h-3.5 w-3.5" })
1070
+ }
1071
+ ) }),
1072
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: isPinned ? "Unpin (allow auto-hide on play)" : "Pin (keep open while playing)" }) })
1073
+ ] }),
1074
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1075
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1076
+ Button,
1077
+ {
1078
+ variant: "ghost",
1079
+ size: "icon",
1080
+ className: "h-7 w-7",
1081
+ onClick: () => onSizeChange(NEXT_SIZE[size]),
1082
+ "aria-label": "Resize Playlist",
1083
+ children: size === "lg" ? /* @__PURE__ */ jsx(Minimize2, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Maximize2, { className: "h-3.5 w-3.5" })
1084
+ }
1085
+ ) }),
1086
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: size === "lg" ? "Make smaller" : "Make larger" }) })
1087
+ ] }),
1088
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1089
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1090
+ Button,
1091
+ {
1092
+ variant: "ghost",
1093
+ size: "icon",
1094
+ className: "h-7 w-7",
1095
+ onClick: onToggle,
1096
+ "aria-label": "Collapse Playlist",
1097
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
1098
+ }
1099
+ ) }),
1100
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Collapse" }) })
1101
+ ] })
1102
+ ] })
1103
+ ] }),
1104
+ /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2 border-b border-border shrink-0", children: [
1105
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1106
+ /* @__PURE__ */ jsxs(Button, { onClick: () => fileInputRef.current?.click(), className: "flex-1 h-8 text-xs", children: [
1107
+ /* @__PURE__ */ jsx(FilePlus, { className: "mr-1.5 h-3.5 w-3.5" }),
1108
+ " Add Files"
1109
+ ] }),
1110
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1111
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1112
+ Button,
1113
+ {
1114
+ variant: "outline",
1115
+ size: "icon",
1116
+ className: "h-8 w-8 shrink-0",
1117
+ onClick: () => folderInputRef.current?.click(),
1118
+ "aria-label": "Open Folder",
1119
+ children: /* @__PURE__ */ jsx(FolderOpen, { className: "h-3.5 w-3.5" })
1120
+ }
1121
+ ) }),
1122
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Open Folder" }) })
1123
+ ] })
1124
+ ] }),
1125
+ /* @__PURE__ */ jsx(
1126
+ "input",
1078
1127
  {
1079
- type: "button",
1080
- size: "icon",
1081
- variant: showMagnetInput ? "default" : "outline",
1082
- className: "h-8 w-8 shrink-0",
1083
- onClick: () => {
1084
- setShowMagnetInput((v) => !v);
1085
- setMagnetError(null);
1086
- },
1087
- "aria-label": "Add magnet link",
1088
- children: /* @__PURE__ */ jsx(Link2, { className: "h-3.5 w-3.5" })
1128
+ type: "file",
1129
+ ref: fileInputRef,
1130
+ className: "hidden",
1131
+ multiple: true,
1132
+ accept: "video/*,.mkv,.avi,.mov,.wmv,.flv,.webm,.vtt,.srt",
1133
+ onChange: (e) => e.target.files && onFilesAdded(e.target.files)
1089
1134
  }
1090
- ) }),
1091
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Add Magnet Link" }) })
1092
- ] })
1093
- ] }),
1094
- showMagnet && showMagnetInput && /* @__PURE__ */ jsxs("form", { onSubmit: handleMagnetSubmit, className: "space-y-1.5", children: [
1095
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1135
+ ),
1096
1136
  /* @__PURE__ */ jsx(
1097
- Input,
1137
+ "input",
1098
1138
  {
1099
- placeholder: "magnet:?xt=urn:btih:\u2026",
1100
- value: magnetUri,
1101
- onChange: (e) => {
1102
- setMagnetUri(e.target.value);
1103
- setMagnetError(null);
1104
- },
1105
- className: "h-8 text-xs font-mono",
1106
- disabled: torrentStatus.status === "loading-metadata"
1139
+ type: "file",
1140
+ ref: folderInputRef,
1141
+ className: "hidden",
1142
+ multiple: true,
1143
+ accept: "video/*",
1144
+ webkitdirectory: "",
1145
+ onChange: handleFolderSelect
1107
1146
  }
1108
1147
  ),
1109
1148
  /* @__PURE__ */ jsx(
1110
- Button,
1149
+ "input",
1111
1150
  {
1112
- type: "submit",
1113
- size: "icon",
1114
- variant: "default",
1115
- className: "h-8 w-8 shrink-0",
1116
- disabled: !magnetUri.trim() || torrentStatus.status === "loading-metadata",
1117
- "aria-label": "Load magnet link",
1118
- children: torrentStatus.status === "loading-metadata" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Globe, { className: "h-3.5 w-3.5" })
1151
+ type: "file",
1152
+ ref: m3uInputRef,
1153
+ className: "hidden",
1154
+ accept: ".m3u,.m3u8",
1155
+ onChange: handleM3USelect
1119
1156
  }
1120
- )
1121
- ] }),
1122
- torrentStatus.status === "loading-metadata" && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground flex items-center gap-1", children: [
1123
- /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
1124
- "Fetching torrent info\u2026"
1157
+ ),
1158
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleStreamUrlSubmit, className: "flex gap-1.5", children: [
1159
+ /* @__PURE__ */ jsx(
1160
+ Input,
1161
+ {
1162
+ type: "url",
1163
+ placeholder: "Enter stream URL",
1164
+ value: streamUrl,
1165
+ onChange: (e) => setStreamUrl(e.target.value),
1166
+ className: "h-8 text-xs"
1167
+ }
1168
+ ),
1169
+ /* @__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" }) }),
1170
+ showMagnet && /* @__PURE__ */ jsxs(Tooltip, { children: [
1171
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1172
+ Button,
1173
+ {
1174
+ type: "button",
1175
+ size: "icon",
1176
+ variant: showMagnetInput ? "default" : "outline",
1177
+ className: "h-8 w-8 shrink-0",
1178
+ onClick: () => {
1179
+ setShowMagnetInput((v) => !v);
1180
+ setMagnetError(null);
1181
+ },
1182
+ "aria-label": "Add magnet link",
1183
+ children: /* @__PURE__ */ jsx(Link2, { className: "h-3.5 w-3.5" })
1184
+ }
1185
+ ) }),
1186
+ /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsx("p", { children: "Add Magnet Link" }) })
1187
+ ] })
1188
+ ] }),
1189
+ showMagnet && showMagnetInput && /* @__PURE__ */ jsxs("form", { onSubmit: handleMagnetSubmit, className: "space-y-1.5", children: [
1190
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
1191
+ /* @__PURE__ */ jsx(
1192
+ Input,
1193
+ {
1194
+ placeholder: "magnet:?xt=urn:btih:\u2026",
1195
+ value: magnetUri,
1196
+ onChange: (e) => {
1197
+ setMagnetUri(e.target.value);
1198
+ setMagnetError(null);
1199
+ },
1200
+ className: "h-8 text-xs font-mono",
1201
+ disabled: torrentStatus.status === "loading-metadata"
1202
+ }
1203
+ ),
1204
+ /* @__PURE__ */ jsx(
1205
+ Button,
1206
+ {
1207
+ type: "submit",
1208
+ size: "icon",
1209
+ variant: "default",
1210
+ className: "h-8 w-8 shrink-0",
1211
+ disabled: !magnetUri.trim() || torrentStatus.status === "loading-metadata",
1212
+ "aria-label": "Load magnet link",
1213
+ children: torrentStatus.status === "loading-metadata" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Globe, { className: "h-3.5 w-3.5" })
1214
+ }
1215
+ )
1216
+ ] }),
1217
+ torrentStatus.status === "loading-metadata" && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground flex items-center gap-1", children: [
1218
+ /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
1219
+ "Fetching torrent info\u2026"
1220
+ ] }),
1221
+ magnetError && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-destructive flex items-center gap-1", children: [
1222
+ /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" }),
1223
+ magnetError
1224
+ ] })
1225
+ ] }),
1226
+ showMagnet && torrentStatus.status === "ready" && (torrentStatus.progress >= 1 ? /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-emerald-500 flex items-center gap-1", children: [
1227
+ /* @__PURE__ */ jsx(Download, { className: "h-3 w-3 shrink-0" }),
1228
+ "Download ready \u2014 hover a file to save it"
1229
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1230
+ /* @__PURE__ */ jsx("div", { className: "w-full bg-muted rounded-full h-1 overflow-hidden", children: /* @__PURE__ */ jsx(
1231
+ "div",
1232
+ {
1233
+ className: "bg-emerald-500 h-1 rounded-full transition-all duration-500",
1234
+ style: { width: `${Math.round(torrentStatus.progress * 100)}%` }
1235
+ }
1236
+ ) }),
1237
+ /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground flex items-center justify-between", children: [
1238
+ /* @__PURE__ */ jsxs("span", { children: [
1239
+ "\u2193 ",
1240
+ formatBytes(torrentStatus.downloadSpeed),
1241
+ "/s \xB7 ",
1242
+ torrentStatus.numPeers,
1243
+ " peer",
1244
+ torrentStatus.numPeers !== 1 ? "s" : ""
1245
+ ] }),
1246
+ /* @__PURE__ */ jsxs("span", { children: [
1247
+ Math.round(torrentStatus.progress * 100),
1248
+ "%"
1249
+ ] })
1250
+ ] })
1251
+ ] })),
1252
+ playlist.length > 1 && /* @__PURE__ */ jsxs(Select, { value: sortKey, onValueChange: handleSort, children: [
1253
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-7 text-xs", "aria-label": "Sort playlist", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort by\u2026" }) }),
1254
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
1255
+ /* @__PURE__ */ jsx(SelectItem, { value: "name-asc", children: "Name A\u2013Z" }),
1256
+ /* @__PURE__ */ jsx(SelectItem, { value: "name-desc", children: "Name Z\u2013A" }),
1257
+ /* @__PURE__ */ jsx(SelectItem, { value: "duration-asc", children: "Shortest first" }),
1258
+ /* @__PURE__ */ jsx(SelectItem, { value: "duration-desc", children: "Longest first" })
1259
+ ] })
1260
+ ] })
1125
1261
  ] }),
1126
- magnetError && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-destructive flex items-center gap-1", children: [
1127
- /* @__PURE__ */ jsx(AlertCircle, { className: "h-3 w-3 shrink-0" }),
1128
- magnetError
1129
- ] })
1130
- ] }),
1131
- showMagnet && torrentStatus.status === "ready" && (torrentStatus.progress >= 1 ? /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-emerald-500 flex items-center gap-1", children: [
1132
- /* @__PURE__ */ jsx(Download, { className: "h-3 w-3 shrink-0" }),
1133
- "Download ready \u2014 hover a file to save it"
1134
- ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1135
- /* @__PURE__ */ jsx("div", { className: "w-full bg-muted rounded-full h-1 overflow-hidden", children: /* @__PURE__ */ jsx(
1136
- "div",
1262
+ /* @__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: [
1263
+ /* @__PURE__ */ jsx("p", { children: "Your playlist is empty." }),
1264
+ /* @__PURE__ */ jsx("p", { children: "Add files or a stream URL to get started." })
1265
+ ] }) : /* @__PURE__ */ jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(
1266
+ SortableContext,
1137
1267
  {
1138
- className: "bg-emerald-500 h-1 rounded-full transition-all duration-500",
1139
- style: { width: `${Math.round(torrentStatus.progress * 100)}%` }
1268
+ items: playlist.map((i) => i.id),
1269
+ strategy: verticalListSortingStrategy,
1270
+ children: playlist.map((item, index) => /* @__PURE__ */ jsx(
1271
+ SortablePlaylistItem,
1272
+ {
1273
+ item,
1274
+ index,
1275
+ isActive: index === currentVideoIndex,
1276
+ downloadReady: item.source === "torrent" && torrentStatus.progress >= 1,
1277
+ onSelect: onSelectVideo,
1278
+ onRemove: onRemoveItem
1279
+ },
1280
+ item.id
1281
+ ))
1140
1282
  }
1141
- ) }),
1142
- /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground flex items-center justify-between", children: [
1143
- /* @__PURE__ */ jsxs("span", { children: [
1144
- "\u2193 ",
1145
- formatBytes(torrentStatus.downloadSpeed),
1146
- "/s \xB7 ",
1147
- torrentStatus.numPeers,
1148
- " peer",
1149
- torrentStatus.numPeers !== 1 ? "s" : ""
1150
- ] }),
1151
- /* @__PURE__ */ jsxs("span", { children: [
1152
- Math.round(torrentStatus.progress * 100),
1153
- "%"
1154
- ] })
1155
- ] })
1156
- ] })),
1157
- playlist.length > 1 && /* @__PURE__ */ jsxs(Select, { value: sortKey, onValueChange: handleSort, children: [
1158
- /* @__PURE__ */ jsx(SelectTrigger, { className: "h-7 text-xs", "aria-label": "Sort playlist", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort by\u2026" }) }),
1159
- /* @__PURE__ */ jsxs(SelectContent, { children: [
1160
- /* @__PURE__ */ jsx(SelectItem, { value: "name-asc", children: "Name A\u2013Z" }),
1161
- /* @__PURE__ */ jsx(SelectItem, { value: "name-desc", children: "Name Z\u2013A" }),
1162
- /* @__PURE__ */ jsx(SelectItem, { value: "duration-asc", children: "Shortest first" }),
1163
- /* @__PURE__ */ jsx(SelectItem, { value: "duration-desc", children: "Longest first" })
1164
- ] })
1283
+ ) }) }) })
1165
1284
  ] })
1166
- ] }),
1167
- /* @__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: [
1168
- /* @__PURE__ */ jsx("p", { children: "Your playlist is empty." }),
1169
- /* @__PURE__ */ jsx("p", { children: "Add files or a stream URL to get started." })
1170
- ] }) : /* @__PURE__ */ jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(
1171
- SortableContext,
1172
- {
1173
- items: playlist.map((i) => i.id),
1174
- strategy: verticalListSortingStrategy,
1175
- children: playlist.map((item, index) => /* @__PURE__ */ jsx(
1176
- SortablePlaylistItem,
1177
- {
1178
- item,
1179
- index,
1180
- isActive: index === currentVideoIndex,
1181
- downloadReady: item.source === "torrent" && torrentStatus.progress >= 1,
1182
- onSelect: onSelectVideo,
1183
- onRemove: onRemoveItem
1184
- },
1185
- item.id
1186
- ))
1187
- }
1188
- ) }) }) })
1189
- ] })
1285
+ )
1286
+ }
1190
1287
  ) });
1191
1288
  };
1192
1289
  var playlist_panel_default = PlaylistPanel;
@@ -1279,7 +1376,7 @@ function PlayerErrorDisplay({ error, onRetry, onSkip, onDismiss }) {
1279
1376
  ] })
1280
1377
  ] });
1281
1378
  }
1282
- function formatTime3(seconds) {
1379
+ function formatTime4(seconds) {
1283
1380
  if (isNaN(seconds) || seconds === 0) return "\u2014";
1284
1381
  const h = Math.floor(seconds / 3600);
1285
1382
  const m = Math.floor(seconds % 3600 / 60);
@@ -1302,7 +1399,7 @@ function VideoInfoPanel({ metadata, onClose }) {
1302
1399
  const rows = [
1303
1400
  ["File", metadata.filename ? metadata.filename.split("/").pop() ?? metadata.filename : "\u2014"],
1304
1401
  ["Size", formatSize(metadata.fileSize)],
1305
- ["Duration", formatTime3(metadata.duration)],
1402
+ ["Duration", formatTime4(metadata.duration)],
1306
1403
  ["Container", metadata.container || "\u2014"],
1307
1404
  ["Resolution", metadata.width && metadata.height ? `${metadata.width} \xD7 ${metadata.height}` : "\u2014"],
1308
1405
  ["Frame Rate", metadata.frameRate ? `${metadata.frameRate} fps` : "\u2014"],
@@ -1340,7 +1437,7 @@ function VideoInfoPanel({ metadata, onClose }) {
1340
1437
  }
1341
1438
  var Dialog = DialogPrimitive.Root;
1342
1439
  var DialogPortal = DialogPrimitive.Portal;
1343
- var DialogOverlay = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1440
+ var DialogOverlay = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1344
1441
  DialogPrimitive.Overlay,
1345
1442
  {
1346
1443
  ref,
@@ -1352,7 +1449,7 @@ var DialogOverlay = React10.forwardRef(({ className, ...props }, ref) => /* @__P
1352
1449
  }
1353
1450
  ));
1354
1451
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
1355
- var DialogContent = React10.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
1452
+ var DialogContent = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(DialogPortal, { children: [
1356
1453
  /* @__PURE__ */ jsx(DialogOverlay, {}),
1357
1454
  /* @__PURE__ */ jsxs(
1358
1455
  DialogPrimitive.Content,
@@ -1388,7 +1485,7 @@ var DialogHeader = ({
1388
1485
  }
1389
1486
  );
1390
1487
  DialogHeader.displayName = "DialogHeader";
1391
- var DialogTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1488
+ var DialogTitle = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1392
1489
  DialogPrimitive.Title,
1393
1490
  {
1394
1491
  ref,
@@ -1400,7 +1497,7 @@ var DialogTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PUR
1400
1497
  }
1401
1498
  ));
1402
1499
  DialogTitle.displayName = DialogPrimitive.Title.displayName;
1403
- var DialogDescription = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1500
+ var DialogDescription = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1404
1501
  DialogPrimitive.Description,
1405
1502
  {
1406
1503
  ref,
@@ -1509,8 +1606,8 @@ function toast({ ...props }) {
1509
1606
  };
1510
1607
  }
1511
1608
  function useToast() {
1512
- const [state, setState] = React10.useState(memoryState);
1513
- React10.useEffect(() => {
1609
+ const [state, setState] = React11.useState(memoryState);
1610
+ React11.useEffect(() => {
1514
1611
  listeners.push(setState);
1515
1612
  return () => {
1516
1613
  const index = listeners.indexOf(setState);
@@ -1706,7 +1803,7 @@ function SubtitleOverlay({ videoRef, activeSubtitle }) {
1706
1803
  }
1707
1804
  var AlertDialog = AlertDialogPrimitive.Root;
1708
1805
  var AlertDialogPortal = AlertDialogPrimitive.Portal;
1709
- var AlertDialogOverlay = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1806
+ var AlertDialogOverlay = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1710
1807
  AlertDialogPrimitive.Overlay,
1711
1808
  {
1712
1809
  className: cn(
@@ -1718,7 +1815,7 @@ var AlertDialogOverlay = React10.forwardRef(({ className, ...props }, ref) => /*
1718
1815
  }
1719
1816
  ));
1720
1817
  AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
1721
- var AlertDialogContent = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(AlertDialogPortal, { children: [
1818
+ var AlertDialogContent = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxs(AlertDialogPortal, { children: [
1722
1819
  /* @__PURE__ */ jsx(AlertDialogOverlay, {}),
1723
1820
  /* @__PURE__ */ jsx(
1724
1821
  AlertDialogPrimitive.Content,
@@ -1761,7 +1858,7 @@ var AlertDialogFooter = ({
1761
1858
  }
1762
1859
  );
1763
1860
  AlertDialogFooter.displayName = "AlertDialogFooter";
1764
- var AlertDialogTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1861
+ var AlertDialogTitle = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1765
1862
  AlertDialogPrimitive.Title,
1766
1863
  {
1767
1864
  ref,
@@ -1770,7 +1867,7 @@ var AlertDialogTitle = React10.forwardRef(({ className, ...props }, ref) => /* @
1770
1867
  }
1771
1868
  ));
1772
1869
  AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
1773
- var AlertDialogDescription = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1870
+ var AlertDialogDescription = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1774
1871
  AlertDialogPrimitive.Description,
1775
1872
  {
1776
1873
  ref,
@@ -1779,7 +1876,7 @@ var AlertDialogDescription = React10.forwardRef(({ className, ...props }, ref) =
1779
1876
  }
1780
1877
  ));
1781
1878
  AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
1782
- var AlertDialogAction = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1879
+ var AlertDialogAction = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1783
1880
  AlertDialogPrimitive.Action,
1784
1881
  {
1785
1882
  ref,
@@ -1788,7 +1885,7 @@ var AlertDialogAction = React10.forwardRef(({ className, ...props }, ref) => /*
1788
1885
  }
1789
1886
  ));
1790
1887
  AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
1791
- var AlertDialogCancel = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1888
+ var AlertDialogCancel = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1792
1889
  AlertDialogPrimitive.Cancel,
1793
1890
  {
1794
1891
  ref,
@@ -1857,6 +1954,8 @@ var LightBirdPlayer = () => {
1857
1954
  const [cancellableProcessing, setCancellableProcessing] = useState(false);
1858
1955
  const [mediaThumbnail, setMediaThumbnail] = useState(null);
1859
1956
  const [tracksLoading, setTracksLoading] = useState(false);
1957
+ const abLoopCycleRef = useRef(() => {
1958
+ });
1860
1959
  const shortcutHandlers = useMemo(() => ({
1861
1960
  "play-pause": () => playback.togglePlay(),
1862
1961
  "seek-forward-5": () => {
@@ -1906,9 +2005,13 @@ var LightBirdPlayer = () => {
1906
2005
  const prev = chapters[cur.index - 1];
1907
2006
  if (prev) el.currentTime = prev.startTime;
1908
2007
  }
1909
- }
2008
+ },
2009
+ "frame-step-forward": () => playback.frameStep("forward"),
2010
+ "frame-step-backward": () => playback.frameStep("backward"),
2011
+ "loop-toggle": () => playback.toggleLoop(),
2012
+ "ab-loop-cycle": () => abLoopCycleRef.current()
1910
2013
  // eslint-disable-next-line react-hooks/exhaustive-deps
1911
- }), [playback.togglePlay, playback.seek, playback.setVolume, playback.toggleMute, fullscreen.toggle, chapters, currentChapter]);
2014
+ }), [playback.togglePlay, playback.seek, playback.setVolume, playback.toggleMute, playback.frameStep, playback.toggleLoop, fullscreen.toggle, chapters, currentChapter]);
1912
2015
  useKeyboardShortcuts(shortcuts, shortcutHandlers);
1913
2016
  const stopStallDetection = () => {
1914
2017
  if (streamStallDetectorRef.current) {
@@ -2311,6 +2414,9 @@ var LightBirdPlayer = () => {
2311
2414
  else if (abLoop.pointB === null) abLoop.setPointB();
2312
2415
  else abLoop.clear();
2313
2416
  }, [abLoop.pointA, abLoop.pointB, abLoop.setPointA, abLoop.setPointB, abLoop.clear]);
2417
+ useEffect(() => {
2418
+ abLoopCycleRef.current = handleABLoopCycle;
2419
+ }, [handleABLoopCycle]);
2314
2420
  const abLoopState = useMemo(
2315
2421
  () => ({ pointA: abLoop.pointA, pointB: abLoop.pointB, isLooping: abLoop.isLooping }),
2316
2422
  [abLoop.pointA, abLoop.pointB, abLoop.isLooping]
@@ -2439,9 +2545,11 @@ var LightBirdPlayer = () => {
2439
2545
  )
2440
2546
  }
2441
2547
  ),
2442
- playlist.currentItem && /* @__PURE__ */ jsx(
2548
+ /* @__PURE__ */ jsx(
2443
2549
  player_controls_default,
2444
2550
  {
2551
+ isDisabled: !playlist.currentItem,
2552
+ videoRef,
2445
2553
  isPlaying: playback.isPlaying,
2446
2554
  progress: playback.progress,
2447
2555
  duration: playback.duration,
@@ -2510,9 +2618,17 @@ var LightBirdPlayer = () => {
2510
2618
  }
2511
2619
  }
2512
2620
  ),
2513
- !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: [
2514
- /* @__PURE__ */ jsx("p", { className: "text-2xl font-semibold", children: "LightBird Player" }),
2515
- /* @__PURE__ */ jsx("p", { children: "Add a local file or stream to begin." })
2621
+ !playlist.currentItem && !isLoading && !loadingMessage && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center px-6", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center text-center max-w-sm", children: [
2622
+ /* @__PURE__ */ jsx(
2623
+ "div",
2624
+ {
2625
+ "aria-hidden": true,
2626
+ className: "flex h-16 w-16 items-center justify-center rounded-full bg-accent/10 ring-1 ring-accent/30",
2627
+ children: /* @__PURE__ */ jsx(Film, { className: "h-7 w-7", style: { color: "hsl(var(--accent))" } })
2628
+ }
2629
+ ),
2630
+ /* @__PURE__ */ jsx("p", { className: "mt-5 text-lg font-medium text-foreground", children: "No video loaded" }),
2631
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Drop files anywhere, or pick a source from the playlist panel." })
2516
2632
  ] }) })
2517
2633
  ]
2518
2634
  }
@@ -2595,7 +2711,7 @@ var PlayerErrorBoundary = class extends Component {
2595
2711
  }
2596
2712
  };
2597
2713
  var ToastProvider = ToastPrimitives.Provider;
2598
- var ToastViewport = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2714
+ var ToastViewport = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2599
2715
  ToastPrimitives.Viewport,
2600
2716
  {
2601
2717
  ref,
@@ -2621,7 +2737,7 @@ var toastVariants = cva(
2621
2737
  }
2622
2738
  }
2623
2739
  );
2624
- var Toast = React10.forwardRef(({ className, variant, ...props }, ref) => {
2740
+ var Toast = React11.forwardRef(({ className, variant, ...props }, ref) => {
2625
2741
  return /* @__PURE__ */ jsx(
2626
2742
  ToastPrimitives.Root,
2627
2743
  {
@@ -2632,7 +2748,7 @@ var Toast = React10.forwardRef(({ className, variant, ...props }, ref) => {
2632
2748
  );
2633
2749
  });
2634
2750
  Toast.displayName = ToastPrimitives.Root.displayName;
2635
- var ToastAction = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2751
+ var ToastAction = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2636
2752
  ToastPrimitives.Action,
2637
2753
  {
2638
2754
  ref,
@@ -2644,7 +2760,7 @@ var ToastAction = React10.forwardRef(({ className, ...props }, ref) => /* @__PUR
2644
2760
  }
2645
2761
  ));
2646
2762
  ToastAction.displayName = ToastPrimitives.Action.displayName;
2647
- var ToastClose = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2763
+ var ToastClose = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2648
2764
  ToastPrimitives.Close,
2649
2765
  {
2650
2766
  ref,
@@ -2658,7 +2774,7 @@ var ToastClose = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE
2658
2774
  }
2659
2775
  ));
2660
2776
  ToastClose.displayName = ToastPrimitives.Close.displayName;
2661
- var ToastTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2777
+ var ToastTitle = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2662
2778
  ToastPrimitives.Title,
2663
2779
  {
2664
2780
  ref,
@@ -2667,7 +2783,7 @@ var ToastTitle = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE
2667
2783
  }
2668
2784
  ));
2669
2785
  ToastTitle.displayName = ToastPrimitives.Title.displayName;
2670
- var ToastDescription = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2786
+ var ToastDescription = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
2671
2787
  ToastPrimitives.Description,
2672
2788
  {
2673
2789
  ref,