analytica-frontend-lib 1.1.80 → 1.1.81

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.
@@ -23,9 +23,9 @@ __export(VideoPlayer_exports, {
23
23
  default: () => VideoPlayer_default
24
24
  });
25
25
  module.exports = __toCommonJS(VideoPlayer_exports);
26
- var import_react3 = require("react");
26
+ var import_react4 = require("react");
27
27
  var import_react_dom = require("react-dom");
28
- var import_phosphor_react = require("phosphor-react");
28
+ var import_phosphor_react2 = require("phosphor-react");
29
29
 
30
30
  // src/utils/utils.ts
31
31
  var import_clsx = require("clsx");
@@ -218,13 +218,197 @@ var useMobile = () => {
218
218
  };
219
219
  };
220
220
 
221
- // src/components/VideoPlayer/VideoPlayer.tsx
221
+ // src/components/DownloadButton/DownloadButton.tsx
222
+ var import_react3 = require("react");
223
+ var import_phosphor_react = require("phosphor-react");
222
224
  var import_jsx_runtime3 = require("react/jsx-runtime");
225
+ var getMimeType = (url) => {
226
+ const extension = getFileExtension(url);
227
+ const mimeTypes = {
228
+ pdf: "application/pdf",
229
+ png: "image/png",
230
+ jpg: "image/jpeg",
231
+ jpeg: "image/jpeg",
232
+ mp3: "audio/mpeg",
233
+ mp4: "video/mp4",
234
+ vtt: "text/vtt"
235
+ };
236
+ return mimeTypes[extension] || "application/octet-stream";
237
+ };
238
+ var triggerDownload = async (url, filename) => {
239
+ try {
240
+ const response = await fetch(url, {
241
+ mode: "cors",
242
+ credentials: "same-origin"
243
+ });
244
+ if (!response.ok) {
245
+ throw new Error(
246
+ `Failed to fetch file: ${response.status} ${response.statusText}`
247
+ );
248
+ }
249
+ const blob = await response.blob();
250
+ const mimeType = getMimeType(url);
251
+ const typedBlob = new Blob([blob], { type: mimeType });
252
+ const blobUrl = URL.createObjectURL(typedBlob);
253
+ const link = document.createElement("a");
254
+ link.href = blobUrl;
255
+ link.download = filename;
256
+ link.rel = "noopener noreferrer";
257
+ document.body.appendChild(link);
258
+ link.click();
259
+ link.remove();
260
+ setTimeout(() => {
261
+ URL.revokeObjectURL(blobUrl);
262
+ }, 1e3);
263
+ } catch (error) {
264
+ console.warn("Fetch download failed, falling back to direct link:", error);
265
+ const link = document.createElement("a");
266
+ link.href = url;
267
+ link.download = filename;
268
+ link.rel = "noopener noreferrer";
269
+ link.target = "_blank";
270
+ document.body.appendChild(link);
271
+ link.click();
272
+ link.remove();
273
+ }
274
+ };
275
+ var getFileExtension = (url) => {
276
+ try {
277
+ const u = new URL(url, globalThis.location?.origin || "http://localhost");
278
+ url = u.pathname;
279
+ } catch {
280
+ }
281
+ const path = url.split(/[?#]/)[0];
282
+ const dot = path.lastIndexOf(".");
283
+ return dot > -1 ? path.slice(dot + 1).toLowerCase() : "file";
284
+ };
285
+ var generateFilename = (contentType, url, lessonTitle = "aula") => {
286
+ const sanitizedTitle = lessonTitle.toLowerCase().replaceAll(/[^a-z0-9\s]/g, "").replaceAll(/\s+/g, "-").substring(0, 50);
287
+ const extension = getFileExtension(url);
288
+ return `${sanitizedTitle}-${contentType}.${extension}`;
289
+ };
290
+ var DownloadButton = ({
291
+ content,
292
+ className,
293
+ onDownloadStart,
294
+ onDownloadComplete,
295
+ onDownloadError,
296
+ lessonTitle = "aula",
297
+ disabled = false
298
+ }) => {
299
+ const [isDownloading, setIsDownloading] = (0, import_react3.useState)(false);
300
+ const isValidUrl = (0, import_react3.useCallback)((url) => {
301
+ return Boolean(
302
+ url && url.trim() !== "" && url !== "undefined" && url !== "null"
303
+ );
304
+ }, []);
305
+ const getAvailableContent = (0, import_react3.useCallback)(() => {
306
+ const downloads = [];
307
+ if (isValidUrl(content.urlDoc)) {
308
+ downloads.push({
309
+ type: "documento",
310
+ url: content.urlDoc,
311
+ label: "Documento"
312
+ });
313
+ }
314
+ if (isValidUrl(content.urlInitialFrame)) {
315
+ downloads.push({
316
+ type: "quadro-inicial",
317
+ url: content.urlInitialFrame,
318
+ label: "Quadro Inicial"
319
+ });
320
+ }
321
+ if (isValidUrl(content.urlFinalFrame)) {
322
+ downloads.push({
323
+ type: "quadro-final",
324
+ url: content.urlFinalFrame,
325
+ label: "Quadro Final"
326
+ });
327
+ }
328
+ if (isValidUrl(content.urlPodcast)) {
329
+ downloads.push({
330
+ type: "podcast",
331
+ url: content.urlPodcast,
332
+ label: "Podcast"
333
+ });
334
+ }
335
+ if (isValidUrl(content.urlVideo)) {
336
+ downloads.push({ type: "video", url: content.urlVideo, label: "V\xEDdeo" });
337
+ }
338
+ return downloads;
339
+ }, [content, isValidUrl]);
340
+ const handleDownload = (0, import_react3.useCallback)(async () => {
341
+ if (disabled || isDownloading) return;
342
+ const availableContent = getAvailableContent();
343
+ if (availableContent.length === 0) {
344
+ return;
345
+ }
346
+ setIsDownloading(true);
347
+ try {
348
+ for (let i = 0; i < availableContent.length; i++) {
349
+ const item = availableContent[i];
350
+ try {
351
+ onDownloadStart?.(item.type);
352
+ const filename = generateFilename(item.type, item.url, lessonTitle);
353
+ await triggerDownload(item.url, filename);
354
+ onDownloadComplete?.(item.type);
355
+ if (i < availableContent.length - 1) {
356
+ await new Promise((resolve) => setTimeout(resolve, 200));
357
+ }
358
+ } catch (error) {
359
+ console.error(`Erro ao baixar ${item.label}:`, error);
360
+ onDownloadError?.(
361
+ item.type,
362
+ error instanceof Error ? error : new Error(`Falha ao baixar ${item.label}`)
363
+ );
364
+ }
365
+ }
366
+ } finally {
367
+ setIsDownloading(false);
368
+ }
369
+ }, [
370
+ disabled,
371
+ isDownloading,
372
+ getAvailableContent,
373
+ lessonTitle,
374
+ onDownloadStart,
375
+ onDownloadComplete,
376
+ onDownloadError
377
+ ]);
378
+ const hasContent = getAvailableContent().length > 0;
379
+ if (!hasContent) {
380
+ return null;
381
+ }
382
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: cn("flex items-center", className), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
383
+ IconButton_default,
384
+ {
385
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.DownloadSimple, { size: 24 }),
386
+ onClick: handleDownload,
387
+ disabled: disabled || isDownloading,
388
+ "aria-label": (() => {
389
+ if (isDownloading) {
390
+ return "Baixando conte\xFAdo...";
391
+ }
392
+ const contentCount = getAvailableContent().length;
393
+ const suffix = contentCount > 1 ? "s" : "";
394
+ return `Baixar conte\xFAdo da aula (${contentCount} arquivo${suffix})`;
395
+ })(),
396
+ className: cn(
397
+ "!bg-transparent hover:!bg-black/10 transition-colors",
398
+ isDownloading && "opacity-60 cursor-not-allowed"
399
+ )
400
+ }
401
+ ) });
402
+ };
403
+ var DownloadButton_default = DownloadButton;
404
+
405
+ // src/components/VideoPlayer/VideoPlayer.tsx
406
+ var import_jsx_runtime4 = require("react/jsx-runtime");
223
407
  var CONTROLS_HIDE_TIMEOUT = 3e3;
