@scarlett-player/ui 1.0.2 → 1.2.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/README.md +5 -0
- package/dist/index.cjs +158 -43
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +158 -43
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -30,9 +30,14 @@ const player = new ScarlettPlayer({
|
|
|
30
30
|
|
|
31
31
|
- Play/pause, seek, volume controls
|
|
32
32
|
- Fullscreen toggle
|
|
33
|
+
- Picture-in-Picture toggle (disabled until media metadata is loaded; hidden
|
|
34
|
+
when the browser has no PiP support; Safari webkit presentation mode
|
|
35
|
+
supported)
|
|
33
36
|
- Quality selector
|
|
34
37
|
- Progress bar with buffering indicator
|
|
35
38
|
- Time display (current / duration)
|
|
39
|
+
- Error overlay with viewer-friendly copy per error code, a Try Again action,
|
|
40
|
+
and a reconnecting state while the player self-heals
|
|
36
41
|
- Keyboard shortcuts
|
|
37
42
|
- Customizable theming
|
|
38
43
|
- Auto-hide controls
|
package/dist/index.cjs
CHANGED
|
@@ -712,6 +712,17 @@ var styles = `
|
|
|
712
712
|
fill: currentColor;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
/* Reconnecting: pulse the icon so the overlay reads as active work,
|
|
716
|
+
not a dead-end error */
|
|
717
|
+
.sp-error-overlay--reconnecting .sp-error-overlay__icon {
|
|
718
|
+
animation: sp-reconnect-pulse 1.2s ease-in-out infinite;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
@keyframes sp-reconnect-pulse {
|
|
722
|
+
0%, 100% { opacity: 0.4; }
|
|
723
|
+
50% { opacity: 1; }
|
|
724
|
+
}
|
|
725
|
+
|
|
715
726
|
.sp-error-overlay__message {
|
|
716
727
|
color: rgba(255, 255, 255, 0.9);
|
|
717
728
|
font-size: 15px;
|
|
@@ -918,12 +929,24 @@ function createButton(className, label, icon) {
|
|
|
918
929
|
"aria-label": label,
|
|
919
930
|
type: "button"
|
|
920
931
|
});
|
|
921
|
-
btn
|
|
932
|
+
setHTML(btn, icon);
|
|
922
933
|
return btn;
|
|
923
934
|
}
|
|
924
935
|
function getVideo(container) {
|
|
925
936
|
return container.querySelector("video");
|
|
926
937
|
}
|
|
938
|
+
var lastHTML = /* @__PURE__ */ new WeakMap();
|
|
939
|
+
function setHTML(el, html) {
|
|
940
|
+
if (lastHTML.get(el) === html) return false;
|
|
941
|
+
el.innerHTML = html;
|
|
942
|
+
lastHTML.set(el, html);
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
function setAttr(el, name, value) {
|
|
946
|
+
if (el.getAttribute(name) === value) return false;
|
|
947
|
+
el.setAttribute(name, value);
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
927
950
|
|
|
928
951
|
// src/utils/format.ts
|
|
929
952
|
function formatTime(seconds) {
|
|
@@ -978,8 +1001,8 @@ var PlayButton = class {
|
|
|
978
1001
|
icon = icons.play;
|
|
979
1002
|
label = "Play";
|
|
980
1003
|
}
|
|
981
|
-
this.el
|
|
982
|
-
this.el
|
|
1004
|
+
setHTML(this.el, icon);
|
|
1005
|
+
setAttr(this.el, "aria-label", label);
|
|
983
1006
|
}
|
|
984
1007
|
toggle() {
|
|
985
1008
|
const video = getVideo(this.api.container);
|
|
@@ -1483,13 +1506,16 @@ var VolumeControl = class {
|
|
|
1483
1506
|
icon = icons.volumeHigh;
|
|
1484
1507
|
label = "Mute";
|
|
1485
1508
|
}
|
|
1486
|
-
this.btn
|
|
1487
|
-
this.btn
|
|
1509
|
+
setHTML(this.btn, icon);
|
|
1510
|
+
setAttr(this.btn, "aria-label", label);
|
|
1488
1511
|
const displayVolume = muted ? 0 : volume;
|
|
1489
|
-
|
|
1512
|
+
const width = `${displayVolume * 100}%`;
|
|
1513
|
+
if (this.level.style.width !== width) {
|
|
1514
|
+
this.level.style.width = width;
|
|
1515
|
+
}
|
|
1490
1516
|
const volumePercent = Math.round(displayVolume * 100);
|
|
1491
|
-
this.slider
|
|
1492
|
-
this.slider
|
|
1517
|
+
setAttr(this.slider, "aria-valuenow", String(volumePercent));
|
|
1518
|
+
setAttr(this.slider, "aria-valuetext", `${volumePercent}%`);
|
|
1493
1519
|
}
|
|
1494
1520
|
toggleMute() {
|
|
1495
1521
|
const video = getVideo(this.api.container);
|
|
@@ -1726,11 +1752,11 @@ var CastButton = class {
|
|
|
1726
1752
|
this.el.classList.toggle("sp-cast--active", !!active);
|
|
1727
1753
|
this.el.classList.toggle("sp-cast--unavailable", !available && !active);
|
|
1728
1754
|
if (active) {
|
|
1729
|
-
this.el
|
|
1730
|
-
this.el
|
|
1755
|
+
setHTML(this.el, icons.chromecastConnected);
|
|
1756
|
+
setAttr(this.el, "aria-label", "Stop casting");
|
|
1731
1757
|
} else {
|
|
1732
|
-
this.el
|
|
1733
|
-
this.el
|
|
1758
|
+
setHTML(this.el, icons.chromecast);
|
|
1759
|
+
setAttr(this.el, "aria-label", available ? "Cast" : "No Cast devices found");
|
|
1734
1760
|
}
|
|
1735
1761
|
} else {
|
|
1736
1762
|
const active = this.api.getState("airplayActive");
|
|
@@ -1738,7 +1764,7 @@ var CastButton = class {
|
|
|
1738
1764
|
this.el.disabled = false;
|
|
1739
1765
|
this.el.classList.toggle("sp-cast--active", !!active);
|
|
1740
1766
|
this.el.classList.remove("sp-cast--unavailable");
|
|
1741
|
-
this.el
|
|
1767
|
+
setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
|
|
1742
1768
|
}
|
|
1743
1769
|
}
|
|
1744
1770
|
handleClick() {
|
|
@@ -1776,25 +1802,38 @@ var CastButton = class {
|
|
|
1776
1802
|
var PipButton = class {
|
|
1777
1803
|
constructor(api) {
|
|
1778
1804
|
this.clickHandler = () => {
|
|
1779
|
-
this.toggle()
|
|
1805
|
+
void this.toggle().catch(() => {
|
|
1806
|
+
});
|
|
1780
1807
|
};
|
|
1781
1808
|
this.api = api;
|
|
1782
|
-
const
|
|
1783
|
-
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in
|
|
1809
|
+
const probe = document.createElement("video");
|
|
1810
|
+
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
|
|
1784
1811
|
this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
|
|
1785
1812
|
this.el.addEventListener("click", this.clickHandler);
|
|
1786
1813
|
if (!this.supported) {
|
|
1787
1814
|
this.el.style.display = "none";
|
|
1815
|
+
} else {
|
|
1816
|
+
this.el.disabled = true;
|
|
1817
|
+
this.el.setAttribute("aria-disabled", "true");
|
|
1788
1818
|
}
|
|
1789
1819
|
}
|
|
1790
1820
|
render() {
|
|
1791
1821
|
return this.el;
|
|
1792
1822
|
}
|
|
1823
|
+
/** Whether the media element is ready to enter PiP (metadata loaded). */
|
|
1824
|
+
isMediaReady() {
|
|
1825
|
+
const video = getVideo(this.api.container);
|
|
1826
|
+
return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
|
|
1827
|
+
}
|
|
1793
1828
|
update() {
|
|
1794
1829
|
if (!this.supported) return;
|
|
1795
|
-
const pip = this.api.getState("pip");
|
|
1796
|
-
|
|
1797
|
-
this.el.
|
|
1830
|
+
const pip = !!this.api.getState("pip");
|
|
1831
|
+
const enabled = pip || this.isMediaReady();
|
|
1832
|
+
this.el.disabled = !enabled;
|
|
1833
|
+
setAttr(this.el, "aria-disabled", String(!enabled));
|
|
1834
|
+
setHTML(this.el, pip ? icons.exitPip : icons.pip);
|
|
1835
|
+
setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
|
|
1836
|
+
this.el.classList.toggle("sp-pip--active", pip);
|
|
1798
1837
|
}
|
|
1799
1838
|
async toggle() {
|
|
1800
1839
|
const video = getVideo(this.api.container);
|
|
@@ -1802,8 +1841,14 @@ var PipButton = class {
|
|
|
1802
1841
|
this.api.logger.warn("PiP: video element not found");
|
|
1803
1842
|
return;
|
|
1804
1843
|
}
|
|
1844
|
+
const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
|
|
1845
|
+
if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
|
|
1846
|
+
this.api.logger.debug("PiP: ignored, media not ready", {
|
|
1847
|
+
readyState: video.readyState
|
|
1848
|
+
});
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1805
1851
|
try {
|
|
1806
|
-
const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
|
|
1807
1852
|
if (isInPip) {
|
|
1808
1853
|
if (document.pictureInPictureElement) {
|
|
1809
1854
|
await document.exitPictureInPicture();
|
|
@@ -1820,7 +1865,8 @@ var PipButton = class {
|
|
|
1820
1865
|
this.api.logger.debug("PiP: entered");
|
|
1821
1866
|
}
|
|
1822
1867
|
} catch (e) {
|
|
1823
|
-
|
|
1868
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1869
|
+
this.api.logger.warn("PiP: failed", { error: message });
|
|
1824
1870
|
}
|
|
1825
1871
|
}
|
|
1826
1872
|
destroy() {
|
|
@@ -1845,11 +1891,11 @@ var FullscreenButton = class {
|
|
|
1845
1891
|
update() {
|
|
1846
1892
|
const fullscreen = this.api.getState("fullscreen");
|
|
1847
1893
|
if (fullscreen) {
|
|
1848
|
-
this.el
|
|
1849
|
-
this.el
|
|
1894
|
+
setHTML(this.el, icons.exitFullscreen);
|
|
1895
|
+
setAttr(this.el, "aria-label", "Exit fullscreen");
|
|
1850
1896
|
} else {
|
|
1851
|
-
this.el
|
|
1852
|
-
this.el
|
|
1897
|
+
setHTML(this.el, icons.fullscreen);
|
|
1898
|
+
setAttr(this.el, "aria-label", "Fullscreen");
|
|
1853
1899
|
}
|
|
1854
1900
|
}
|
|
1855
1901
|
async toggle() {
|
|
@@ -1890,6 +1936,27 @@ var Spacer = class {
|
|
|
1890
1936
|
// src/controls/ErrorOverlay.ts
|
|
1891
1937
|
function getUserMessage(error) {
|
|
1892
1938
|
if (!error) return "Something went wrong.";
|
|
1939
|
+
const code = error.code;
|
|
1940
|
+
if (code) {
|
|
1941
|
+
switch (code) {
|
|
1942
|
+
case "MEDIA_NETWORK_ERROR":
|
|
1943
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1944
|
+
case "MEDIA_DECODE_ERROR":
|
|
1945
|
+
return "This video can't be played right now.";
|
|
1946
|
+
case "SOURCE_LOAD_FAILED":
|
|
1947
|
+
case "SOURCE_NOT_SUPPORTED":
|
|
1948
|
+
case "PROVIDER_NOT_FOUND":
|
|
1949
|
+
return "Unable to load video. Please try again.";
|
|
1950
|
+
case "PLAYBACK_FAILED":
|
|
1951
|
+
return "Playback stopped unexpectedly. Please try again.";
|
|
1952
|
+
case "MEDIA_APPEND_ERROR":
|
|
1953
|
+
return "Video playback was interrupted. Please try again.";
|
|
1954
|
+
case "MEDIA_BUFFER_FULL":
|
|
1955
|
+
return "Your device is low on video memory. Close other apps or tabs and try again.";
|
|
1956
|
+
case "PLAYLIST_INVALID":
|
|
1957
|
+
return "The stream is temporarily unavailable. Please try again.";
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1893
1960
|
const msg = error.message?.toLowerCase() || "";
|
|
1894
1961
|
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1895
1962
|
return "Having trouble connecting. Check your internet and try again.";
|
|
@@ -1917,13 +1984,6 @@ var ErrorOverlay = class {
|
|
|
1917
1984
|
const src = source?.src || this.lastSource;
|
|
1918
1985
|
if (src) {
|
|
1919
1986
|
this.api.emit("error:retry", { src });
|
|
1920
|
-
const video = this.api.container.querySelector("video");
|
|
1921
|
-
if (video) {
|
|
1922
|
-
video.src = src;
|
|
1923
|
-
video.load();
|
|
1924
|
-
video.play().catch(() => {
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
1987
|
}
|
|
1928
1988
|
setTimeout(() => {
|
|
1929
1989
|
this.retryBtn.disabled = false;
|
|
@@ -1984,12 +2044,36 @@ var ErrorOverlay = class {
|
|
|
1984
2044
|
}
|
|
1985
2045
|
this.visible = true;
|
|
1986
2046
|
this.retryBtn.disabled = false;
|
|
2047
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
2048
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Show the reconnecting state.
|
|
2052
|
+
*
|
|
2053
|
+
* Displayed while the provider auto-reconnects after a fatal error, so the
|
|
2054
|
+
* viewer sees the player working on the problem instead of a dead-end
|
|
2055
|
+
* error. Try Again stays available for viewers who want to force an
|
|
2056
|
+
* immediate attempt.
|
|
2057
|
+
*/
|
|
2058
|
+
showReconnecting() {
|
|
2059
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
2060
|
+
if (messageEl) {
|
|
2061
|
+
messageEl.textContent = "Connection lost. Reconnecting...";
|
|
2062
|
+
}
|
|
2063
|
+
const source = this.api.getState("source");
|
|
2064
|
+
if (source?.src) {
|
|
2065
|
+
this.lastSource = source.src;
|
|
2066
|
+
}
|
|
2067
|
+
this.visible = true;
|
|
2068
|
+
this.retryBtn.disabled = false;
|
|
2069
|
+
this.el.classList.add("sp-error-overlay--reconnecting");
|
|
1987
2070
|
this.el.classList.add("sp-error-overlay--visible");
|
|
1988
2071
|
}
|
|
1989
2072
|
/** Hide the error overlay */
|
|
1990
2073
|
hide() {
|
|
1991
2074
|
this.visible = false;
|
|
1992
2075
|
this.el.classList.remove("sp-error-overlay--visible");
|
|
2076
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
1993
2077
|
}
|
|
1994
2078
|
isVisible() {
|
|
1995
2079
|
return this.visible;
|
|
@@ -2473,12 +2557,12 @@ var CaptionsButton = class {
|
|
|
2473
2557
|
}
|
|
2474
2558
|
this.el.style.display = "";
|
|
2475
2559
|
if (currentTrack) {
|
|
2476
|
-
this.el
|
|
2477
|
-
this.el
|
|
2560
|
+
setHTML(this.el, icons.captions);
|
|
2561
|
+
setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
|
|
2478
2562
|
this.el.classList.add("sp-captions--active");
|
|
2479
2563
|
} else {
|
|
2480
|
-
this.el
|
|
2481
|
-
this.el
|
|
2564
|
+
setHTML(this.el, icons.captionsOff);
|
|
2565
|
+
setAttr(this.el, "aria-label", "Captions");
|
|
2482
2566
|
this.el.classList.remove("sp-captions--active");
|
|
2483
2567
|
}
|
|
2484
2568
|
}
|
|
@@ -2561,7 +2645,10 @@ function uiPlugin(config = {}) {
|
|
|
2561
2645
|
let hideTimeout = null;
|
|
2562
2646
|
let stateUnsubscribe = null;
|
|
2563
2647
|
let errorUnsubscribe = null;
|
|
2648
|
+
let reconnectingUnsubscribe = null;
|
|
2649
|
+
let recoveredUnsubscribe = null;
|
|
2564
2650
|
let controlsVisible = true;
|
|
2651
|
+
let rafHandle = null;
|
|
2565
2652
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2566
2653
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
2567
2654
|
const createControl = (slot) => {
|
|
@@ -2613,6 +2700,13 @@ function uiPlugin(config = {}) {
|
|
|
2613
2700
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2614
2701
|
errorOverlay?.update();
|
|
2615
2702
|
};
|
|
2703
|
+
const scheduleUpdate = () => {
|
|
2704
|
+
if (rafHandle !== null) return;
|
|
2705
|
+
rafHandle = requestAnimationFrame(() => {
|
|
2706
|
+
rafHandle = null;
|
|
2707
|
+
updateControls();
|
|
2708
|
+
});
|
|
2709
|
+
};
|
|
2616
2710
|
const showControls = () => {
|
|
2617
2711
|
if (controlsVisible) {
|
|
2618
2712
|
resetHideTimer();
|
|
@@ -2662,7 +2756,12 @@ function uiPlugin(config = {}) {
|
|
|
2662
2756
|
case " ":
|
|
2663
2757
|
case "k":
|
|
2664
2758
|
e.preventDefault();
|
|
2665
|
-
video.paused
|
|
2759
|
+
if (video.paused) {
|
|
2760
|
+
video.play().catch(() => {
|
|
2761
|
+
});
|
|
2762
|
+
} else {
|
|
2763
|
+
video.pause();
|
|
2764
|
+
}
|
|
2666
2765
|
break;
|
|
2667
2766
|
case "m":
|
|
2668
2767
|
e.preventDefault();
|
|
@@ -2671,9 +2770,11 @@ function uiPlugin(config = {}) {
|
|
|
2671
2770
|
case "f":
|
|
2672
2771
|
e.preventDefault();
|
|
2673
2772
|
if (document.fullscreenElement) {
|
|
2674
|
-
document.exitFullscreen()
|
|
2773
|
+
document.exitFullscreen().catch(() => {
|
|
2774
|
+
});
|
|
2675
2775
|
} else {
|
|
2676
|
-
api.container.requestFullscreen?.()
|
|
2776
|
+
api.container.requestFullscreen?.().catch(() => {
|
|
2777
|
+
});
|
|
2677
2778
|
}
|
|
2678
2779
|
break;
|
|
2679
2780
|
case "ArrowLeft":
|
|
@@ -2741,10 +2842,16 @@ function uiPlugin(config = {}) {
|
|
|
2741
2842
|
container.appendChild(errorOverlay.render());
|
|
2742
2843
|
errorUnsubscribe = api.on("error", (payload) => {
|
|
2743
2844
|
if (payload?.fatal) {
|
|
2744
|
-
const error = api.getState("error") ||
|
|
2845
|
+
const error = api.getState("error") || payload;
|
|
2745
2846
|
errorOverlay?.show(error);
|
|
2746
2847
|
}
|
|
2747
2848
|
});
|
|
2849
|
+
reconnectingUnsubscribe = api.on("error:reconnecting", () => {
|
|
2850
|
+
errorOverlay?.showReconnecting();
|
|
2851
|
+
});
|
|
2852
|
+
recoveredUnsubscribe = api.on("error:recovered", () => {
|
|
2853
|
+
errorOverlay?.hide();
|
|
2854
|
+
});
|
|
2748
2855
|
progressBar = new ProgressBar(api);
|
|
2749
2856
|
container.appendChild(progressBar.render());
|
|
2750
2857
|
if (!isPlaying) {
|
|
@@ -2768,8 +2875,8 @@ function uiPlugin(config = {}) {
|
|
|
2768
2875
|
container.addEventListener("touchstart", handleInteraction, { passive: true });
|
|
2769
2876
|
container.addEventListener("click", handleInteraction);
|
|
2770
2877
|
document.addEventListener("keydown", handleKeyDown);
|
|
2771
|
-
stateUnsubscribe = api.subscribeToState(
|
|
2772
|
-
document.addEventListener("fullscreenchange",
|
|
2878
|
+
stateUnsubscribe = api.subscribeToState(scheduleUpdate);
|
|
2879
|
+
document.addEventListener("fullscreenchange", scheduleUpdate);
|
|
2773
2880
|
updateControls();
|
|
2774
2881
|
if (!container.hasAttribute("tabindex")) {
|
|
2775
2882
|
container.setAttribute("tabindex", "0");
|
|
@@ -2786,10 +2893,18 @@ function uiPlugin(config = {}) {
|
|
|
2786
2893
|
clearTimeout(hideTimeout);
|
|
2787
2894
|
hideTimeout = null;
|
|
2788
2895
|
}
|
|
2896
|
+
if (rafHandle !== null) {
|
|
2897
|
+
cancelAnimationFrame(rafHandle);
|
|
2898
|
+
rafHandle = null;
|
|
2899
|
+
}
|
|
2789
2900
|
stateUnsubscribe?.();
|
|
2790
2901
|
stateUnsubscribe = null;
|
|
2791
2902
|
errorUnsubscribe?.();
|
|
2792
2903
|
errorUnsubscribe = null;
|
|
2904
|
+
reconnectingUnsubscribe?.();
|
|
2905
|
+
reconnectingUnsubscribe = null;
|
|
2906
|
+
recoveredUnsubscribe?.();
|
|
2907
|
+
recoveredUnsubscribe = null;
|
|
2793
2908
|
if (api?.container) {
|
|
2794
2909
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2795
2910
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2798,7 +2913,7 @@ function uiPlugin(config = {}) {
|
|
|
2798
2913
|
api.container.removeEventListener("click", handleInteraction);
|
|
2799
2914
|
}
|
|
2800
2915
|
document.removeEventListener("keydown", handleKeyDown);
|
|
2801
|
-
document.removeEventListener("fullscreenchange",
|
|
2916
|
+
document.removeEventListener("fullscreenchange", scheduleUpdate);
|
|
2802
2917
|
controls.forEach((c) => c.destroy());
|
|
2803
2918
|
controls = [];
|
|
2804
2919
|
progressBar?.destroy();
|
package/dist/index.d.cts
CHANGED
|
@@ -105,7 +105,7 @@ declare const icons: {
|
|
|
105
105
|
* Modern, minimal design inspired by Mux Player and Vidstack.
|
|
106
106
|
* Uses CSS custom properties for theming.
|
|
107
107
|
*/
|
|
108
|
-
declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
|
|
108
|
+
declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Formatting utility functions
|
package/dist/index.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ declare const icons: {
|
|
|
105
105
|
* Modern, minimal design inspired by Mux Player and Vidstack.
|
|
106
106
|
* Uses CSS custom properties for theming.
|
|
107
107
|
*/
|
|
108
|
-
declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
|
|
108
|
+
declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n gap: 4px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: 48px;\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: 64px;\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: 64px;\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n overflow: hidden;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Formatting utility functions
|
package/dist/index.js
CHANGED
|
@@ -681,6 +681,17 @@ var styles = `
|
|
|
681
681
|
fill: currentColor;
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
+
/* Reconnecting: pulse the icon so the overlay reads as active work,
|
|
685
|
+
not a dead-end error */
|
|
686
|
+
.sp-error-overlay--reconnecting .sp-error-overlay__icon {
|
|
687
|
+
animation: sp-reconnect-pulse 1.2s ease-in-out infinite;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@keyframes sp-reconnect-pulse {
|
|
691
|
+
0%, 100% { opacity: 0.4; }
|
|
692
|
+
50% { opacity: 1; }
|
|
693
|
+
}
|
|
694
|
+
|
|
684
695
|
.sp-error-overlay__message {
|
|
685
696
|
color: rgba(255, 255, 255, 0.9);
|
|
686
697
|
font-size: 15px;
|
|
@@ -887,12 +898,24 @@ function createButton(className, label, icon) {
|
|
|
887
898
|
"aria-label": label,
|
|
888
899
|
type: "button"
|
|
889
900
|
});
|
|
890
|
-
btn
|
|
901
|
+
setHTML(btn, icon);
|
|
891
902
|
return btn;
|
|
892
903
|
}
|
|
893
904
|
function getVideo(container) {
|
|
894
905
|
return container.querySelector("video");
|
|
895
906
|
}
|
|
907
|
+
var lastHTML = /* @__PURE__ */ new WeakMap();
|
|
908
|
+
function setHTML(el, html) {
|
|
909
|
+
if (lastHTML.get(el) === html) return false;
|
|
910
|
+
el.innerHTML = html;
|
|
911
|
+
lastHTML.set(el, html);
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
function setAttr(el, name, value) {
|
|
915
|
+
if (el.getAttribute(name) === value) return false;
|
|
916
|
+
el.setAttribute(name, value);
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
896
919
|
|
|
897
920
|
// src/utils/format.ts
|
|
898
921
|
function formatTime(seconds) {
|
|
@@ -947,8 +970,8 @@ var PlayButton = class {
|
|
|
947
970
|
icon = icons.play;
|
|
948
971
|
label = "Play";
|
|
949
972
|
}
|
|
950
|
-
this.el
|
|
951
|
-
this.el
|
|
973
|
+
setHTML(this.el, icon);
|
|
974
|
+
setAttr(this.el, "aria-label", label);
|
|
952
975
|
}
|
|
953
976
|
toggle() {
|
|
954
977
|
const video = getVideo(this.api.container);
|
|
@@ -1452,13 +1475,16 @@ var VolumeControl = class {
|
|
|
1452
1475
|
icon = icons.volumeHigh;
|
|
1453
1476
|
label = "Mute";
|
|
1454
1477
|
}
|
|
1455
|
-
this.btn
|
|
1456
|
-
this.btn
|
|
1478
|
+
setHTML(this.btn, icon);
|
|
1479
|
+
setAttr(this.btn, "aria-label", label);
|
|
1457
1480
|
const displayVolume = muted ? 0 : volume;
|
|
1458
|
-
|
|
1481
|
+
const width = `${displayVolume * 100}%`;
|
|
1482
|
+
if (this.level.style.width !== width) {
|
|
1483
|
+
this.level.style.width = width;
|
|
1484
|
+
}
|
|
1459
1485
|
const volumePercent = Math.round(displayVolume * 100);
|
|
1460
|
-
this.slider
|
|
1461
|
-
this.slider
|
|
1486
|
+
setAttr(this.slider, "aria-valuenow", String(volumePercent));
|
|
1487
|
+
setAttr(this.slider, "aria-valuetext", `${volumePercent}%`);
|
|
1462
1488
|
}
|
|
1463
1489
|
toggleMute() {
|
|
1464
1490
|
const video = getVideo(this.api.container);
|
|
@@ -1695,11 +1721,11 @@ var CastButton = class {
|
|
|
1695
1721
|
this.el.classList.toggle("sp-cast--active", !!active);
|
|
1696
1722
|
this.el.classList.toggle("sp-cast--unavailable", !available && !active);
|
|
1697
1723
|
if (active) {
|
|
1698
|
-
this.el
|
|
1699
|
-
this.el
|
|
1724
|
+
setHTML(this.el, icons.chromecastConnected);
|
|
1725
|
+
setAttr(this.el, "aria-label", "Stop casting");
|
|
1700
1726
|
} else {
|
|
1701
|
-
this.el
|
|
1702
|
-
this.el
|
|
1727
|
+
setHTML(this.el, icons.chromecast);
|
|
1728
|
+
setAttr(this.el, "aria-label", available ? "Cast" : "No Cast devices found");
|
|
1703
1729
|
}
|
|
1704
1730
|
} else {
|
|
1705
1731
|
const active = this.api.getState("airplayActive");
|
|
@@ -1707,7 +1733,7 @@ var CastButton = class {
|
|
|
1707
1733
|
this.el.disabled = false;
|
|
1708
1734
|
this.el.classList.toggle("sp-cast--active", !!active);
|
|
1709
1735
|
this.el.classList.remove("sp-cast--unavailable");
|
|
1710
|
-
this.el
|
|
1736
|
+
setAttr(this.el, "aria-label", active ? "Stop AirPlay" : "AirPlay");
|
|
1711
1737
|
}
|
|
1712
1738
|
}
|
|
1713
1739
|
handleClick() {
|
|
@@ -1745,25 +1771,38 @@ var CastButton = class {
|
|
|
1745
1771
|
var PipButton = class {
|
|
1746
1772
|
constructor(api) {
|
|
1747
1773
|
this.clickHandler = () => {
|
|
1748
|
-
this.toggle()
|
|
1774
|
+
void this.toggle().catch(() => {
|
|
1775
|
+
});
|
|
1749
1776
|
};
|
|
1750
1777
|
this.api = api;
|
|
1751
|
-
const
|
|
1752
|
-
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in
|
|
1778
|
+
const probe = document.createElement("video");
|
|
1779
|
+
this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in probe;
|
|
1753
1780
|
this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
|
|
1754
1781
|
this.el.addEventListener("click", this.clickHandler);
|
|
1755
1782
|
if (!this.supported) {
|
|
1756
1783
|
this.el.style.display = "none";
|
|
1784
|
+
} else {
|
|
1785
|
+
this.el.disabled = true;
|
|
1786
|
+
this.el.setAttribute("aria-disabled", "true");
|
|
1757
1787
|
}
|
|
1758
1788
|
}
|
|
1759
1789
|
render() {
|
|
1760
1790
|
return this.el;
|
|
1761
1791
|
}
|
|
1792
|
+
/** Whether the media element is ready to enter PiP (metadata loaded). */
|
|
1793
|
+
isMediaReady() {
|
|
1794
|
+
const video = getVideo(this.api.container);
|
|
1795
|
+
return !!video && video.readyState >= HTMLMediaElement.HAVE_METADATA;
|
|
1796
|
+
}
|
|
1762
1797
|
update() {
|
|
1763
1798
|
if (!this.supported) return;
|
|
1764
|
-
const pip = this.api.getState("pip");
|
|
1765
|
-
|
|
1766
|
-
this.el.
|
|
1799
|
+
const pip = !!this.api.getState("pip");
|
|
1800
|
+
const enabled = pip || this.isMediaReady();
|
|
1801
|
+
this.el.disabled = !enabled;
|
|
1802
|
+
setAttr(this.el, "aria-disabled", String(!enabled));
|
|
1803
|
+
setHTML(this.el, pip ? icons.exitPip : icons.pip);
|
|
1804
|
+
setAttr(this.el, "aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
|
|
1805
|
+
this.el.classList.toggle("sp-pip--active", pip);
|
|
1767
1806
|
}
|
|
1768
1807
|
async toggle() {
|
|
1769
1808
|
const video = getVideo(this.api.container);
|
|
@@ -1771,8 +1810,14 @@ var PipButton = class {
|
|
|
1771
1810
|
this.api.logger.warn("PiP: video element not found");
|
|
1772
1811
|
return;
|
|
1773
1812
|
}
|
|
1813
|
+
const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
|
|
1814
|
+
if (!isInPip && video.readyState < HTMLMediaElement.HAVE_METADATA) {
|
|
1815
|
+
this.api.logger.debug("PiP: ignored, media not ready", {
|
|
1816
|
+
readyState: video.readyState
|
|
1817
|
+
});
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1774
1820
|
try {
|
|
1775
|
-
const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
|
|
1776
1821
|
if (isInPip) {
|
|
1777
1822
|
if (document.pictureInPictureElement) {
|
|
1778
1823
|
await document.exitPictureInPicture();
|
|
@@ -1789,7 +1834,8 @@ var PipButton = class {
|
|
|
1789
1834
|
this.api.logger.debug("PiP: entered");
|
|
1790
1835
|
}
|
|
1791
1836
|
} catch (e) {
|
|
1792
|
-
|
|
1837
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1838
|
+
this.api.logger.warn("PiP: failed", { error: message });
|
|
1793
1839
|
}
|
|
1794
1840
|
}
|
|
1795
1841
|
destroy() {
|
|
@@ -1814,11 +1860,11 @@ var FullscreenButton = class {
|
|
|
1814
1860
|
update() {
|
|
1815
1861
|
const fullscreen = this.api.getState("fullscreen");
|
|
1816
1862
|
if (fullscreen) {
|
|
1817
|
-
this.el
|
|
1818
|
-
this.el
|
|
1863
|
+
setHTML(this.el, icons.exitFullscreen);
|
|
1864
|
+
setAttr(this.el, "aria-label", "Exit fullscreen");
|
|
1819
1865
|
} else {
|
|
1820
|
-
this.el
|
|
1821
|
-
this.el
|
|
1866
|
+
setHTML(this.el, icons.fullscreen);
|
|
1867
|
+
setAttr(this.el, "aria-label", "Fullscreen");
|
|
1822
1868
|
}
|
|
1823
1869
|
}
|
|
1824
1870
|
async toggle() {
|
|
@@ -1859,6 +1905,27 @@ var Spacer = class {
|
|
|
1859
1905
|
// src/controls/ErrorOverlay.ts
|
|
1860
1906
|
function getUserMessage(error) {
|
|
1861
1907
|
if (!error) return "Something went wrong.";
|
|
1908
|
+
const code = error.code;
|
|
1909
|
+
if (code) {
|
|
1910
|
+
switch (code) {
|
|
1911
|
+
case "MEDIA_NETWORK_ERROR":
|
|
1912
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1913
|
+
case "MEDIA_DECODE_ERROR":
|
|
1914
|
+
return "This video can't be played right now.";
|
|
1915
|
+
case "SOURCE_LOAD_FAILED":
|
|
1916
|
+
case "SOURCE_NOT_SUPPORTED":
|
|
1917
|
+
case "PROVIDER_NOT_FOUND":
|
|
1918
|
+
return "Unable to load video. Please try again.";
|
|
1919
|
+
case "PLAYBACK_FAILED":
|
|
1920
|
+
return "Playback stopped unexpectedly. Please try again.";
|
|
1921
|
+
case "MEDIA_APPEND_ERROR":
|
|
1922
|
+
return "Video playback was interrupted. Please try again.";
|
|
1923
|
+
case "MEDIA_BUFFER_FULL":
|
|
1924
|
+
return "Your device is low on video memory. Close other apps or tabs and try again.";
|
|
1925
|
+
case "PLAYLIST_INVALID":
|
|
1926
|
+
return "The stream is temporarily unavailable. Please try again.";
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1862
1929
|
const msg = error.message?.toLowerCase() || "";
|
|
1863
1930
|
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1864
1931
|
return "Having trouble connecting. Check your internet and try again.";
|
|
@@ -1886,13 +1953,6 @@ var ErrorOverlay = class {
|
|
|
1886
1953
|
const src = source?.src || this.lastSource;
|
|
1887
1954
|
if (src) {
|
|
1888
1955
|
this.api.emit("error:retry", { src });
|
|
1889
|
-
const video = this.api.container.querySelector("video");
|
|
1890
|
-
if (video) {
|
|
1891
|
-
video.src = src;
|
|
1892
|
-
video.load();
|
|
1893
|
-
video.play().catch(() => {
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
1956
|
}
|
|
1897
1957
|
setTimeout(() => {
|
|
1898
1958
|
this.retryBtn.disabled = false;
|
|
@@ -1953,12 +2013,36 @@ var ErrorOverlay = class {
|
|
|
1953
2013
|
}
|
|
1954
2014
|
this.visible = true;
|
|
1955
2015
|
this.retryBtn.disabled = false;
|
|
2016
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
2017
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Show the reconnecting state.
|
|
2021
|
+
*
|
|
2022
|
+
* Displayed while the provider auto-reconnects after a fatal error, so the
|
|
2023
|
+
* viewer sees the player working on the problem instead of a dead-end
|
|
2024
|
+
* error. Try Again stays available for viewers who want to force an
|
|
2025
|
+
* immediate attempt.
|
|
2026
|
+
*/
|
|
2027
|
+
showReconnecting() {
|
|
2028
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
2029
|
+
if (messageEl) {
|
|
2030
|
+
messageEl.textContent = "Connection lost. Reconnecting...";
|
|
2031
|
+
}
|
|
2032
|
+
const source = this.api.getState("source");
|
|
2033
|
+
if (source?.src) {
|
|
2034
|
+
this.lastSource = source.src;
|
|
2035
|
+
}
|
|
2036
|
+
this.visible = true;
|
|
2037
|
+
this.retryBtn.disabled = false;
|
|
2038
|
+
this.el.classList.add("sp-error-overlay--reconnecting");
|
|
1956
2039
|
this.el.classList.add("sp-error-overlay--visible");
|
|
1957
2040
|
}
|
|
1958
2041
|
/** Hide the error overlay */
|
|
1959
2042
|
hide() {
|
|
1960
2043
|
this.visible = false;
|
|
1961
2044
|
this.el.classList.remove("sp-error-overlay--visible");
|
|
2045
|
+
this.el.classList.remove("sp-error-overlay--reconnecting");
|
|
1962
2046
|
}
|
|
1963
2047
|
isVisible() {
|
|
1964
2048
|
return this.visible;
|
|
@@ -2442,12 +2526,12 @@ var CaptionsButton = class {
|
|
|
2442
2526
|
}
|
|
2443
2527
|
this.el.style.display = "";
|
|
2444
2528
|
if (currentTrack) {
|
|
2445
|
-
this.el
|
|
2446
|
-
this.el
|
|
2529
|
+
setHTML(this.el, icons.captions);
|
|
2530
|
+
setAttr(this.el, "aria-label", `Captions: ${currentTrack.label}`);
|
|
2447
2531
|
this.el.classList.add("sp-captions--active");
|
|
2448
2532
|
} else {
|
|
2449
|
-
this.el
|
|
2450
|
-
this.el
|
|
2533
|
+
setHTML(this.el, icons.captionsOff);
|
|
2534
|
+
setAttr(this.el, "aria-label", "Captions");
|
|
2451
2535
|
this.el.classList.remove("sp-captions--active");
|
|
2452
2536
|
}
|
|
2453
2537
|
}
|
|
@@ -2530,7 +2614,10 @@ function uiPlugin(config = {}) {
|
|
|
2530
2614
|
let hideTimeout = null;
|
|
2531
2615
|
let stateUnsubscribe = null;
|
|
2532
2616
|
let errorUnsubscribe = null;
|
|
2617
|
+
let reconnectingUnsubscribe = null;
|
|
2618
|
+
let recoveredUnsubscribe = null;
|
|
2533
2619
|
let controlsVisible = true;
|
|
2620
|
+
let rafHandle = null;
|
|
2534
2621
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2535
2622
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
2536
2623
|
const createControl = (slot) => {
|
|
@@ -2582,6 +2669,13 @@ function uiPlugin(config = {}) {
|
|
|
2582
2669
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2583
2670
|
errorOverlay?.update();
|
|
2584
2671
|
};
|
|
2672
|
+
const scheduleUpdate = () => {
|
|
2673
|
+
if (rafHandle !== null) return;
|
|
2674
|
+
rafHandle = requestAnimationFrame(() => {
|
|
2675
|
+
rafHandle = null;
|
|
2676
|
+
updateControls();
|
|
2677
|
+
});
|
|
2678
|
+
};
|
|
2585
2679
|
const showControls = () => {
|
|
2586
2680
|
if (controlsVisible) {
|
|
2587
2681
|
resetHideTimer();
|
|
@@ -2631,7 +2725,12 @@ function uiPlugin(config = {}) {
|
|
|
2631
2725
|
case " ":
|
|
2632
2726
|
case "k":
|
|
2633
2727
|
e.preventDefault();
|
|
2634
|
-
video.paused
|
|
2728
|
+
if (video.paused) {
|
|
2729
|
+
video.play().catch(() => {
|
|
2730
|
+
});
|
|
2731
|
+
} else {
|
|
2732
|
+
video.pause();
|
|
2733
|
+
}
|
|
2635
2734
|
break;
|
|
2636
2735
|
case "m":
|
|
2637
2736
|
e.preventDefault();
|
|
@@ -2640,9 +2739,11 @@ function uiPlugin(config = {}) {
|
|
|
2640
2739
|
case "f":
|
|
2641
2740
|
e.preventDefault();
|
|
2642
2741
|
if (document.fullscreenElement) {
|
|
2643
|
-
document.exitFullscreen()
|
|
2742
|
+
document.exitFullscreen().catch(() => {
|
|
2743
|
+
});
|
|
2644
2744
|
} else {
|
|
2645
|
-
api.container.requestFullscreen?.()
|
|
2745
|
+
api.container.requestFullscreen?.().catch(() => {
|
|
2746
|
+
});
|
|
2646
2747
|
}
|
|
2647
2748
|
break;
|
|
2648
2749
|
case "ArrowLeft":
|
|
@@ -2710,10 +2811,16 @@ function uiPlugin(config = {}) {
|
|
|
2710
2811
|
container.appendChild(errorOverlay.render());
|
|
2711
2812
|
errorUnsubscribe = api.on("error", (payload) => {
|
|
2712
2813
|
if (payload?.fatal) {
|
|
2713
|
-
const error = api.getState("error") ||
|
|
2814
|
+
const error = api.getState("error") || payload;
|
|
2714
2815
|
errorOverlay?.show(error);
|
|
2715
2816
|
}
|
|
2716
2817
|
});
|
|
2818
|
+
reconnectingUnsubscribe = api.on("error:reconnecting", () => {
|
|
2819
|
+
errorOverlay?.showReconnecting();
|
|
2820
|
+
});
|
|
2821
|
+
recoveredUnsubscribe = api.on("error:recovered", () => {
|
|
2822
|
+
errorOverlay?.hide();
|
|
2823
|
+
});
|
|
2717
2824
|
progressBar = new ProgressBar(api);
|
|
2718
2825
|
container.appendChild(progressBar.render());
|
|
2719
2826
|
if (!isPlaying) {
|
|
@@ -2737,8 +2844,8 @@ function uiPlugin(config = {}) {
|
|
|
2737
2844
|
container.addEventListener("touchstart", handleInteraction, { passive: true });
|
|
2738
2845
|
container.addEventListener("click", handleInteraction);
|
|
2739
2846
|
document.addEventListener("keydown", handleKeyDown);
|
|
2740
|
-
stateUnsubscribe = api.subscribeToState(
|
|
2741
|
-
document.addEventListener("fullscreenchange",
|
|
2847
|
+
stateUnsubscribe = api.subscribeToState(scheduleUpdate);
|
|
2848
|
+
document.addEventListener("fullscreenchange", scheduleUpdate);
|
|
2742
2849
|
updateControls();
|
|
2743
2850
|
if (!container.hasAttribute("tabindex")) {
|
|
2744
2851
|
container.setAttribute("tabindex", "0");
|
|
@@ -2755,10 +2862,18 @@ function uiPlugin(config = {}) {
|
|
|
2755
2862
|
clearTimeout(hideTimeout);
|
|
2756
2863
|
hideTimeout = null;
|
|
2757
2864
|
}
|
|
2865
|
+
if (rafHandle !== null) {
|
|
2866
|
+
cancelAnimationFrame(rafHandle);
|
|
2867
|
+
rafHandle = null;
|
|
2868
|
+
}
|
|
2758
2869
|
stateUnsubscribe?.();
|
|
2759
2870
|
stateUnsubscribe = null;
|
|
2760
2871
|
errorUnsubscribe?.();
|
|
2761
2872
|
errorUnsubscribe = null;
|
|
2873
|
+
reconnectingUnsubscribe?.();
|
|
2874
|
+
reconnectingUnsubscribe = null;
|
|
2875
|
+
recoveredUnsubscribe?.();
|
|
2876
|
+
recoveredUnsubscribe = null;
|
|
2762
2877
|
if (api?.container) {
|
|
2763
2878
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2764
2879
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2767,7 +2882,7 @@ function uiPlugin(config = {}) {
|
|
|
2767
2882
|
api.container.removeEventListener("click", handleInteraction);
|
|
2768
2883
|
}
|
|
2769
2884
|
document.removeEventListener("keydown", handleKeyDown);
|
|
2770
|
-
document.removeEventListener("fullscreenchange",
|
|
2885
|
+
document.removeEventListener("fullscreenchange", scheduleUpdate);
|
|
2771
2886
|
controls.forEach((c) => c.destroy());
|
|
2772
2887
|
controls = [];
|
|
2773
2888
|
progressBar?.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scarlett-player/ui",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "UI Controls Plugin for Scarlett Player",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@scarlett-player/core": "^1.0.
|
|
25
|
+
"@scarlett-player/core": "^1.0.3"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"tsup": "^8.0.0",
|
|
29
29
|
"typescript": "^5.3.0",
|
|
30
30
|
"vitest": "^1.6.0",
|
|
31
31
|
"jsdom": "^24.0.0",
|
|
32
|
-
"@scarlett-player/core": "1.0
|
|
32
|
+
"@scarlett-player/core": "1.2.0"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"video",
|