@geekapps/silo-elements-nextjs 0.1.21 → 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
  });
@@ -311,7 +323,7 @@ function Video({
311
323
  setQualities([AUTO_QUALITY]);
312
324
  setAudioTracks([]);
313
325
  setSelectedAudio(0);
314
- setOpenMenu(null);
326
+ setSettingsOpen(false);
315
327
  video.pause();
316
328
  video.removeAttribute("src");
317
329
  video.load();
@@ -453,11 +465,15 @@ function Video({
453
465
  const video = videoRef.current;
454
466
  if (!video) return;
455
467
  try {
456
- if (video.paused) {
468
+ const wasPaused = video.paused;
469
+ if (wasPaused) {
457
470
  await video.play();
458
471
  } else {
459
472
  video.pause();
460
473
  }
474
+ if (clickIconTimerRef.current) window.clearTimeout(clickIconTimerRef.current);
475
+ setClickIcon(wasPaused ? "play" : "pause");
476
+ clickIconTimerRef.current = window.setTimeout(() => setClickIcon(null), 600);
461
477
  } catch {
462
478
  setError("O navegador bloqueou a reprodu\xE7\xE3o autom\xE1tica.");
463
479
  }
@@ -516,7 +532,7 @@ function Video({
516
532
  }, []);
517
533
  const changeAudio = useCallback((trackId) => {
518
534
  setSelectedAudio(trackId);
519
- setOpenMenu(null);
535
+ setSettingsOpen(false);
520
536
  if (hlsRef.current) {
521
537
  hlsRef.current.audioTrack = trackId;
522
538
  }
@@ -526,7 +542,7 @@ function Video({
526
542
  const option = qualities.find((quality) => quality.id === qualityId);
527
543
  if (!option) return;
528
544
  setSelectedQuality(qualityId);
529
- setOpenMenu(null);
545
+ setSettingsOpen(false);
530
546
  if (option.type === "auto") {
531
547
  if (hlsRef.current) {
532
548
  hlsRef.current.currentLevel = -1;
@@ -656,7 +672,7 @@ function Video({
656
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",
657
673
  style: maxHeight ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight, aspectRatio: "16/9" } : { aspectRatio: "16/9" },
658
674
  children: [
659
- /* @__PURE__ */ jsx(
675
+ /* @__PURE__ */ jsxs(
660
676
  "video",
661
677
  {
662
678
  ref: videoRef,
@@ -664,17 +680,27 @@ function Video({
664
680
  playsInline: true,
665
681
  preload: "metadata",
666
682
  crossOrigin: "anonymous",
667
- children: parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
668
- "track",
669
- {
670
- kind: "subtitles",
671
- src: subtitle.src,
672
- srcLang: subtitle.srclang,
673
- label: subtitle.label,
674
- default: subtitle.default
675
- },
676
- `${activeSource.src}-${subtitle.srclang}`
677
- ))
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
+ ]
678
704
  }
679
705
  ),
680
706
  poster && !hasPlayed && /* @__PURE__ */ jsx(
@@ -682,7 +708,7 @@ function Video({
682
708
  {
683
709
  src: poster,
684
710
  "aria-hidden": true,
685
- 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"
686
712
  }
687
713
  ),
688
714
  /* @__PURE__ */ jsx(
@@ -720,6 +746,81 @@ function Video({
720
746
  )
721
747
  ] }),
722
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
+ ] }),
723
824
  /* @__PURE__ */ jsxs(
724
825
  "div",
725
826
  {
@@ -774,6 +875,16 @@ function Video({
774
875
  ),
775
876
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 @sm:gap-4 @lg:gap-5", children: [
776
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
+ ),
777
888
  /* @__PURE__ */ jsx(
778
889
  "button",
779
890
  {
@@ -789,7 +900,7 @@ function Video({
789
900
  {
790
901
  type: "button",
791
902
  onClick: () => seekRelative(10),
792
- 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",
793
904
  "aria-label": "Forward 10 seconds",
794
905
  children: /* @__PURE__ */ jsx(FastForward, { className: "size-5 @sm:size-7" })
795
906
  }
@@ -829,91 +940,19 @@ function Video({
829
940
  }
830
941
  ),
831
942
  /* @__PURE__ */ jsx("div", { className: "mx-0.5 hidden h-4 w-px bg-white/20 @md:mx-1 @md:block" }),
832
- audioTracks.length > 1 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
833
- /* @__PURE__ */ jsx(
834
- "button",
835
- {
836
- type: "button",
837
- onClick: () => setOpenMenu(openMenu === "audio" ? null : "audio"),
838
- className: `grid size-6 place-items-center rounded transition hover:text-white/80 @sm:size-8 ${openMenu === "audio" ? "text-white" : "text-white/60"}`,
839
- "aria-label": "Audio track",
840
- children: /* @__PURE__ */ jsx(AudioLines, { className: "size-4 @sm:size-5" })
841
- }
842
- ),
843
- openMenu === "audio" && /* @__PURE__ */ jsx(PopoverMenu, { onClose: () => setOpenMenu(null), children: audioTracks.map((track) => /* @__PURE__ */ jsx(
844
- PopoverItem,
845
- {
846
- active: selectedAudio === track.id,
847
- onClick: () => changeAudio(track.id),
848
- children: track.label
849
- },
850
- track.id
851
- )) })
852
- ] }),
853
- parsed.subtitles.length > 0 && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
854
- /* @__PURE__ */ jsx(
855
- "button",
856
- {
857
- type: "button",
858
- onClick: () => setOpenMenu(openMenu === "captions" ? null : "captions"),
859
- 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"}`,
860
- "aria-label": "Captions",
861
- children: /* @__PURE__ */ jsx(Captions, { className: "size-4 @sm:size-5" })
862
- }
863
- ),
864
- openMenu === "captions" && /* @__PURE__ */ jsxs(PopoverMenu, { onClose: () => setOpenMenu(null), children: [
865
- /* @__PURE__ */ jsx(
866
- PopoverItem,
867
- {
868
- active: subtitleMode === "off",
869
- onClick: () => {
870
- setSubtitleMode("off");
871
- setOpenMenu(null);
872
- },
873
- children: "Off"
874
- }
875
- ),
876
- parsed.subtitles.map((subtitle) => /* @__PURE__ */ jsx(
877
- PopoverItem,
878
- {
879
- active: subtitleMode === subtitle.srclang,
880
- onClick: () => {
881
- setSubtitleMode(subtitle.srclang);
882
- setOpenMenu(null);
883
- },
884
- children: subtitle.label
885
- },
886
- subtitle.srclang
887
- ))
888
- ] })
889
- ] }),
890
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
891
- /* @__PURE__ */ jsxs(
892
- "button",
893
- {
894
- type: "button",
895
- onClick: () => setOpenMenu(openMenu === "quality" ? null : "quality"),
896
- 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"}`,
897
- "aria-label": "Quality",
898
- children: [
899
- /* @__PURE__ */ jsx(Settings, { className: "size-3.5 @sm:size-4" }),
900
- /* @__PURE__ */ jsx("span", { className: "hidden @sm:inline", children: qualities.find((q) => q.id === selectedQuality)?.label ?? "Auto" })
901
- ]
902
- }
903
- ),
904
- openMenu === "quality" && /* @__PURE__ */ jsx(PopoverMenu, { onClose: () => setOpenMenu(null), children: [...qualities].reverse().map((quality) => /* @__PURE__ */ jsxs(
905
- PopoverItem,
906
- {
907
- active: selectedQuality === quality.id,
908
- onClick: () => changeQuality(quality.id),
909
- children: [
910
- quality.label,
911
- quality.id === "auto" && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] text-white/40", children: "ABR" })
912
- ]
913
- },
914
- quality.id
915
- )) })
916
- ] }),
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
+ ) }),
917
956
  /* @__PURE__ */ jsx(
918
957
  "button",
919
958
  {
@@ -930,6 +969,14 @@ function Video({
930
969
  ]
931
970
  }
932
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
+ ),
933
980
  activeCue && /* @__PURE__ */ jsx(
934
981
  "div",
935
982
  {
@@ -951,13 +998,7 @@ function Video({
951
998
  );
952
999
  }
953
1000
  var VideoPlayer = Video;
954
- function PopoverMenu({ children, onClose }) {
955
- return /* @__PURE__ */ jsxs(Fragment, { children: [
956
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-40", onClick: onClose }),
957
- /* @__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 })
958
- ] });
959
- }
960
- function PopoverItem({
1001
+ function SettingsItem({
961
1002
  children,
962
1003
  active,
963
1004
  onClick