224
408
  var LEAVE_HIDE_TIMEOUT = 1e3;
225
409
  var INIT_DELAY = 100;
226
410
  var formatTime = (seconds) => {
227
- if (!seconds || isNaN(seconds)) return "0:00";
411
+ if (!seconds || Number.isNaN(seconds)) return "0:00";
228
412
  const mins = Math.floor(seconds / 60);
229
413
  const secs = Math.floor(seconds % 60);
230
414
  return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -235,14 +419,14 @@ var ProgressBar = ({
235
419
  progressPercentage,
236
420
  onSeek,
237
421
  className = "px-4 pb-2"
238
- }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
422
+ }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
239
423
  "input",
240
424
  {
241
425
  type: "range",
242
426
  min: 0,
243
427
  max: duration || 100,
244
428
  value: currentTime,
245
- onChange: (e) => onSeek(parseFloat(e.target.value)),
429
+ onChange: (e) => onSeek(Number.parseFloat(e.target.value)),
246
430
  className: "w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500",
247
431
  "aria-label": "Video progress",
248
432
  style: {
@@ -257,24 +441,24 @@ var VolumeControls = ({
257
441
  onToggleMute,
258
442
  iconSize = 24,
259
443
  showSlider = true
260
- }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
261
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
444
+ }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2", children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
262
446
  IconButton_default,
263
447
  {
264
- icon: isMuted ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerSlash, { size: iconSize }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.SpeakerHigh, { size: iconSize }),
448
+ icon: isMuted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.SpeakerSlash, { size: iconSize }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.SpeakerHigh, { size: iconSize }),
265
449
  onClick: onToggleMute,
266
450
  "aria-label": isMuted ? "Unmute" : "Mute",
267
451
  className: "!bg-transparent !text-white hover:!bg-white/20"
268
452
  }
269
453
  ),
