@geekapps/silo-elements-nextjs 0.1.20 → 0.1.22

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.
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
2
2
  import gsap from 'gsap';
3
- import { Play, Pause, FastForward, VolumeX, Volume2, AudioLines, Captions, Settings, Minimize, Maximize } from 'lucide-react';
3
+ import { Play, Rewind, Pause, FastForward, VolumeX, Volume2, Settings, Minimize, Maximize } from 'lucide-react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  var AUTO_QUALITY = {
@@ -8,6 +8,7 @@ var AUTO_QUALITY = {
8
8
  label: "Auto",
9
9
  type: "auto"
10
10
  };
11
+ var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
11
12
  function Sources(_props) {
12
13
  return null;
13
14
  }
@@ -58,7 +59,9 @@ function Video({
58
59
  const [selectedQuality, setSelectedQuality] = useState("auto");
59
60
  const [audioTracks, setAudioTracks] = useState([]);
60
61
  const [selectedAudio, setSelectedAudio] = useState(0);
61
- const [openMenu, setOpenMenu] = useState(null);
62
+ const [settingsOpen, setSettingsOpen] = useState(false);
63
+ const [settingsTab, setSettingsTab] = useState("quality");
64
+ const [playbackRate, setPlaybackRate] = useState(1);
62
65
  const [subtitleMode, setSubtitleMode] = useState(initialSubtitleMode);
63
66
  const [activeCue, setActiveCue] = useState(null);
64
67
  const [storyboardCues, setStoryboardCues] = useState(
@@ -70,6 +73,8 @@ function Video({
70
73
  const [bufferedTime, setBufferedTime] = useState(0);
71
74
  const [isPlaying, setIsPlaying] = useState(false);
72
75
  const [hasPlayed, setHasPlayed] = useState(false);
76
+ const [clickIcon, setClickIcon] = useState(null);
77
+ const clickIconTimerRef = useRef(null);
73
78
  const [isLoading, setIsLoading] = useState(true);
74
79
  const [controlsVisible, setControlsVisible] = useState(true);
75
80
  const [volume, setVolume] = useState(defaultVolume);
@@ -93,6 +98,7 @@ function Video({
93
98
  const video = videoRef.current;
94
99
  if (!video) return;
95
100
  Array.from(video.textTracks).forEach((track) => {
101
+ if (track.kind === "metadata") return;
96
102
  track.mode = mode !== "off" && track.language === mode ? "hidden" : "disabled";
97
103
  });
98
104
  if (mode === "off") setActiveCue(null);
@@ -195,6 +201,11 @@ function Video({
195
201
  applySubtitleMode(subtitleMode);
196
202
  if (subtitleMode === "off") setActiveCue(null);
197
203
  }, [subtitleMode, applySubtitleMode]);
204
+ useEffect(() => {
205
+ const video = videoRef.current;
206
+ if (!video) return;
207
+ video.playbackRate = playbackRate;
208
+ }, [playbackRate]);
198
209
  const subtitleModeRef = useRef(subtitleMode);
199
210
  useEffect(() => {
200
211
  subtitleModeRef.current = subtitleMode;
@@ -208,7 +219,7 @@ function Video({
208
219
  setActiveCue(null);
209
220
  return;
210
221
  }
211
- const track = Array.from(video.textTracks).find((t) => t.language === mode);
222
+ const track = Array.from(video.textTracks).find((t) => t.language === mode && t.kind !== "metadata");
212
223
  if (!track || !track.activeCues || track.activeCues.length === 0) {
213
224
  setActiveCue(null);
214
225
  return;
@@ -218,6 +229,7 @@ function Video({
218
229
  };
219
230
  const bindTracks = () => {
220
231
  Array.from(video.textTracks).forEach((t) => {
232
+ if (t.kind === "metadata") return;
221
233
  t.removeEventListener("cuechange", onCueChange);
222
234
  t.addEventListener("cuechange", onCueChange);
223
235
  });
@@ -230,12 +242,22 @@ function Video({
230
242
  };
231
243
  }, []);
232
244
  useEffect(() => {
245
+ const video = videoRef.current;
233
246
  const onFullscreenChange = () => {
234
- setIsFullscreen(Boolean(document.fullscreenElement));
247
+ const doc = document;
248
+ const nativeFullscreen = Boolean(document.fullscreenElement || doc.webkitFullscreenElement);
249
+ const iosVideoFullscreen = Boolean(video?.webkitDisplayingFullscreen);
250
+ setIsFullscreen(nativeFullscreen || iosVideoFullscreen);
235
251
  };
236
252
  document.addEventListener("fullscreenchange", onFullscreenChange);
253
+ document.addEventListener("webkitfullscreenchange", onFullscreenChange);
254
+ video?.addEventListener("webkitbeginfullscreen", onFullscreenChange);
255
+ video?.addEventListener("webkitendfullscreen", onFullscreenChange);
237
256
  return () => {
238
257
  document.removeEventListener("fullscreenchange", onFullscreenChange);
258
+ document.removeEventListener("webkitfullscreenchange", onFullscreenChange);
259
+ video?.removeEventListener("webkitbeginfullscreen", onFullscreenChange);
260
+ video?.removeEventListener("webkitendfullscreen", onFullscreenChange);
239
261
  };
240
262
  }, []);
241
263
  useEffect(() => {
@@ -301,7 +323,7 @@ function Video({
301
323
  setQualities([AUTO_QUALITY]);
302
324
  setAudioTracks([]);
303
325
  setSelectedAudio(0);
304
- setOpenMenu(null);
326
+ setSettingsOpen(false);
305
327
  video.pause();
306
328
  video.removeAttribute("src");
307
329
  video.load();
@@ -443,11 +465,15 @@ function Video({
443
465
  const video = videoRef.current;
444
466
  if (!video) return;
445
467
  try {
446
- if (video.paused) {
468
+ const wasPaused = video.paused;
469
+ if (wasPaused) {
447
470
  await video.play();
448
471
  } else {
449
472
  video.pause();
450
473
  }
474
+ if (clickIconTimerRef.current) window.clearTimeout(clickIconTimerRef.current);
475
+ setClickIcon(wasPaused ? "play" : "pause");
476
+ clickIconTimerRef.current = window.setTimeout(() => setClickIcon(null), 600);
451
477
  } catch {
452
478
  setError("O navegador bloqueou a reprodu\xE7\xE3o autom\xE1tica.");
453
479
  }
@@ -462,20 +488,51 @@ function Video({
462
488
  }, []);
463
489
  const toggleFullscreen = useCallback(async () => {
464
490
  const player = playerRef.current;
465
- if (!player) return;
491
+ const video = videoRef.current;
492
+ const doc = document;
493
+ if (!player && !video) return;
466
494
  try {
467
- if (!document.fullscreenElement) {
495
+ const nativeFullscreen = Boolean(document.fullscreenElement || doc.webkitFullscreenElement);
496
+ const iosVideoFullscreen = Boolean(video?.webkitDisplayingFullscreen);
497
+ if (nativeFullscreen || iosVideoFullscreen) {
498
+ if (document.fullscreenElement && document.exitFullscreen) {
499
+ await document.exitFullscreen();
500
+ return;
501
+ }
502
+ if (doc.webkitExitFullscreen) {
503
+ await doc.webkitExitFullscreen();
504
+ return;
505
+ }
506
+ if (video?.webkitExitFullscreen) {
507
+ video.webkitExitFullscreen();
508
+ return;
509
+ }
510
+ return;
511
+ }
512
+ if (player?.requestFullscreen) {
468
513
  await player.requestFullscreen();
469
- } else {
470
- await document.exitFullscreen();
514
+ return;
515
+ }
516
+ if (player?.webkitRequestFullscreen) {
517
+ await player.webkitRequestFullscreen();
518
+ return;
519
+ }
520
+ if (video?.webkitEnterFullscreen) {
521
+ video.webkitEnterFullscreen();
522
+ return;
523
+ }
524
+ if (video?.requestFullscreen) {
525
+ await video.requestFullscreen();
526
+ return;
471
527
  }
528
+ throw new Error("Fullscreen unavailable");
472
529
  } catch {
473
530
  setError("N\xE3o foi poss\xEDvel alterar o modo fullscreen.");
474
531
  }
475
532
  }, []);
476
533
  const changeAudio = useCallback((trackId) => {
477
534
  setSelectedAudio(trackId);
478
- setOpenMenu(null);
535
+ setSettingsOpen(false);
479
536
  if (hlsRef.current) {
480
537
  hlsRef.current.audioTrack = trackId;
481
538
  }
@@ -485,7 +542,7 @@ function Video({
485
542
  const option = qualities.find((quality) => quality.id === qualityId);
486
543
  if (!option) return;
487
544
  setSelectedQuality(qualityId);
488
- setOpenMenu(null);
545
+ setSettingsOpen(false);
489
546
  if (option.type === "auto") {
490
547
  if (hlsRef.current) {
491
548
  hlsRef.current.currentLevel = -1;
@@ -615,7 +672,7 @@ function Video({
615
672
  className: "relative w-full overflow-hidden rounded-[14px] bg-black shadow-[0_30px_90px_rgba(15,15,15,0.22)] outline-none ring-1 ring-black/5",
616
673
  style: maxHeight ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight, aspectRatio: "16/9" } : { aspectRatio: "16/9" },
617
674
  children: [
618
- /* @__PURE__ */ jsx(
675
+ /* @__PURE__ */ jsxs(
619
676
  "video",
620
677
  {
621
678
  ref: videoRef,
@@ -623,17 +680,27 @@ function Video({
623
680
  playsInline: true,
624
681
  preload: "metadata",
625
682
  crossOrigin: "anonymous",
626
- children: parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
627
- "track",
628
- {
629
- kind: "subtitles",
630
- src: subtitle.src,
631
- srcLang: subtitle.srclang,
632
- label: subtitle.label,
633
- default: subtitle.default
634
- },
635
- `${activeSource.src}-${subtitle.srclang}`
636
- ))
683
+ children: [
684
+ parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
685
+ "track",
686
+ {
687
+ kind: "subtitles",
688
+ src: subtitle.src,
689
+ srcLang: subtitle.srclang,
690
+ label: subtitle.label,
691
+ default: subtitle.default
692
+ },
693
+ `${activeSource.src}-${subtitle.srclang}`
694
+ )),
695
+ parsed.storyboard?.src && /* @__PURE__ */ jsx(
696
+ "track",
697
+ {
698
+ kind: "metadata",
699
+ label: "thumbnails",
700
+ src: parsed.storyboard.src
701
+ }
702
+ )
703
+ ]
637
704
  }
638
705
  ),
639
706
  poster && !hasPlayed && /* @__PURE__ */ jsx(
@@ -641,7 +708,7 @@ function Video({
641
708
  {
642
709
  src: poster,
643
710
  "aria-hidden": true,
644
- className: "pointer-events-none absolute inset-0 h-full w-full object-cover"
711
+ className: "pointer-events-none absolute inset-0 h-full w-full object-contain bg-black"
645
712
  }
646
713
  ),
647
714
  /* @__PURE__ */ jsx(
@@ -679,6 +746,81 @@ function Video({
679
746
  )
680
747
  ] }),
681
748
  /* @__PURE__ */ jsxs("footer", { onClick: (e) => e.stopPropagation(), className: "relative z-10 px-3 pb-3 text-white @sm:px-5 @sm:pb-5 @lg:px-9 @lg:pb-8", children: [
749
+ settingsOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
750
+ /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-40", onClick: () => setSettingsOpen(false) }),
751
+ /* @__PURE__ */ jsxs("div", { className: "absolute bottom-full left-0 right-0 z-50 mb-2 mx-0 overflow-hidden rounded-xl border border-white/10 bg-black/90 shadow-2xl backdrop-blur-xl", children: [
752
+ /* @__PURE__ */ jsx("div", { className: "flex border-b border-white/10", children: ["quality", "subtitles", ...audioTracks.length > 1 ? ["audio"] : [], "playback"].map((tab) => /* @__PURE__ */ jsx(
753
+ "button",
754
+ {
755
+ type: "button",
756
+ onClick: () => setSettingsTab(tab),
757
+ className: `flex-1 px-3 py-2.5 text-xs font-semibold capitalize transition ${settingsTab === tab ? "text-white border-b-2 border-white -mb-px" : "text-white/50 hover:text-white/80"}`,
758
+ children: tab
759
+ },
760
+ tab
761
+ )) }),
762
+ /* @__PURE__ */ jsxs("div", { className: "max-h-48 overflow-y-auto py-1", children: [
763
+ settingsTab === "quality" && /* @__PURE__ */ jsx(Fragment, { children: [...qualities].reverse().map((quality) => /* @__PURE__ */ jsxs(
764
+ SettingsItem,
765
+ {
766
+ active: selectedQuality === quality.id,
767
+ onClick: () => changeQuality(quality.id),
768
+ children: [
769
+ quality.label,
770
+ quality.id === "auto" && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] text-white/40", children: "ABR" })
771
+ ]
772
+ },
773
+ quality.id
774
+ )) }),
775
+ settingsTab === "subtitles" && /* @__PURE__ */ jsxs(Fragment, { children: [
776
+ /* @__PURE__ */ jsx(
777
+ SettingsItem,
778
+ {
779
+ active: subtitleMode === "off",
780
+ onClick: () => {
781
+ setSubtitleMode("off");
782
+ setSettingsOpen(false);
783
+ },
784
+ children: "Off"
785
+ }
786
+ ),
787
+ parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
788
+ SettingsItem,
789
+ {
790
+ active: subtitleMode === subtitle.srclang,
791
+ onClick: () => {
792
+ setSubtitleMode(subtitle.srclang);
793
+ setSettingsOpen(false);
794
+ },
795
+ children: subtitle.label
796
+ },
797
+ subtitle.srclang
798
+ ))
799
+ ] }),
800
+ settingsTab === "audio" && audioTracks.length > 1 && /* @__PURE__ */ jsx(Fragment, { children: audioTracks.map((track) => /* @__PURE__ */ jsx(
801
+ SettingsItem,
802
+ {
803
+ active: selectedAudio === track.id,
804
+ onClick: () => changeAudio(track.id),
805
+ children: track.label
806
+ },
807
+ track.id
808
+ )) }),
809
+ settingsTab === "playback" && /* @__PURE__ */ jsx(Fragment, { children: PLAYBACK_SPEEDS.map((speed) => /* @__PURE__ */ jsx(
810
+ SettingsItem,
811
+ {
812
+ active: playbackRate === speed,
813
+ onClick: () => {
814
+ setPlaybackRate(speed);
815
+ setSettingsOpen(false);
816
+ },
817
+ children: speed === 1 ? "Normal" : `${speed}x`
818
+ },
819
+ speed
820
+ )) })
821
+ ] })
822
+ ] })
823
+ ] }),
682
824
  /* @__PURE__ */ jsxs(
683
825
  "div",
684
826
  {
@@ -733,6 +875,16 @@ function Video({
733
875
  ),
734
876
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 @sm:gap-4 @lg:gap-5", children: [
735
877
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 @sm:gap-4 @lg:gap-5", children: [
878
+ /* @__PURE__ */ jsx(
879
+ "button",
880
+ {
881
+ type: "button",
882
+ onClick: () => seekRelative(-10),
883
+ className: "grid size-6 place-items-center text-white transition hover:scale-105 hover:text-white/80 @sm:size-8",
884
+ "aria-label": "Rewind 10 seconds",
885
+ children: /* @__PURE__ */ jsx(Rewind, { className: "size-5 @sm:size-7" })
886
+ }
887
+ ),
736
888
  /* @__PURE__ */ jsx(
737
889
  "button",
738
890
  {
@@ -748,7 +900,7 @@ function Video({
748
900
  {
749
901
  type: "button",
750
902
  onClick: () => seekRelative(10),
751
- className: "hidden size-6 place-items-center text-white transition hover:scale-105 hover:text-white/80 @sm:grid @sm:size-8",
903
+ className: "grid size-6 place-items-center text-white transition hover:scale-105 hover:text-white/80 @sm:size-8",
752
904
  "aria-label": "Forward 10 seconds",
753
905
  children: /* @__PURE__ */ jsx(FastForward, { className: "size-5 @sm:size-7" })
754
906
  }
@@ -788,91 +940,19 @@ function Video({
788
940
  }
789
941
  ),
790
942
  /* @__PURE__ */ jsx("div", { className: "mx-0.5 hidden h-4 w-px bg-white/20 @md:mx-1 @md:block" }),
791
- audioTracks.length > 1 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
792
- /* @__PURE__ */ jsx(
793
- "button",
794
- {
795
- type: "button",
796
- onClick: () => setOpenMenu(openMenu === "audio" ? null : "audio"),
797
- className: `grid size-6 place-items-center rounded transition hover:text-white/80 @sm:size-8 ${openMenu === "audio" ? "text-white" : "text-white/60"}`,
798
- "aria-label": "Audio track",
799
- children: /* @__PURE__ */ jsx(AudioLines, { className: "size-4 @sm:size-5" })
800
- }
801
- ),
802
- openMenu === "audio" && /* @__PURE__ */ jsx(PopoverMenu, { onClose: () => setOpenMenu(null), children: audioTracks.map((track) => /* @__PURE__ */ jsx(
803
- PopoverItem,
804
- {
805
- active: selectedAudio === track.id,
806
- onClick: () => changeAudio(track.id),
807
- children: track.label
808
- },
809
- track.id
810
- )) })
811
- ] }),
812
- parsed.subtitles.length > 0 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
813
- /* @__PURE__ */ jsx(
814
- "button",
815
- {
816
- type: "button",
817
- onClick: () => setOpenMenu(openMenu === "captions" ? null : "captions"),
818
- className: `grid size-6 place-items-center rounded transition hover:text-white/80 @sm:size-8 ${subtitleMode !== "off" || openMenu === "captions" ? "text-white" : "text-white/60"}`,
819
- "aria-label": "Captions",
820
- children: /* @__PURE__ */ jsx(Captions, { className: "size-4 @sm:size-5" })
821
- }
822
- ),
823
- openMenu === "captions" && /* @__PURE__ */ jsxs(PopoverMenu, { onClose: () => setOpenMenu(null), children: [
824
- /* @__PURE__ */ jsx(
825
- PopoverItem,
826
- {
827
- active: subtitleMode === "off",
828
- onClick: () => {
829
- setSubtitleMode("off");
830
- setOpenMenu(null);
831
- },
832
- children: "Off"
833
- }
834
- ),
835
- parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
836
- PopoverItem,
837
- {
838
- active: subtitleMode === subtitle.srclang,
839
- onClick: () => {
840
- setSubtitleMode(subtitle.srclang);
841
- setOpenMenu(null);
842
- },
843
- children: subtitle.label
844
- },
845
- subtitle.srclang
846
- ))
847
- ] })
848
- ] }),
849
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
850
- /* @__PURE__ */ jsxs(
851
- "button",
852
- {
853
- type: "button",
854
- onClick: () => setOpenMenu(openMenu === "quality" ? null : "quality"),
855
- className: `flex h-6 items-center gap-1 rounded px-1.5 text-xs font-semibold transition hover:text-white/80 @sm:h-8 @sm:px-2 ${openMenu === "quality" ? "text-white" : "text-white/60"}`,
856
- "aria-label": "Quality",
857
- children: [
858
- /* @__PURE__ */ jsx(Settings, { className: "size-3.5 @sm:size-4" }),
859
- /* @__PURE__ */ jsx("span", { className: "hidden @sm:inline", children: qualities.find((q) => q.id === selectedQuality)?.label ?? "Auto" })
860
- ]
861
- }
862
- ),
863
- openMenu === "quality" && /* @__PURE__ */ jsx(PopoverMenu, { onClose: () => setOpenMenu(null), children: [...qualities].reverse().map((quality) => /* @__PURE__ */ jsxs(
864
- PopoverItem,
865
- {
866
- active: selectedQuality === quality.id,
867
- onClick: () => changeQuality(quality.id),
868
- children: [
869
- quality.label,
870
- quality.id === "auto" && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] text-white/40", children: "ABR" })
871
- ]
872
- },
873
- quality.id
874
- )) })
875
- ] }),
943
+ /* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsxs(
944
+ "button",
945
+ {
946
+ type: "button",
947
+ onClick: () => setSettingsOpen((v) => !v),
948
+ className: `flex h-6 items-center gap-1 rounded px-1.5 text-xs font-semibold transition hover:text-white/80 @sm:h-8 @sm:px-2 ${settingsOpen ? "text-white" : "text-white/60"}`,
949
+ "aria-label": "Settings",
950
+ children: [
951
+ /* @__PURE__ */ jsx(Settings, { className: "size-3.5 @sm:size-4" }),
952
+ /* @__PURE__ */ jsx("span", { className: "hidden @sm:inline", children: qualities.find((q) => q.id === selectedQuality)?.label ?? "Auto" })
953
+ ]
954
+ }
955
+ ) }),
876
956
  /* @__PURE__ */ jsx(
877
957
  "button",
878
958
  {
@@ -889,6 +969,14 @@ function Video({
889
969
  ]
890
970
  }
891
971
  ),
972
+ /* @__PURE__ */ jsx(
973
+ "div",
974
+ {
975
+ className: "pointer-events-none absolute inset-0 z-50 grid place-items-center",
976
+ style: { opacity: clickIcon ? 1 : 0, transition: clickIcon ? "opacity 0.08s ease-in" : "opacity 0.45s ease-out" },
977
+ children: /* @__PURE__ */ jsx("span", { className: "grid size-14 place-items-center rounded-full bg-black/40 text-white backdrop-blur-sm ring-1 ring-white/20 @sm:size-18 @lg:size-22", children: clickIcon === "pause" ? /* @__PURE__ */ jsx(Pause, { className: "size-6 @sm:size-8 @lg:size-10" }) : /* @__PURE__ */ jsx(Play, { className: "ml-1 size-6 @sm:size-8 @lg:size-10" }) })
978
+ }
979
+ ),
892
980
  activeCue && /* @__PURE__ */ jsx(
893
981
  "div",
894
982
  {
@@ -910,13 +998,7 @@ function Video({
910
998
  );
911
999
  }
912
1000
  var VideoPlayer = Video;
913
- function PopoverMenu({ children, onClose }) {
914
- return /* @__PURE__ */ jsxs(Fragment, { children: [
915
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-40", onClick: onClose }),
916
- /* @__PURE__ */ jsx("div", { className: "absolute bottom-full right-0 z-50 mb-2 min-w-35 overflow-hidden rounded-xl border border-white/10 bg-black/85 py-1 shadow-2xl backdrop-blur-xl", children })
917
- ] });
918
- }
919
- function PopoverItem({
1001
+ function SettingsItem({
920
1002
  children,
921
1003
  active,
922
1004
  onClick