270
- showSlider && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
454
+ showSlider && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
271
455
  "input",
272
456
  {
273
457
  type: "range",
274
458
  min: 0,
275
459
  max: 100,
276
460
  value: Math.round(volume * 100),
277
- onChange: (e) => onVolumeChange(parseInt(e.target.value)),
461
+ onChange: (e) => onVolumeChange(Number.parseInt(e.target.value)),
278
462
  className: "w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500",
279
463
  "aria-label": "Volume control",
280
464
  style: {
@@ -292,9 +476,9 @@ var SpeedMenu = ({
292
476
  iconSize = 24,
293
477
  isTinyMobile = false
294
478
  }) => {
295
- const buttonRef = (0, import_react3.useRef)(null);
296
- const speedMenuContainerRef = (0, import_react3.useRef)(null);
297
- const speedMenuRef = (0, import_react3.useRef)(null);
479
+ const buttonRef = (0, import_react4.useRef)(null);
480
+ const speedMenuContainerRef = (0, import_react4.useRef)(null);
481
+ const speedMenuRef = (0, import_react4.useRef)(null);
298
482
  const getMenuPosition = () => {
299
483
  if (!buttonRef.current) return { top: 0, left: 0 };
300
484
  const rect = buttonRef.current.getBoundingClientRect();
@@ -308,7 +492,7 @@ var SpeedMenu = ({
308
492
  };
309
493
  };
310
494
  const position = getMenuPosition();
311
- (0, import_react3.useEffect)(() => {
495
+ (0, import_react4.useEffect)(() => {
312
496
  const handleClickOutside = (event) => {
313
497
  const target = event.target;
314
498
  const isOutsideContainer = speedMenuContainerRef.current && !speedMenuContainerRef.current.contains(target);
@@ -324,18 +508,18 @@ var SpeedMenu = ({
324
508
  document.removeEventListener("mousedown", handleClickOutside);
325
509
  };
326
510
  }, [showSpeedMenu, onToggleMenu]);
327
- const menuContent = /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
511
+ const menuContent = /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
328
512
  "div",
329
513
  {
330
514
  ref: speedMenuRef,
331
515
  role: "menu",
332
516
  "aria-label": "Playback speed",
333
517
  className: isFullscreen ? "absolute bottom-12 right-0 bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]" : "fixed bg-background border border-border-100 rounded-lg shadow-lg p-2 min-w-24 z-[9999]",
334
- style: !isFullscreen ? {
518
+ style: isFullscreen ? void 0 : {
335
519
  top: `${position.top}px`,
336
520
  left: `${position.left}px`
337
- } : void 0,
338
- children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
521
+ },
522
+ children: [0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
339
523
  "button",
340
524
  {
341
525
  role: "menuitemradio",
@@ -351,13 +535,13 @@ var SpeedMenu = ({
351
535
  ))
352
536
  }
353
537
  );
354
- const portalContent = typeof window !== "undefined" && typeof document !== "undefined" ? (0, import_react_dom.createPortal)(menuContent, document.body) : null;
355
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", ref: speedMenuContainerRef, children: [
356
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
538
+ const portalContent = showSpeedMenu && typeof window !== "undefined" && typeof document !== "undefined" && !!globalThis.document?.body ? (0, import_react_dom.createPortal)(menuContent, globalThis.document.body) : null;
539
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative", ref: speedMenuContainerRef, children: [
540
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
357
541
  IconButton_default,
358
542
  {
359
543
  ref: buttonRef,
360
- icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.DotsThreeVertical, { size: iconSize }),
544
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.DotsThreeVertical, { size: iconSize }),
361
545
  onClick: onToggleMenu,
362
546
  "aria-label": "Playback speed",
363
547
  "aria-haspopup": "menu",
@@ -380,29 +564,34 @@ var VideoPlayer = ({
380
564
  onVideoComplete,
381
565
  className,
382
566
  autoSave = true,
383
- storageKey = "video-progress"
567
+ storageKey = "video-progress",
568
+ downloadContent,
569
+ showDownloadButton = false,
570
+ onDownloadStart,
571
+ onDownloadComplete,
572
+ onDownloadError
384
573
  }) => {
385
- const videoRef = (0, import_react3.useRef)(null);
574
+ const videoRef = (0, import_react4.useRef)(null);
386
575
  const { isUltraSmallMobile, isTinyMobile } = useMobile();
387
- const [isPlaying, setIsPlaying] = (0, import_react3.useState)(false);
388
- const [currentTime, setCurrentTime] = (0, import_react3.useState)(0);
389
- const [duration, setDuration] = (0, import_react3.useState)(0);
390
- const [isMuted, setIsMuted] = (0, import_react3.useState)(false);
391
- const [volume, setVolume] = (0, import_react3.useState)(1);
392
- const [isFullscreen, setIsFullscreen] = (0, import_react3.useState)(false);
393
- const [showControls, setShowControls] = (0, import_react3.useState)(true);
394
- const [hasCompleted, setHasCompleted] = (0, import_react3.useState)(false);
395
- const [showCaptions, setShowCaptions] = (0, import_react3.useState)(false);
396
- (0, import_react3.useEffect)(() => {
576
+ const [isPlaying, setIsPlaying] = (0, import_react4.useState)(false);
577
+ const [currentTime, setCurrentTime] = (0, import_react4.useState)(0);
578
+ const [duration, setDuration] = (0, import_react4.useState)(0);
579
+ const [isMuted, setIsMuted] = (0, import_react4.useState)(false);
580
+ const [volume, setVolume] = (0, import_react4.useState)(1);
581
+ const [isFullscreen, setIsFullscreen] = (0, import_react4.useState)(false);
582
+ const [showControls, setShowControls] = (0, import_react4.useState)(true);
583
+ const [hasCompleted, setHasCompleted] = (0, import_react4.useState)(false);
584
+ const [showCaptions, setShowCaptions] = (0, import_react4.useState)(false);
585
+ (0, import_react4.useEffect)(() => {
397
586
  setHasCompleted(false);
398
587
  }, [src]);
399
- const [playbackRate, setPlaybackRate] = (0, import_react3.useState)(1);
400
- const [showSpeedMenu, setShowSpeedMenu] = (0, import_react3.useState)(false);
401
- const lastSaveTimeRef = (0, import_react3.useRef)(0);
402
- const trackRef = (0, import_react3.useRef)(null);
403
- const controlsTimeoutRef = (0, import_react3.useRef)(null);
404
- const lastMousePositionRef = (0, import_react3.useRef)({ x: 0, y: 0 });
405
- const isUserInteracting = (0, import_react3.useCallback)(() => {
588
+ const [playbackRate, setPlaybackRate] = (0, import_react4.useState)(1);
589
+ const [showSpeedMenu, setShowSpeedMenu] = (0, import_react4.useState)(false);
590
+ const lastSaveTimeRef = (0, import_react4.useRef)(0);
591
+ const trackRef = (0, import_react4.useRef)(null);
592
+ const controlsTimeoutRef = (0, import_react4.useRef)(null);
593
+ const lastMousePositionRef = (0, import_react4.useRef)({ x: 0, y: 0 });
594
+ const isUserInteracting = (0, import_react4.useCallback)(() => {
406
595
  if (showSpeedMenu) {
407
596
  return true;
408
597
  }
@@ -419,28 +608,28 @@ var VideoPlayer = ({
419
608
  }
420
609
  return false;
421
610
  }, [showSpeedMenu]);
422
- const clearControlsTimeout = (0, import_react3.useCallback)(() => {
611
+ const clearControlsTimeout = (0, import_react4.useCallback)(() => {
423
612
  if (controlsTimeoutRef.current) {
424
613
  clearTimeout(controlsTimeoutRef.current);
425
614
  controlsTimeoutRef.current = null;
426
615
  }
427
616
  }, []);
428
- const showControlsWithTimer = (0, import_react3.useCallback)(() => {
617
+ const showControlsWithTimer = (0, import_react4.useCallback)(() => {
429
618
  setShowControls(true);
430
619
  clearControlsTimeout();
431
620
  if (isFullscreen) {
432
621
  if (isPlaying) {
433
- controlsTimeoutRef.current = window.setTimeout(() => {
622
+ controlsTimeoutRef.current = globalThis.setTimeout(() => {
434
623
  setShowControls(false);
435
624
  }, CONTROLS_HIDE_TIMEOUT);
436
625
  }
437
626
  } else {
438
- controlsTimeoutRef.current = window.setTimeout(() => {
627
+ controlsTimeoutRef.current = globalThis.setTimeout(() => {
439
628
  setShowControls(false);
440
629
  }, CONTROLS_HIDE_TIMEOUT);
441
630
  }
442
631
  }, [isFullscreen, isPlaying, clearControlsTimeout]);
443
- const handleMouseMove = (0, import_react3.useCallback)(
632
+ const handleMouseMove = (0, import_react4.useCallback)(
444
633
  (event) => {
445
634
  const currentX = event.clientX;
446
635
  const currentY = event.clientY;
@@ -453,25 +642,25 @@ var VideoPlayer = ({
453
642
  },
454
643
  [showControlsWithTimer]
455
644
  );
456
- const handleMouseEnter = (0, import_react3.useCallback)(() => {
645
+ const handleMouseEnter = (0, import_react4.useCallback)(() => {
457
646
  showControlsWithTimer();
458
647
  }, [showControlsWithTimer]);
459
- const handleMouseLeave = (0, import_react3.useCallback)(() => {
648
+ const handleMouseLeave = (0, import_react4.useCallback)(() => {
460
649
  const userInteracting = isUserInteracting();
461
650
  clearControlsTimeout();
462
651
  if (!isFullscreen && !userInteracting) {
463
- controlsTimeoutRef.current = window.setTimeout(() => {
652
+ controlsTimeoutRef.current = globalThis.setTimeout(() => {
464
653
  setShowControls(false);
465
654
  }, LEAVE_HIDE_TIMEOUT);
466
655
  }
467
656
  }, [isFullscreen, clearControlsTimeout, isUserInteracting]);
468
- (0, import_react3.useEffect)(() => {
657
+ (0, import_react4.useEffect)(() => {
469
658
  if (videoRef.current) {
470
659
  videoRef.current.volume = volume;
471
660
  videoRef.current.muted = isMuted;
472
661
  }
473
662
  }, [volume, isMuted]);
474
- (0, import_react3.useEffect)(() => {
663
+ (0, import_react4.useEffect)(() => {
475
664
  const video = videoRef.current;
476
665
  if (!video) return;
477
666
  const onPlay = () => setIsPlaying(true);
@@ -486,13 +675,13 @@ var VideoPlayer = ({
486
675
  video.removeEventListener("ended", onEnded);
487
676
  };
488
677
  }, []);
489
- (0, import_react3.useEffect)(() => {
678
+ (0, import_react4.useEffect)(() => {
490
679
  const video = videoRef.current;
491
680
  if (!video) return;
492
681
  video.setAttribute("playsinline", "");
493
682
  video.setAttribute("webkit-playsinline", "");
494
683
  }, []);
495
- (0, import_react3.useEffect)(() => {
684
+ (0, import_react4.useEffect)(() => {
496
685
  if (isPlaying) {
497
686
  showControlsWithTimer();
498
687
  } else {
@@ -504,7 +693,7 @@ var VideoPlayer = ({
504
693
  }
505
694
  }
506
695
  }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);
507
- (0, import_react3.useEffect)(() => {
696
+ (0, import_react4.useEffect)(() => {
508
697
  const handleFullscreenChange = () => {
509
698
  const isCurrentlyFullscreen = !!document.fullscreenElement;
510
699
  setIsFullscreen(isCurrentlyFullscreen);
@@ -517,14 +706,19 @@ var VideoPlayer = ({
517
706
  document.removeEventListener("fullscreenchange", handleFullscreenChange);
518
707
  };
519
708
  }, [showControlsWithTimer]);
520
- (0, import_react3.useEffect)(() => {
709
+ (0, import_react4.useEffect)(() => {
521
710
  const init = () => {
522
711
  if (!isFullscreen) {
523
712
  showControlsWithTimer();
524
713
  }
525
714
  };
526
715
  let raf1 = 0, raf2 = 0, tid;
527
- if (typeof window.requestAnimationFrame === "function") {
716
+ if (globalThis.requestAnimationFrame === void 0) {
717
+ tid = globalThis.setTimeout(init, INIT_DELAY);
718
+ return () => {
719
+ if (tid) clearTimeout(tid);
720
+ };
721
+ } else {
528
722
  raf1 = requestAnimationFrame(() => {
529
723
  raf2 = requestAnimationFrame(init);
530
724
  });
@@ -532,32 +726,29 @@ var VideoPlayer = ({
532
726
  cancelAnimationFrame(raf1);
533
727
  cancelAnimationFrame(raf2);
534
728
  };
535
- } else {
536
- tid = window.setTimeout(init, INIT_DELAY);
537
- return () => {
538
- if (tid) clearTimeout(tid);
539
- };
540
729
  }
541
730
  }, []);
542
- const getInitialTime = (0, import_react3.useCallback)(() => {
731
+ const getInitialTime = (0, import_react4.useCallback)(() => {
543
732
  if (!autoSave || !storageKey) {
544
733
  return Number.isFinite(initialTime) && initialTime >= 0 ? initialTime : void 0;
545
734
  }
546
- const saved = Number(localStorage.getItem(`${storageKey}-${src}`) || NaN);
735
+ const saved = Number(
736
+ localStorage.getItem(`${storageKey}-${src}`) || Number.NaN
737
+ );
547
738
  const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;
548
739
  const hasValidSaved = Number.isFinite(saved) && saved >= 0;
549
740
  if (hasValidInitial) return initialTime;
550
741
  if (hasValidSaved) return saved;
551
742
  return void 0;
552
743
  }, [autoSave, storageKey, src, initialTime]);
553
- (0, import_react3.useEffect)(() => {
744
+ (0, import_react4.useEffect)(() => {
554
745
  const start = getInitialTime();
555
746
  if (start !== void 0 && videoRef.current) {
556
747
  videoRef.current.currentTime = start;
557
748
  setCurrentTime(start);
558
749
  }
559
750
  }, [getInitialTime]);
560
- const saveProgress = (0, import_react3.useCallback)(
751
+ const saveProgress = (0, import_react4.useCallback)(
561
752
  (time) => {
562
753
  if (!autoSave || !storageKey) return;
563
754
  const now = Date.now();
@@ -568,7 +759,7 @@ var VideoPlayer = ({
568
759
  },
569
760
  [autoSave, storageKey, src]
570
761
  );
571
- const togglePlayPause = (0, import_react3.useCallback)(async () => {
762
+ const togglePlayPause = (0, import_react4.useCallback)(async () => {
572
763
  const video = videoRef.current;
573
764
  if (!video) return;
574
765
  if (!video.paused) {
@@ -580,7 +771,7 @@ var VideoPlayer = ({
580
771
  } catch {
581
772
  }
582
773
  }, []);
583
- const handleVolumeChange = (0, import_react3.useCallback)(
774
+ const handleVolumeChange = (0, import_react4.useCallback)(
584
775
  (newVolume) => {
585
776
  const video = videoRef.current;
586
777
  if (!video) return;
@@ -599,7 +790,7 @@ var VideoPlayer = ({
599
790
  },
600
791
  [isMuted]
601
792
  );
602
- const toggleMute = (0, import_react3.useCallback)(() => {
793
+ const toggleMute = (0, import_react4.useCallback)(() => {
603
794
  const video = videoRef.current;
604
795
  if (!video) return;
605
796
  if (isMuted) {
@@ -613,13 +804,13 @@ var VideoPlayer = ({
613
804
  setIsMuted(true);
614
805
  }
615
806
  }, [isMuted, volume]);
616
- const handleSeek = (0, import_react3.useCallback)((newTime) => {
807
+ const handleSeek = (0, import_react4.useCallback)((newTime) => {
617
808
  const video = videoRef.current;
618
809
  if (video) {
619
810
  video.currentTime = newTime;
620
811
  }
621
812
  }, []);
622
- const toggleFullscreen = (0, import_react3.useCallback)(() => {
813
+ const toggleFullscreen = (0, import_react4.useCallback)(() => {
623
814
  const container = videoRef.current?.parentElement;
624
815
  if (!container) return;
625
816
  if (!isFullscreen && container.requestFullscreen) {
@@ -628,23 +819,23 @@ var VideoPlayer = ({
628
819
  document.exitFullscreen();
629
820
  }
630
821
  }, [isFullscreen]);
631
- const handleSpeedChange = (0, import_react3.useCallback)((speed) => {
822
+ const handleSpeedChange = (0, import_react4.useCallback)((speed) => {
632
823
  if (videoRef.current) {
633
824
  videoRef.current.playbackRate = speed;
634
825
  setPlaybackRate(speed);
635
826
  setShowSpeedMenu(false);
636
827
  }
637
828
  }, []);
638
- const toggleSpeedMenu = (0, import_react3.useCallback)(() => {
829
+ const toggleSpeedMenu = (0, import_react4.useCallback)(() => {
639
830
  setShowSpeedMenu(!showSpeedMenu);
640
831
  }, [showSpeedMenu]);
641
- const toggleCaptions = (0, import_react3.useCallback)(() => {
832
+ const toggleCaptions = (0, import_react4.useCallback)(() => {
642
833
  if (!trackRef.current?.track || !subtitles) return;
643
834
  const newShowCaptions = !showCaptions;
644
835
  setShowCaptions(newShowCaptions);
645
836
  trackRef.current.track.mode = newShowCaptions && subtitles ? "showing" : "hidden";
646
837
  }, [showCaptions, subtitles]);
647
- const checkVideoCompletion = (0, import_react3.useCallback)(
838
+ const checkVideoCompletion = (0, import_react4.useCallback)(
648
839
  (progressPercent) => {
649
840
  if (progressPercent >= 95 && !hasCompleted) {
650
841
  setHasCompleted(true);
@@ -653,7 +844,7 @@ var VideoPlayer = ({
653
844
  },
654
845
  [hasCompleted, onVideoComplete]
655
846
  );
656
- const handleTimeUpdate = (0, import_react3.useCallback)(() => {
847
+ const handleTimeUpdate = (0, import_react4.useCallback)(() => {
657
848
  const video = videoRef.current;
658
849
  if (!video) return;
659
850
  const current = video.currentTime;
@@ -666,17 +857,17 @@ var VideoPlayer = ({
666
857
  checkVideoCompletion(progressPercent);
667
858
  }
668
859
  }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);
669
- const handleLoadedMetadata = (0, import_react3.useCallback)(() => {
860
+ const handleLoadedMetadata = (0, import_react4.useCallback)(() => {
670
861
  if (videoRef.current) {
671
862
  setDuration(videoRef.current.duration);
672
863
  }
673
864
  }, []);
674
- (0, import_react3.useEffect)(() => {
865
+ (0, import_react4.useEffect)(() => {
675
866
  if (trackRef.current?.track) {
676
867
  trackRef.current.track.mode = showCaptions && subtitles ? "showing" : "hidden";
677
868
  }
678
869
  }, [subtitles, showCaptions]);
679
- (0, import_react3.useEffect)(() => {
870
+ (0, import_react4.useEffect)(() => {
680
871
  const handleVisibilityChange = () => {
681
872
  if (document.hidden && isPlaying && videoRef.current) {
682
873
  videoRef.current.pause();
@@ -690,46 +881,46 @@ var VideoPlayer = ({
690
881
  }
691
882
  };
692
883
  document.addEventListener("visibilitychange", handleVisibilityChange);
693
- window.addEventListener("blur", handleBlur);
884
+ globalThis.addEventListener("blur", handleBlur);
694
885
  return () => {
695
886
  document.removeEventListener("visibilitychange", handleVisibilityChange);
696
- window.removeEventListener("blur", handleBlur);
887
+ globalThis.removeEventListener("blur", handleBlur);
697
888
  clearControlsTimeout();
698
889
  };
699
890
  }, [isPlaying, clearControlsTimeout]);
700
891
  const progressPercentage = duration > 0 ? currentTime / duration * 100 : 0;
701
- const getIconSize = (0, import_react3.useCallback)(() => {
892
+ const getIconSize = (0, import_react4.useCallback)(() => {
702
893
  if (isTinyMobile) return 18;
703
894
  if (isUltraSmallMobile) return 20;
704
895
  return 24;
705
896
  }, [isTinyMobile, isUltraSmallMobile]);
706
- const getControlsPadding = (0, import_react3.useCallback)(() => {
897
+ const getControlsPadding = (0, import_react4.useCallback)(() => {
707
898
  if (isTinyMobile) return "px-2 pb-2 pt-1";
708
899
  if (isUltraSmallMobile) return "px-3 pb-3 pt-1";
709
900
  return "px-4 pb-4";
710
901
  }, [isTinyMobile, isUltraSmallMobile]);
711
- const getControlsGap = (0, import_react3.useCallback)(() => {
902
+ const getControlsGap = (0, import_react4.useCallback)(() => {
712
903
  if (isTinyMobile) return "gap-1";
713
904
  if (isUltraSmallMobile) return "gap-2";
714
905
  return "gap-4";
715
906
  }, [isTinyMobile, isUltraSmallMobile]);
716
- const getProgressBarPadding = (0, import_react3.useCallback)(() => {
907
+ const getProgressBarPadding = (0, import_react4.useCallback)(() => {
717
908
  if (isTinyMobile) return "px-2 pb-1";
718
909
  if (isUltraSmallMobile) return "px-3 pb-1";
719
910
  return "px-4 pb-2";
720
911
  }, [isTinyMobile, isUltraSmallMobile]);
721
- const getCenterPlayButtonPosition = (0, import_react3.useCallback)(() => {
912
+ const getCenterPlayButtonPosition = (0, import_react4.useCallback)(() => {
722
913
  if (isTinyMobile) return "items-center justify-center -translate-y-12";
723
914
  if (isUltraSmallMobile) return "items-center justify-center -translate-y-8";
724
915
  return "items-center justify-center";
725
916
  }, [isTinyMobile, isUltraSmallMobile]);
726
- const getTopControlsOpacity = (0, import_react3.useCallback)(() => {
917
+ const getTopControlsOpacity = (0, import_react4.useCallback)(() => {
727
918
  return showControls ? "opacity-100" : "opacity-0";
728
919
  }, [showControls]);
729
- const getBottomControlsOpacity = (0, import_react3.useCallback)(() => {
920
+ const getBottomControlsOpacity = (0, import_react4.useCallback)(() => {
730
921
  return showControls ? "opacity-100" : "opacity-0";
731
922
  }, [showControls]);
732
- const handleVideoKeyDown = (0, import_react3.useCallback)(
923
+ const handleVideoKeyDown = (0, import_react4.useCallback)(
733
924
  (e) => {
734
925
  if (e.key) {
735
926
  e.stopPropagation();
@@ -784,32 +975,45 @@ var VideoPlayer = ({
784
975
  toggleFullscreen
785
976
  ]
786
977
  );
787
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex flex-col", className), children: [
788
- (title || subtitleText) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-1", children: [
789
- title && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
790
- Text_default,
791
- {
792
- as: "h2",
793
- size: "lg",
794
- weight: "bold",
795
- color: "text-text-900",
796
- className: "leading-5 tracking-wide",
797
- children: title
798
- }
799
- ),
800
- subtitleText && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
801
- Text_default,
978
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("flex flex-col", className), children: [
979
+ (title || subtitleText) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20", children: [
980
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1", children: [
981
+ title && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
982
+ Text_default,
983
+ {
984
+ as: "h2",
985
+ size: "lg",
986
+ weight: "bold",
987
+ color: "text-text-900",
988
+ className: "leading-5 tracking-wide",
989
+ children: title
990
+ }
991
+ ),
992
+ subtitleText && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
993
+ Text_default,
994
+ {
995
+ as: "p",
996
+ size: "sm",
997
+ weight: "normal",
998
+ color: "text-text-600",
999
+ className: "leading-5",
1000
+ children: subtitleText
1001
+ }
1002
+ )
1003
+ ] }),
1004
+ showDownloadButton && downloadContent && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1005
+ DownloadButton_default,
802
1006
  {
803
- as: "p",
804
- size: "sm",
805
- weight: "normal",
806
- color: "text-text-600",
807
- className: "leading-5",
808
- children: subtitleText
1007
+ content: downloadContent,
1008
+ lessonTitle: title,
1009
+ onDownloadStart,
1010
+ onDownloadComplete,
1011
+ onDownloadError,
1012
+ className: "flex-shrink-0"
809
1013
  }
810
1014
  )
811
- ] }) }),
812
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1015
+ ] }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
813
1017
  "section",
814
1018
  {
815
1019
  className: cn(
@@ -824,7 +1028,7 @@ var VideoPlayer = ({
824
1028
  onTouchStart: handleMouseEnter,
825
1029
  onMouseLeave: handleMouseLeave,
826
1030
  children: [
827
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1031
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
828
1032
  "video",
829
1033
  {
830
1034
  ref: videoRef,
@@ -839,7 +1043,7 @@ var VideoPlayer = ({
839
1043
  onKeyDown: handleVideoKeyDown,
840
1044
  tabIndex: 0,
841
1045
  "aria-label": title ? `Video: ${title}` : "Video player",
842
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1046
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
843
1047
  "track",
844
1048
  {
845
1049
  ref: trackRef,
@@ -852,17 +1056,17 @@ var VideoPlayer = ({
852
1056
  )
853
1057
  }
854
1058
  ),
855
- !isPlaying && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1059
+ !isPlaying && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
856
1060
  "div",
857
1061
  {
858
1062
  className: cn(
859
1063
  "absolute inset-0 flex bg-black/30 transition-opacity",
860
1064
  getCenterPlayButtonPosition()
861
1065
  ),
862
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1066
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
863
1067
  IconButton_default,
864
1068
  {
865
- icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.Play, { size: 32, weight: "regular", className: "ml-1" }),
1069
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Play, { size: 32, weight: "regular", className: "ml-1" }),
866
1070
  onClick: togglePlayPause,
867
1071
  "aria-label": "Play video",
868
1072
  className: "!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200"
@@ -870,17 +1074,17 @@ var VideoPlayer = ({
870
1074
  )
871
1075
  }
872
1076
  ),
873
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1077
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
874
1078
  "div",
875
1079
  {
876
1080
  className: cn(
877
1081
  "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity",
878
1082
  getTopControlsOpacity()
879
1083
  ),
880
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1084
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
881
1085
  IconButton_default,
882
1086
  {
883
- icon: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.ArrowsInSimple, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.ArrowsOutSimple, { size: 24 }),
1087
+ icon: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ArrowsInSimple, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ArrowsOutSimple, { size: 24 }),
884
1088
  onClick: toggleFullscreen,
885
1089
  "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
886
1090
  className: "!bg-transparent !text-white hover:!bg-white/20"
@@ -888,7 +1092,7 @@ var VideoPlayer = ({
888
1092
  ) })
889
1093
  }
890
1094
  ),
891
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
892
1096
  "div",
893
1097
  {
894
1098
  className: cn(
@@ -896,7 +1100,7 @@ var VideoPlayer = ({
896
1100
  getBottomControlsOpacity()
897
1101
  ),
898
1102
  children: [
899
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1103
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
900
1104
  ProgressBar,
901
1105
  {
902
1106
  currentTime,
@@ -906,7 +1110,7 @@ var VideoPlayer = ({
906
1110
  className: getProgressBarPadding()
907
1111
  }
908
1112
  ),
909
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1113
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
910
1114
  "div",
911
1115
  {
912
1116
  className: cn(
@@ -914,17 +1118,17 @@ var VideoPlayer = ({
914
1118
  getControlsPadding()
915
1119
  ),
916
1120
  children: [
917
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex items-center", getControlsGap()), children: [
918
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1121
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("flex items-center", getControlsGap()), children: [
1122
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
919
1123
  IconButton_default,
920
1124
  {
921
- icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.Pause, { size: getIconSize() }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.Play, { size: getIconSize() }),
1125
+ icon: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Pause, { size: getIconSize() }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.Play, { size: getIconSize() }),
922
1126
  onClick: togglePlayPause,
923
1127
  "aria-label": isPlaying ? "Pause" : "Play",
924
1128
  className: "!bg-transparent !text-white hover:!bg-white/20"
925
1129
  }
926
1130
  ),
927
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1131
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
928
1132
  VolumeControls,
929
1133
  {
930
1134
  volume,
@@ -935,10 +1139,10 @@ var VideoPlayer = ({
935
1139
  showSlider: !isUltraSmallMobile
936
1140
  }
937
1141
  ),
938
- subtitles && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1142
+ subtitles && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
939
1143
  IconButton_default,
940
1144
  {
941
- icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_phosphor_react.ClosedCaptioning, { size: getIconSize() }),
1145
+ icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_phosphor_react2.ClosedCaptioning, { size: getIconSize() }),
942
1146
  onClick: toggleCaptions,
943
1147
  "aria-label": showCaptions ? "Hide captions" : "Show captions",
944
1148
  className: cn(
@@ -947,13 +1151,13 @@ var VideoPlayer = ({
947
1151
  )
948
1152
  }
949
1153
  ),
950
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text_default, { size: "sm", weight: "medium", color: "text-white", children: [
1154
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text_default, { size: "sm", weight: "medium", color: "text-white", children: [
951
1155
  formatTime(currentTime),
952
1156
  " / ",
953
1157
  formatTime(duration)
954
1158
  ] })
955
1159
  ] }),
956
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1160
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
957
1161
  SpeedMenu,
958
1162
  {
959
1163
  showSpeedMenu,