bobinejs 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +114 -3
- package/dist/core.d.ts +85 -0
- package/dist/core.js +381 -0
- package/dist/core.js.map +1 -0
- package/dist/hls.d.ts +16 -0
- package/dist/hls.js +39 -0
- package/dist/hls.js.map +1 -0
- package/dist/index.d.ts +289 -0
- package/dist/index.js +1720 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +17 -0
- package/package.json +65 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/utils.ts
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
function cn(...inputs) {
|
|
7
|
+
return twMerge(clsx(inputs));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ../../components/ui/media-icons.tsx
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
var base = {
|
|
14
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
15
|
+
width: 16,
|
|
16
|
+
height: 16,
|
|
17
|
+
viewBox: "0 0 24 24",
|
|
18
|
+
"aria-hidden": true
|
|
19
|
+
};
|
|
20
|
+
function PlayIcon(props) {
|
|
21
|
+
return /* @__PURE__ */ jsx("svg", { ...base, fill: "currentColor", ...props, children: /* @__PURE__ */ jsx("path", { d: "M6.87 4.9v14.2a1.5 1.5 0 0 0 2.31 1.26l11.07-7.1a1.5 1.5 0 0 0 0-2.52L9.18 3.64A1.5 1.5 0 0 0 6.87 4.9Z" }) });
|
|
22
|
+
}
|
|
23
|
+
function PauseIcon(props) {
|
|
24
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "currentColor", ...props, children: [
|
|
25
|
+
/* @__PURE__ */ jsx("rect", { x: "5", y: "3.5", width: "5", height: "17", rx: "1.6" }),
|
|
26
|
+
/* @__PURE__ */ jsx("rect", { x: "14", y: "3.5", width: "5", height: "17", rx: "1.6" })
|
|
27
|
+
] });
|
|
28
|
+
}
|
|
29
|
+
function VolumeIcon({ muted: muted2, ...props }) {
|
|
30
|
+
const fade = "origin-[11px_12px] transition-[opacity,transform] duration-200 ease-media motion-reduce:transition-none";
|
|
31
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
|
|
32
|
+
/* @__PURE__ */ jsx(
|
|
33
|
+
"path",
|
|
34
|
+
{
|
|
35
|
+
d: "M11 5.1v13.8a1 1 0 0 1-1.63.77L5.5 16.5h-2A1.5 1.5 0 0 1 2 15V9a1.5 1.5 0 0 1 1.5-1.5h2l3.87-3.17A1 1 0 0 1 11 5.1Z",
|
|
36
|
+
fill: "currentColor",
|
|
37
|
+
stroke: "none"
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
/* @__PURE__ */ jsxs("g", { className: fade, style: { opacity: muted2 ? 0 : 1, transform: muted2 ? "scale(0.6)" : "none" }, children: [
|
|
41
|
+
/* @__PURE__ */ jsx("path", { d: "M15 9.5a3.5 3.5 0 0 1 0 5" }),
|
|
42
|
+
/* @__PURE__ */ jsx("path", { d: "M17.8 6.7a7.5 7.5 0 0 1 0 10.6" })
|
|
43
|
+
] }),
|
|
44
|
+
/* @__PURE__ */ jsxs("g", { className: fade, style: { opacity: muted2 ? 1 : 0, transform: muted2 ? "none" : "scale(0.6)" }, children: [
|
|
45
|
+
/* @__PURE__ */ jsx("path", { d: "m16 9.5 5 5" }),
|
|
46
|
+
/* @__PURE__ */ jsx("path", { d: "m21 9.5-5 5" })
|
|
47
|
+
] })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
50
|
+
function EnterFullscreenIcon(props) {
|
|
51
|
+
return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "M4 9V6a2 2 0 0 1 2-2h3M15 4h3a2 2 0 0 1 2 2v3M20 15v3a2 2 0 0 1-2 2h-3M9 20H6a2 2 0 0 1-2-2v-3" }) });
|
|
52
|
+
}
|
|
53
|
+
function ExitFullscreenIcon(props) {
|
|
54
|
+
return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "M9 4v3a2 2 0 0 1-2 2H4M20 9h-3a2 2 0 0 1-2-2V4M15 20v-3a2 2 0 0 1 2-2h3M4 15h3a2 2 0 0 1 2 2v3" }) });
|
|
55
|
+
}
|
|
56
|
+
function PictureInPictureIcon(props) {
|
|
57
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
|
|
58
|
+
/* @__PURE__ */ jsx("rect", { x: "2.5", y: "4.5", width: "19", height: "15", rx: "2.5" }),
|
|
59
|
+
/* @__PURE__ */ jsx("rect", { x: "12", y: "11.5", width: "6.5", height: "5", rx: "1", fill: "currentColor", stroke: "none" })
|
|
60
|
+
] });
|
|
61
|
+
}
|
|
62
|
+
var captionLetters = "M10.5 10.2a2.2 2.2 0 1 0 0 3.6M17 10.2a2.2 2.2 0 1 0 0 3.6";
|
|
63
|
+
function CaptionsIcon({ active, ...props }) {
|
|
64
|
+
const maskId = `cc${React.useId().replace(/[^\w-]/g, "")}`;
|
|
65
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
|
|
66
|
+
/* @__PURE__ */ jsxs("mask", { id: maskId, children: [
|
|
67
|
+
/* @__PURE__ */ jsx("rect", { width: "24", height: "24", fill: "white" }),
|
|
68
|
+
/* @__PURE__ */ jsx("path", { d: captionLetters, stroke: "black" })
|
|
69
|
+
] }),
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
"rect",
|
|
72
|
+
{
|
|
73
|
+
x: "2.5",
|
|
74
|
+
y: "4.5",
|
|
75
|
+
width: "19",
|
|
76
|
+
height: "15",
|
|
77
|
+
rx: "3",
|
|
78
|
+
fill: "currentColor",
|
|
79
|
+
mask: `url(#${maskId})`,
|
|
80
|
+
className: "transition-opacity duration-200 ease-media motion-reduce:transition-none",
|
|
81
|
+
style: { opacity: active ? 1 : 0 }
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsx("rect", { x: "2.5", y: "4.5", width: "19", height: "15", rx: "3" }),
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
"path",
|
|
87
|
+
{
|
|
88
|
+
d: captionLetters,
|
|
89
|
+
className: "transition-opacity duration-200 ease-media motion-reduce:transition-none",
|
|
90
|
+
style: { opacity: active ? 0 : 1 }
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function SettingsIcon(props) {
|
|
96
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
|
|
97
|
+
/* @__PURE__ */ jsx("path", { d: "M4 7h9M17 7h3M4 17h3M11 17h9" }),
|
|
98
|
+
/* @__PURE__ */ jsx("circle", { cx: "15", cy: "7", r: "2" }),
|
|
99
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "17", r: "2" })
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
function RetryIcon(props) {
|
|
103
|
+
return /* @__PURE__ */ jsxs("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: [
|
|
104
|
+
/* @__PURE__ */ jsx("path", { d: "M3.5 12a8.5 8.5 0 1 0 2.6-6.1" }),
|
|
105
|
+
/* @__PURE__ */ jsx("path", { d: "M3.5 4v4.5H8" })
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
108
|
+
function CheckIcon(props) {
|
|
109
|
+
return /* @__PURE__ */ jsx("svg", { ...base, fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round", ...props, children: /* @__PURE__ */ jsx("path", { d: "m5 12.5 4.5 4.5L19 7.5" }) });
|
|
110
|
+
}
|
|
111
|
+
function SeekIcon({ direction, ...props }) {
|
|
112
|
+
return /* @__PURE__ */ jsx("svg", { ...base, fill: "currentColor", ...props, style: { transform: direction === "back" ? "scaleX(-1)" : void 0, ...props.style }, children: /* @__PURE__ */ jsx("path", { d: "M3 6.7v10.6a1 1 0 0 0 1.55.83L12 13.2v4.1a1 1 0 0 0 1.55.83l7.9-5.3a1 1 0 0 0 0-1.66l-7.9-5.3A1 1 0 0 0 12 6.7v4.1L4.55 5.87A1 1 0 0 0 3 6.7Z" }) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ../../components/ui/media.tsx
|
|
116
|
+
import * as React2 from "react";
|
|
117
|
+
|
|
118
|
+
// ../../lib/media-store.ts
|
|
119
|
+
var MEDIA_EVENTS = [
|
|
120
|
+
"play",
|
|
121
|
+
"pause",
|
|
122
|
+
"playing",
|
|
123
|
+
"ended",
|
|
124
|
+
"waiting",
|
|
125
|
+
"canplay",
|
|
126
|
+
"seeking",
|
|
127
|
+
"seeked",
|
|
128
|
+
"timeupdate",
|
|
129
|
+
"durationchange",
|
|
130
|
+
"loadedmetadata",
|
|
131
|
+
"progress",
|
|
132
|
+
"volumechange",
|
|
133
|
+
"ratechange",
|
|
134
|
+
"emptied",
|
|
135
|
+
"loadstart",
|
|
136
|
+
"error",
|
|
137
|
+
"enterpictureinpicture",
|
|
138
|
+
"leavepictureinpicture"
|
|
139
|
+
];
|
|
140
|
+
var TRACK_EVENTS = ["addtrack", "removetrack", "change"];
|
|
141
|
+
var initialState = {
|
|
142
|
+
paused: true,
|
|
143
|
+
ended: false,
|
|
144
|
+
started: false,
|
|
145
|
+
waiting: false,
|
|
146
|
+
currentTime: 0,
|
|
147
|
+
duration: 0,
|
|
148
|
+
bufferedEnd: 0,
|
|
149
|
+
volume: 1,
|
|
150
|
+
muted: false,
|
|
151
|
+
playbackRate: 1,
|
|
152
|
+
fullscreen: false,
|
|
153
|
+
pictureInPicture: false,
|
|
154
|
+
error: null,
|
|
155
|
+
hasSource: false,
|
|
156
|
+
textTracks: [],
|
|
157
|
+
textTrack: -1,
|
|
158
|
+
cues: []
|
|
159
|
+
};
|
|
160
|
+
function readBufferedEnd(media) {
|
|
161
|
+
const { buffered, currentTime } = media;
|
|
162
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
163
|
+
if (buffered.start(i) <= currentTime && buffered.end(i) >= currentTime) {
|
|
164
|
+
return buffered.end(i);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return buffered.length ? buffered.end(buffered.length - 1) : 0;
|
|
168
|
+
}
|
|
169
|
+
function captionTracks(media) {
|
|
170
|
+
if (!media?.textTracks) return [];
|
|
171
|
+
return Array.from(media.textTracks).filter(
|
|
172
|
+
(track) => track.kind === "captions" || track.kind === "subtitles"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
function cueText(cue) {
|
|
176
|
+
const vtt = cue;
|
|
177
|
+
const text = typeof vtt.getCueAsHTML === "function" ? vtt.getCueAsHTML().textContent : vtt.text?.replace(/<[^>]+>/g, "");
|
|
178
|
+
return (text ?? "").trim();
|
|
179
|
+
}
|
|
180
|
+
function defaultTrack(media, tracks) {
|
|
181
|
+
const el = Array.from(media?.querySelectorAll("track") ?? []).find((track) => track.default);
|
|
182
|
+
return el ? tracks.indexOf(el.track) : -1;
|
|
183
|
+
}
|
|
184
|
+
var sameList = (a, b) => a.length === b.length && a.every((value, i) => Object.is(value, b[i]));
|
|
185
|
+
var sameTracks = (a, b) => a.length === b.length && a.every((t, i) => t.label === b[i].label && t.language === b[i].language && t.kind === b[i].kind);
|
|
186
|
+
function readPreferences(key) {
|
|
187
|
+
try {
|
|
188
|
+
return JSON.parse(localStorage.getItem(key) ?? "{}");
|
|
189
|
+
} catch {
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function createMediaStore({ persistKey } = {}) {
|
|
194
|
+
let state = initialState;
|
|
195
|
+
let media = null;
|
|
196
|
+
let container = null;
|
|
197
|
+
let cueTrack = null;
|
|
198
|
+
let metadata = null;
|
|
199
|
+
let trackChosen = false;
|
|
200
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
201
|
+
const set = (patch) => {
|
|
202
|
+
let changed = false;
|
|
203
|
+
for (const key in patch) {
|
|
204
|
+
const k = key;
|
|
205
|
+
const next = patch[k];
|
|
206
|
+
const prev = state[k];
|
|
207
|
+
if (k === "cues" ? !sameList(prev, next) : k === "textTracks" ? !sameTracks(prev, next) : !Object.is(prev, next)) {
|
|
208
|
+
changed = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!changed) return;
|
|
213
|
+
state = { ...state, ...patch };
|
|
214
|
+
listeners.forEach((l) => l());
|
|
215
|
+
};
|
|
216
|
+
const sync = () => {
|
|
217
|
+
if (!media) return;
|
|
218
|
+
set({
|
|
219
|
+
paused: media.paused,
|
|
220
|
+
ended: media.ended,
|
|
221
|
+
started: state.started || !media.paused || media.currentTime > 0,
|
|
222
|
+
currentTime: media.currentTime,
|
|
223
|
+
duration: Number.isFinite(media.duration) ? media.duration : 0,
|
|
224
|
+
bufferedEnd: readBufferedEnd(media),
|
|
225
|
+
volume: media.volume,
|
|
226
|
+
muted: media.muted,
|
|
227
|
+
playbackRate: media.playbackRate,
|
|
228
|
+
pictureInPicture: typeof document !== "undefined" && document.pictureInPictureElement === media,
|
|
229
|
+
error: media.error,
|
|
230
|
+
hasSource: Boolean(media.src || media.currentSrc || media.querySelector("source[src]"))
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const readCues = () => {
|
|
234
|
+
const active = cueTrack?.activeCues;
|
|
235
|
+
set({ cues: active ? Array.from(active).map(cueText).filter(Boolean) : [] });
|
|
236
|
+
};
|
|
237
|
+
const bindCueTrack = (track) => {
|
|
238
|
+
if (cueTrack === track) return;
|
|
239
|
+
cueTrack?.removeEventListener("cuechange", readCues);
|
|
240
|
+
cueTrack = track;
|
|
241
|
+
cueTrack?.addEventListener("cuechange", readCues);
|
|
242
|
+
};
|
|
243
|
+
const syncTracks = (requested = state.textTrack) => {
|
|
244
|
+
const tracks = captionTracks(media);
|
|
245
|
+
let active = requested === -1 && !trackChosen ? defaultTrack(media, tracks) : requested;
|
|
246
|
+
tracks.forEach((track, i) => {
|
|
247
|
+
if (track.mode === "showing") active = i;
|
|
248
|
+
});
|
|
249
|
+
if (active >= tracks.length) active = -1;
|
|
250
|
+
tracks.forEach((track, i) => {
|
|
251
|
+
const mode = i === active ? "hidden" : "disabled";
|
|
252
|
+
if (track.mode !== mode) track.mode = mode;
|
|
253
|
+
});
|
|
254
|
+
bindCueTrack(tracks[active] ?? null);
|
|
255
|
+
set({
|
|
256
|
+
textTracks: tracks.map((track, i) => ({
|
|
257
|
+
label: track.label || track.language || `Track ${i + 1}`,
|
|
258
|
+
language: track.language,
|
|
259
|
+
kind: track.kind
|
|
260
|
+
})),
|
|
261
|
+
textTrack: active
|
|
262
|
+
});
|
|
263
|
+
readCues();
|
|
264
|
+
};
|
|
265
|
+
const onTracksChange = () => syncTracks();
|
|
266
|
+
const applyMediaSession = () => {
|
|
267
|
+
if (typeof navigator === "undefined" || !("mediaSession" in navigator) || !metadata) return;
|
|
268
|
+
const session = navigator.mediaSession;
|
|
269
|
+
session.metadata = new MediaMetadata(metadata);
|
|
270
|
+
const handlers = [
|
|
271
|
+
["play", () => actions.play()],
|
|
272
|
+
["pause", () => actions.pause()],
|
|
273
|
+
["seekbackward", (d) => actions.seekBy(-(d.seekOffset ?? 10))],
|
|
274
|
+
["seekforward", (d) => actions.seekBy(d.seekOffset ?? 10)],
|
|
275
|
+
["seekto", (d) => d.seekTime !== void 0 && actions.seek(d.seekTime)]
|
|
276
|
+
];
|
|
277
|
+
for (const [action, handler] of handlers) {
|
|
278
|
+
try {
|
|
279
|
+
session.setActionHandler(action, handler);
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const updatePositionState = () => {
|
|
285
|
+
if (!metadata || !media || typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
|
|
286
|
+
if (!Number.isFinite(media.duration) || media.duration <= 0) return;
|
|
287
|
+
try {
|
|
288
|
+
navigator.mediaSession.setPositionState({
|
|
289
|
+
duration: media.duration,
|
|
290
|
+
playbackRate: media.playbackRate,
|
|
291
|
+
position: Math.min(media.currentTime, media.duration)
|
|
292
|
+
});
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const savePreferences = () => {
|
|
297
|
+
if (!persistKey || !media) return;
|
|
298
|
+
try {
|
|
299
|
+
const prefs = {
|
|
300
|
+
volume: media.volume,
|
|
301
|
+
muted: media.muted,
|
|
302
|
+
playbackRate: media.playbackRate
|
|
303
|
+
};
|
|
304
|
+
localStorage.setItem(persistKey, JSON.stringify(prefs));
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const onEvent = (event) => {
|
|
309
|
+
switch (event.type) {
|
|
310
|
+
case "emptied":
|
|
311
|
+
set({ started: false, waiting: false });
|
|
312
|
+
break;
|
|
313
|
+
case "waiting":
|
|
314
|
+
set({ waiting: true });
|
|
315
|
+
break;
|
|
316
|
+
case "playing":
|
|
317
|
+
case "canplay":
|
|
318
|
+
case "pause":
|
|
319
|
+
case "error":
|
|
320
|
+
set({ waiting: false });
|
|
321
|
+
break;
|
|
322
|
+
case "play":
|
|
323
|
+
applyMediaSession();
|
|
324
|
+
break;
|
|
325
|
+
case "volumechange":
|
|
326
|
+
case "ratechange":
|
|
327
|
+
savePreferences();
|
|
328
|
+
break;
|
|
329
|
+
case "loadedmetadata":
|
|
330
|
+
syncTracks();
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (event.type === "durationchange" || event.type === "ratechange" || event.type === "seeked") {
|
|
334
|
+
updatePositionState();
|
|
335
|
+
}
|
|
336
|
+
sync();
|
|
337
|
+
};
|
|
338
|
+
const onFullscreenChange = () => {
|
|
339
|
+
const el = document.fullscreenElement;
|
|
340
|
+
const video = media;
|
|
341
|
+
set({
|
|
342
|
+
fullscreen: !!el && (el === container || el === media) || !!video?.webkitDisplayingFullscreen
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
const attach = (el) => {
|
|
346
|
+
if (media === el) return;
|
|
347
|
+
if (media) {
|
|
348
|
+
MEDIA_EVENTS.forEach((e) => media.removeEventListener(e, onEvent));
|
|
349
|
+
TRACK_EVENTS.forEach((e) => media.textTracks?.removeEventListener?.(e, onTracksChange));
|
|
350
|
+
media.removeEventListener("webkitbeginfullscreen", onFullscreenChange);
|
|
351
|
+
media.removeEventListener("webkitendfullscreen", onFullscreenChange);
|
|
352
|
+
bindCueTrack(null);
|
|
353
|
+
}
|
|
354
|
+
media = el;
|
|
355
|
+
if (!el) {
|
|
356
|
+
state = initialState;
|
|
357
|
+
listeners.forEach((l) => l());
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (persistKey) {
|
|
361
|
+
const prefs = readPreferences(persistKey);
|
|
362
|
+
if (typeof prefs.volume === "number") el.volume = Math.min(Math.max(prefs.volume, 0), 1);
|
|
363
|
+
if (typeof prefs.muted === "boolean") el.muted = prefs.muted;
|
|
364
|
+
if (typeof prefs.playbackRate === "number") el.playbackRate = prefs.playbackRate;
|
|
365
|
+
}
|
|
366
|
+
MEDIA_EVENTS.forEach((e) => el.addEventListener(e, onEvent));
|
|
367
|
+
TRACK_EVENTS.forEach((e) => el.textTracks?.addEventListener?.(e, onTracksChange));
|
|
368
|
+
el.addEventListener("webkitbeginfullscreen", onFullscreenChange);
|
|
369
|
+
el.addEventListener("webkitendfullscreen", onFullscreenChange);
|
|
370
|
+
sync();
|
|
371
|
+
syncTracks();
|
|
372
|
+
};
|
|
373
|
+
const setContainer = (el) => {
|
|
374
|
+
if (typeof document === "undefined") return;
|
|
375
|
+
document.removeEventListener("fullscreenchange", onFullscreenChange);
|
|
376
|
+
container = el;
|
|
377
|
+
if (el) document.addEventListener("fullscreenchange", onFullscreenChange);
|
|
378
|
+
};
|
|
379
|
+
const play = async () => {
|
|
380
|
+
if (!media) return;
|
|
381
|
+
try {
|
|
382
|
+
await media.play();
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const pause = () => media?.pause();
|
|
387
|
+
const actions = {
|
|
388
|
+
play,
|
|
389
|
+
pause,
|
|
390
|
+
toggle: () => media?.paused || media?.ended ? play() : pause(),
|
|
391
|
+
seek: (time) => {
|
|
392
|
+
if (!media) return;
|
|
393
|
+
const max = Number.isFinite(media.duration) ? media.duration : time;
|
|
394
|
+
media.currentTime = Math.min(Math.max(time, 0), max);
|
|
395
|
+
sync();
|
|
396
|
+
},
|
|
397
|
+
seekBy: (delta) => actions.seek((media?.currentTime ?? 0) + delta),
|
|
398
|
+
/** Jump to a fraction (0–1) of the duration. */
|
|
399
|
+
seekToRatio: (ratio) => actions.seek(ratio * (media?.duration || 0)),
|
|
400
|
+
setVolume: (volume) => {
|
|
401
|
+
if (!media) return;
|
|
402
|
+
media.volume = Math.min(Math.max(volume, 0), 1);
|
|
403
|
+
media.muted = media.volume === 0;
|
|
404
|
+
},
|
|
405
|
+
toggleMute: () => {
|
|
406
|
+
if (!media) return;
|
|
407
|
+
if (media.muted && media.volume === 0) media.volume = 1;
|
|
408
|
+
media.muted = !media.muted;
|
|
409
|
+
},
|
|
410
|
+
setPlaybackRate: (rate) => {
|
|
411
|
+
if (media) media.playbackRate = rate;
|
|
412
|
+
},
|
|
413
|
+
/** Select a caption track by index, or -1 to turn captions off. */
|
|
414
|
+
setTextTrack: (index) => syncTracksExplicit(index),
|
|
415
|
+
toggleCaptions: () => {
|
|
416
|
+
if (!state.textTracks.length) return;
|
|
417
|
+
syncTracksExplicit(state.textTrack === -1 ? 0 : -1);
|
|
418
|
+
},
|
|
419
|
+
/** Reload the current source after an error. */
|
|
420
|
+
retry: () => {
|
|
421
|
+
if (!media) return;
|
|
422
|
+
media.load();
|
|
423
|
+
void play();
|
|
424
|
+
},
|
|
425
|
+
setMetadata: (next) => {
|
|
426
|
+
metadata = next;
|
|
427
|
+
if (media && !media.paused) applyMediaSession();
|
|
428
|
+
},
|
|
429
|
+
toggleFullscreen: async () => {
|
|
430
|
+
if (typeof document === "undefined") return;
|
|
431
|
+
if (document.fullscreenElement) {
|
|
432
|
+
await document.exitFullscreen().catch(() => {
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (container?.requestFullscreen) {
|
|
437
|
+
await container.requestFullscreen().catch(() => {
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
;
|
|
442
|
+
media?.webkitEnterFullscreen?.();
|
|
443
|
+
},
|
|
444
|
+
togglePictureInPicture: async () => {
|
|
445
|
+
if (!(media instanceof HTMLVideoElement)) return;
|
|
446
|
+
try {
|
|
447
|
+
if (document.pictureInPictureElement) await document.exitPictureInPicture();
|
|
448
|
+
else await media.requestPictureInPicture();
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
const syncTracksExplicit = (index) => {
|
|
454
|
+
trackChosen = true;
|
|
455
|
+
captionTracks(media).forEach((track, i) => {
|
|
456
|
+
if (i !== index && track.mode === "showing") track.mode = "disabled";
|
|
457
|
+
});
|
|
458
|
+
syncTracks(index);
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
getState: () => state,
|
|
462
|
+
getServerState: () => initialState,
|
|
463
|
+
getMedia: () => media,
|
|
464
|
+
subscribe: (listener) => {
|
|
465
|
+
listeners.add(listener);
|
|
466
|
+
return () => listeners.delete(listener);
|
|
467
|
+
},
|
|
468
|
+
attach,
|
|
469
|
+
setContainer,
|
|
470
|
+
...actions
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function formatTime(seconds, reference = seconds) {
|
|
474
|
+
if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
|
|
475
|
+
const s = Math.floor(seconds % 60);
|
|
476
|
+
const m = Math.floor(seconds / 60 % 60);
|
|
477
|
+
const h = Math.floor(seconds / 3600);
|
|
478
|
+
const ss = s.toString().padStart(2, "0");
|
|
479
|
+
const hours = h > 0 || Number.isFinite(reference) && reference >= 3600;
|
|
480
|
+
return hours ? `${h}:${m.toString().padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
481
|
+
}
|
|
482
|
+
function describeMediaError(error) {
|
|
483
|
+
switch (error?.code) {
|
|
484
|
+
case 1:
|
|
485
|
+
return "Playback was aborted.";
|
|
486
|
+
case 2:
|
|
487
|
+
return "A network error stopped the download.";
|
|
488
|
+
case 3:
|
|
489
|
+
return "This media could not be decoded.";
|
|
490
|
+
case 4:
|
|
491
|
+
return "This media couldn't be loaded.";
|
|
492
|
+
default:
|
|
493
|
+
return "Something went wrong during playback.";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ../../components/ui/media.tsx
|
|
498
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
499
|
+
var MediaContext = React2.createContext(null);
|
|
500
|
+
var MediaOptionsContext = React2.createContext({ tooltips: false, hotkeys: false });
|
|
501
|
+
function useMediaStore() {
|
|
502
|
+
const store = React2.useContext(MediaContext);
|
|
503
|
+
if (!store) throw new Error("Media components must be rendered inside <MediaRoot>.");
|
|
504
|
+
return store;
|
|
505
|
+
}
|
|
506
|
+
function useMedia(selector) {
|
|
507
|
+
const store = useMediaStore();
|
|
508
|
+
return React2.useSyncExternalStore(
|
|
509
|
+
store.subscribe,
|
|
510
|
+
() => selector(store.getState()),
|
|
511
|
+
() => selector(store.getServerState())
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
function useComposedRefs(a, b, c) {
|
|
515
|
+
return React2.useCallback((node) => composeRefs(a, b, c)(node), [a, b, c]);
|
|
516
|
+
}
|
|
517
|
+
function composeRefs(...refs) {
|
|
518
|
+
return (node) => {
|
|
519
|
+
const cleanups = refs.map((ref) => {
|
|
520
|
+
if (typeof ref === "function") return ref(node);
|
|
521
|
+
if (ref) ref.current = node;
|
|
522
|
+
});
|
|
523
|
+
return () => {
|
|
524
|
+
cleanups.forEach((cleanup, i) => {
|
|
525
|
+
const ref = refs[i];
|
|
526
|
+
if (typeof cleanup === "function") cleanup();
|
|
527
|
+
else if (typeof ref === "function") ref(null);
|
|
528
|
+
else if (ref) ref.current = null;
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
var clamp = (value) => Math.min(Math.max(value, 0), 1);
|
|
534
|
+
var SETTINGS_OPEN = "[data-slot='media-settings'][data-open]";
|
|
535
|
+
var holdAwakeSelector = () => window.matchMedia("(hover: hover)").matches ? `[data-slot='media-controls']:hover, ${SETTINGS_OPEN}` : SETTINGS_OPEN;
|
|
536
|
+
function MediaRoot({
|
|
537
|
+
className,
|
|
538
|
+
idleTimeout = 2e3,
|
|
539
|
+
persistKey,
|
|
540
|
+
metadata,
|
|
541
|
+
hotkeys = true,
|
|
542
|
+
tooltips = false,
|
|
543
|
+
onPointerMove,
|
|
544
|
+
onPointerDown,
|
|
545
|
+
onPointerLeave,
|
|
546
|
+
onKeyDown,
|
|
547
|
+
ref,
|
|
548
|
+
...props
|
|
549
|
+
}) {
|
|
550
|
+
const [store] = React2.useState(() => createMediaStore({ persistKey }));
|
|
551
|
+
const [active, setActive] = React2.useState(false);
|
|
552
|
+
const timer = React2.useRef(void 0);
|
|
553
|
+
const rootEl = React2.useRef(null);
|
|
554
|
+
const state = React2.useSyncExternalStore(store.subscribe, store.getState, store.getServerState);
|
|
555
|
+
const rootRef = useComposedRefs(ref, rootEl, store.setContainer);
|
|
556
|
+
const options = React2.useMemo(() => ({ tooltips, hotkeys }), [tooltips, hotkeys]);
|
|
557
|
+
React2.useEffect(() => () => clearTimeout(timer.current), []);
|
|
558
|
+
const { title, artist, album, artwork } = metadata ?? {};
|
|
559
|
+
const artworkKey = JSON.stringify(artwork ?? null);
|
|
560
|
+
React2.useEffect(() => {
|
|
561
|
+
store.setMetadata(title || artist || album ? { title, artist, album, artwork: JSON.parse(artworkKey) ?? void 0 } : null);
|
|
562
|
+
}, [store, title, artist, album, artworkKey]);
|
|
563
|
+
const wake = () => {
|
|
564
|
+
setActive(true);
|
|
565
|
+
clearTimeout(timer.current);
|
|
566
|
+
timer.current = setTimeout(function sleep() {
|
|
567
|
+
if (rootEl.current?.querySelector(holdAwakeSelector())) {
|
|
568
|
+
timer.current = setTimeout(sleep, idleTimeout);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setActive(false);
|
|
572
|
+
}, idleTimeout);
|
|
573
|
+
};
|
|
574
|
+
const handleKeyDown = (event) => {
|
|
575
|
+
onKeyDown?.(event);
|
|
576
|
+
if (!hotkeys || event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
577
|
+
const target = event.target;
|
|
578
|
+
if (target.closest("input, textarea, select, [contenteditable='true'], [data-slot='media-settings']")) return;
|
|
579
|
+
const onButton = target.tagName === "BUTTON";
|
|
580
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
581
|
+
const keymap = {
|
|
582
|
+
" ": () => store.toggle(),
|
|
583
|
+
k: () => store.toggle(),
|
|
584
|
+
j: () => store.seekBy(-10),
|
|
585
|
+
l: () => store.seekBy(10),
|
|
586
|
+
ArrowLeft: () => store.seekBy(-5),
|
|
587
|
+
ArrowRight: () => store.seekBy(5),
|
|
588
|
+
ArrowUp: () => store.setVolume(store.getState().volume + 0.1),
|
|
589
|
+
ArrowDown: () => store.setVolume(store.getState().volume - 0.1),
|
|
590
|
+
m: () => store.toggleMute(),
|
|
591
|
+
f: () => store.toggleFullscreen(),
|
|
592
|
+
c: () => store.toggleCaptions(),
|
|
593
|
+
i: () => store.togglePictureInPicture(),
|
|
594
|
+
Home: () => store.seek(0),
|
|
595
|
+
End: () => store.seekToRatio(1)
|
|
596
|
+
};
|
|
597
|
+
for (let digit = 0; digit <= 9; digit++) {
|
|
598
|
+
keymap[String(digit)] = () => store.seekToRatio(digit / 10);
|
|
599
|
+
}
|
|
600
|
+
const action = keymap[key];
|
|
601
|
+
if (!action || key === " " && onButton) return;
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
action();
|
|
604
|
+
wake();
|
|
605
|
+
};
|
|
606
|
+
const idle = !active && !state.paused;
|
|
607
|
+
return /* @__PURE__ */ jsx2(MediaContext.Provider, { value: store, children: /* @__PURE__ */ jsx2(MediaOptionsContext.Provider, { value: options, children: /* @__PURE__ */ jsx2(
|
|
608
|
+
"div",
|
|
609
|
+
{
|
|
610
|
+
ref: rootRef,
|
|
611
|
+
"data-slot": "media-root",
|
|
612
|
+
"data-paused": state.paused || void 0,
|
|
613
|
+
"data-playing": !state.paused || void 0,
|
|
614
|
+
"data-started": state.started || void 0,
|
|
615
|
+
"data-waiting": state.waiting || void 0,
|
|
616
|
+
"data-fullscreen": state.fullscreen || void 0,
|
|
617
|
+
"data-error": state.error ? "" : void 0,
|
|
618
|
+
"data-captions": state.textTrack !== -1 && state.cues.length > 0 ? "" : void 0,
|
|
619
|
+
"data-idle": idle || void 0,
|
|
620
|
+
"data-active": active || void 0,
|
|
621
|
+
className: cn(
|
|
622
|
+
/* no gray tap flash, no long-press callout over the video, and no
|
|
623
|
+
double-tap-zoom wait before a tap registers */
|
|
624
|
+
"group/media relative isolate touch-manipulation [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none]",
|
|
625
|
+
idle && "cursor-none",
|
|
626
|
+
className
|
|
627
|
+
),
|
|
628
|
+
onPointerMove: (event) => {
|
|
629
|
+
onPointerMove?.(event);
|
|
630
|
+
if (event.pointerType === "mouse") wake();
|
|
631
|
+
},
|
|
632
|
+
onPointerDown: (event) => {
|
|
633
|
+
onPointerDown?.(event);
|
|
634
|
+
wake();
|
|
635
|
+
},
|
|
636
|
+
onPointerLeave: (event) => {
|
|
637
|
+
onPointerLeave?.(event);
|
|
638
|
+
if (event.pointerType !== "mouse") return;
|
|
639
|
+
clearTimeout(timer.current);
|
|
640
|
+
setActive(false);
|
|
641
|
+
},
|
|
642
|
+
onKeyDown: handleKeyDown,
|
|
643
|
+
...props
|
|
644
|
+
}
|
|
645
|
+
) }) });
|
|
646
|
+
}
|
|
647
|
+
function useMediaElement(ref, src, loader) {
|
|
648
|
+
const store = useMediaStore();
|
|
649
|
+
const mediaRef = useComposedRefs(ref, store.attach);
|
|
650
|
+
React2.useEffect(() => {
|
|
651
|
+
const media = store.getMedia();
|
|
652
|
+
if (!loader || !src || !media) return;
|
|
653
|
+
return loader(media, src) ?? void 0;
|
|
654
|
+
}, [store, loader, src]);
|
|
655
|
+
return mediaRef;
|
|
656
|
+
}
|
|
657
|
+
function MediaVideo({ className, ref, src, loader, ...props }) {
|
|
658
|
+
const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
|
|
659
|
+
return /* @__PURE__ */ jsx2(
|
|
660
|
+
"video",
|
|
661
|
+
{
|
|
662
|
+
ref: mediaRef,
|
|
663
|
+
"data-slot": "media-video",
|
|
664
|
+
playsInline: true,
|
|
665
|
+
preload: "metadata",
|
|
666
|
+
...props,
|
|
667
|
+
src: loader ? void 0 : src,
|
|
668
|
+
controls: false,
|
|
669
|
+
className: cn("block", className)
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
function MediaAudio({ ref, src, loader, ...props }) {
|
|
674
|
+
const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
|
|
675
|
+
return /* @__PURE__ */ jsx2(
|
|
676
|
+
"audio",
|
|
677
|
+
{
|
|
678
|
+
ref: mediaRef,
|
|
679
|
+
"data-slot": "media-audio",
|
|
680
|
+
preload: "metadata",
|
|
681
|
+
...props,
|
|
682
|
+
src: loader ? void 0 : src,
|
|
683
|
+
controls: false
|
|
684
|
+
}
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
var buttonClass = "pointer-coarse:after:absolute pointer-coarse:after:-inset-1 pointer-coarse:after:content-[''] group/media-button relative inline-grid size-7 shrink-0 cursor-pointer place-items-center rounded-md outline-none transition-[background-color,scale,opacity] duration-150 ease-media hover:bg-current/15 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:enabled:active:scale-[0.96] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
688
|
+
function MediaTooltip({ label, shortcut, align = "center" }) {
|
|
689
|
+
const { tooltips, hotkeys } = React2.useContext(MediaOptionsContext);
|
|
690
|
+
if (!tooltips || !label) return null;
|
|
691
|
+
return /* @__PURE__ */ jsxs2(
|
|
692
|
+
"span",
|
|
693
|
+
{
|
|
694
|
+
"aria-hidden": "true",
|
|
695
|
+
"data-slot": "media-tooltip",
|
|
696
|
+
className: cn(
|
|
697
|
+
"pointer-events-none absolute bottom-full mb-2 flex items-center gap-1.5 rounded-md bg-foreground px-1.5 py-0.5 text-[11px] leading-4 font-medium whitespace-nowrap text-background opacity-0",
|
|
698
|
+
"transition-[opacity,translate] duration-150 ease-media motion-safe:translate-y-0.5 motion-reduce:transition-none",
|
|
699
|
+
"group-hover/media-button:translate-y-0 group-hover/media-button:opacity-100 group-hover/media-button:delay-300",
|
|
700
|
+
"group-focus-visible/media-button:translate-y-0 group-focus-visible/media-button:opacity-100",
|
|
701
|
+
"group-aria-expanded/media-button:hidden",
|
|
702
|
+
align === "start" && "left-0",
|
|
703
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
704
|
+
align === "end" && "right-0"
|
|
705
|
+
),
|
|
706
|
+
children: [
|
|
707
|
+
label,
|
|
708
|
+
hotkeys && shortcut && /* @__PURE__ */ jsx2("kbd", { className: "font-mono opacity-60", children: shortcut })
|
|
709
|
+
]
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function MediaButton({ className, type = "button", tooltip, shortcut, tooltipAlign, children, ...props }) {
|
|
714
|
+
return /* @__PURE__ */ jsxs2("button", { type, "data-slot": "media-button", className: cn(buttonClass, className), ...props, children: [
|
|
715
|
+
children,
|
|
716
|
+
/* @__PURE__ */ jsx2(MediaTooltip, { label: tooltip, shortcut, align: tooltipAlign })
|
|
717
|
+
] });
|
|
718
|
+
}
|
|
719
|
+
function IconSwap({ show, a, b }) {
|
|
720
|
+
const layer = "col-start-1 row-start-1 grid place-items-center transition-[opacity,scale,filter] duration-200 ease-media motion-reduce:transition-none";
|
|
721
|
+
return /* @__PURE__ */ jsxs2("span", { className: "grid", children: [
|
|
722
|
+
/* @__PURE__ */ jsx2("span", { className: cn(layer, show === "a" ? "opacity-100" : "scale-50 opacity-0 blur-[2px]"), children: a }),
|
|
723
|
+
/* @__PURE__ */ jsx2("span", { className: cn(layer, show === "b" ? "opacity-100" : "scale-50 opacity-0 blur-[2px]"), children: b })
|
|
724
|
+
] });
|
|
725
|
+
}
|
|
726
|
+
function MediaPlayButton({ onClick, ...props }) {
|
|
727
|
+
const store = useMediaStore();
|
|
728
|
+
const paused = useMedia((s) => s.paused);
|
|
729
|
+
const hasSource = useMedia((s) => s.hasSource);
|
|
730
|
+
return /* @__PURE__ */ jsx2(
|
|
731
|
+
MediaButton,
|
|
732
|
+
{
|
|
733
|
+
"aria-label": paused ? "Play" : "Pause",
|
|
734
|
+
tooltip: paused ? "Play" : "Pause",
|
|
735
|
+
shortcut: "K",
|
|
736
|
+
disabled: !hasSource,
|
|
737
|
+
onClick: (event) => {
|
|
738
|
+
onClick?.(event);
|
|
739
|
+
store.toggle();
|
|
740
|
+
},
|
|
741
|
+
...props,
|
|
742
|
+
children: /* @__PURE__ */ jsx2(IconSwap, { show: paused ? "a" : "b", a: /* @__PURE__ */ jsx2(PlayIcon, {}), b: /* @__PURE__ */ jsx2(PauseIcon, {}) })
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
function MediaMuteButton({ onClick, ...props }) {
|
|
747
|
+
const store = useMediaStore();
|
|
748
|
+
const muted2 = useMedia((s) => s.muted || s.volume === 0);
|
|
749
|
+
return /* @__PURE__ */ jsx2(
|
|
750
|
+
MediaButton,
|
|
751
|
+
{
|
|
752
|
+
"aria-label": muted2 ? "Unmute" : "Mute",
|
|
753
|
+
tooltip: muted2 ? "Unmute" : "Mute",
|
|
754
|
+
shortcut: "M",
|
|
755
|
+
onClick: (event) => {
|
|
756
|
+
onClick?.(event);
|
|
757
|
+
store.toggleMute();
|
|
758
|
+
},
|
|
759
|
+
...props,
|
|
760
|
+
children: /* @__PURE__ */ jsx2(VolumeIcon, { muted: muted2 })
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
function MediaFullscreenButton({ onClick, ...props }) {
|
|
765
|
+
const store = useMediaStore();
|
|
766
|
+
const fullscreen = useMedia((s) => s.fullscreen);
|
|
767
|
+
return /* @__PURE__ */ jsx2(
|
|
768
|
+
MediaButton,
|
|
769
|
+
{
|
|
770
|
+
"aria-label": fullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
|
771
|
+
tooltip: fullscreen ? "Exit fullscreen" : "Fullscreen",
|
|
772
|
+
shortcut: "F",
|
|
773
|
+
onClick: (event) => {
|
|
774
|
+
onClick?.(event);
|
|
775
|
+
store.toggleFullscreen();
|
|
776
|
+
},
|
|
777
|
+
...props,
|
|
778
|
+
children: /* @__PURE__ */ jsx2(IconSwap, { show: fullscreen ? "b" : "a", a: /* @__PURE__ */ jsx2(EnterFullscreenIcon, {}), b: /* @__PURE__ */ jsx2(ExitFullscreenIcon, {}) })
|
|
779
|
+
}
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
var subscribeNoop = () => () => {
|
|
783
|
+
};
|
|
784
|
+
function MediaPictureInPictureButton({ onClick, ...props }) {
|
|
785
|
+
const store = useMediaStore();
|
|
786
|
+
const active = useMedia((s) => s.pictureInPicture);
|
|
787
|
+
const supported = React2.useSyncExternalStore(
|
|
788
|
+
subscribeNoop,
|
|
789
|
+
() => "pictureInPictureEnabled" in document && document.pictureInPictureEnabled,
|
|
790
|
+
() => false
|
|
791
|
+
);
|
|
792
|
+
if (!supported) return null;
|
|
793
|
+
return /* @__PURE__ */ jsx2(
|
|
794
|
+
MediaButton,
|
|
795
|
+
{
|
|
796
|
+
"aria-label": active ? "Exit picture in picture" : "Enter picture in picture",
|
|
797
|
+
"aria-pressed": active,
|
|
798
|
+
tooltip: active ? "Exit picture in picture" : "Picture in picture",
|
|
799
|
+
shortcut: "I",
|
|
800
|
+
onClick: (event) => {
|
|
801
|
+
onClick?.(event);
|
|
802
|
+
store.togglePictureInPicture();
|
|
803
|
+
},
|
|
804
|
+
...props,
|
|
805
|
+
children: /* @__PURE__ */ jsx2(PictureInPictureIcon, {})
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function MediaPlaybackRateButton({
|
|
810
|
+
rates = [1, 1.25, 1.5, 2, 0.75],
|
|
811
|
+
className,
|
|
812
|
+
style,
|
|
813
|
+
onClick,
|
|
814
|
+
...props
|
|
815
|
+
}) {
|
|
816
|
+
const store = useMediaStore();
|
|
817
|
+
const rate = useMedia((s) => s.playbackRate);
|
|
818
|
+
const chars = Math.max(...[...rates, rate].map((r) => `${r}\xD7`.length));
|
|
819
|
+
return /* @__PURE__ */ jsx2(
|
|
820
|
+
MediaButton,
|
|
821
|
+
{
|
|
822
|
+
"aria-label": `Playback speed ${rate}x`,
|
|
823
|
+
tooltip: "Playback speed",
|
|
824
|
+
dir: "ltr",
|
|
825
|
+
className: cn("w-(--media-rate-width) min-w-7 px-1.5 font-mono text-xs tabular-nums", className),
|
|
826
|
+
style: { "--media-rate-width": `calc(${chars}ch + 0.75rem)`, ...style },
|
|
827
|
+
onClick: (event) => {
|
|
828
|
+
onClick?.(event);
|
|
829
|
+
const index = rates.indexOf(rate);
|
|
830
|
+
store.setPlaybackRate(rates[(index + 1) % rates.length]);
|
|
831
|
+
},
|
|
832
|
+
...props,
|
|
833
|
+
children: `${rate}\xD7`
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function MediaControls({ className, ...props }) {
|
|
838
|
+
return /* @__PURE__ */ jsx2(
|
|
839
|
+
"div",
|
|
840
|
+
{
|
|
841
|
+
"data-slot": "media-controls",
|
|
842
|
+
className: cn(
|
|
843
|
+
"flex items-center transition-[opacity,translate] duration-200 ease-media motion-reduce:transition-none",
|
|
844
|
+
"pointer-events-none translate-y-2 opacity-0",
|
|
845
|
+
"group-data-active/media:pointer-events-auto group-data-active/media:translate-y-0 group-data-active/media:opacity-100",
|
|
846
|
+
"group-has-focus-visible/media:pointer-events-auto group-has-focus-visible/media:translate-y-0 group-has-focus-visible/media:opacity-100",
|
|
847
|
+
"group-data-paused/media:group-data-started/media:pointer-events-auto group-data-paused/media:group-data-started/media:translate-y-0 group-data-paused/media:group-data-started/media:opacity-100",
|
|
848
|
+
className
|
|
849
|
+
),
|
|
850
|
+
...props
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
var DOUBLE_TAP_MS = 280;
|
|
855
|
+
function MediaSurface({
|
|
856
|
+
className,
|
|
857
|
+
doubleTapSeek = 10,
|
|
858
|
+
doubleClickFullscreen = true,
|
|
859
|
+
indicatorClassName,
|
|
860
|
+
onClick,
|
|
861
|
+
onDoubleClick,
|
|
862
|
+
onPointerDown,
|
|
863
|
+
children,
|
|
864
|
+
...props
|
|
865
|
+
}) {
|
|
866
|
+
const store = useMediaStore();
|
|
867
|
+
const paused = useMedia((s) => s.paused);
|
|
868
|
+
const pointer = React2.useRef({ type: "mouse", revealOnly: false, lastTap: 0 });
|
|
869
|
+
const tapTimer = React2.useRef(void 0);
|
|
870
|
+
const flashTimer = React2.useRef(void 0);
|
|
871
|
+
const [flash, setFlash] = React2.useState(null);
|
|
872
|
+
React2.useEffect(
|
|
873
|
+
() => () => {
|
|
874
|
+
clearTimeout(tapTimer.current);
|
|
875
|
+
clearTimeout(flashTimer.current);
|
|
876
|
+
},
|
|
877
|
+
[]
|
|
878
|
+
);
|
|
879
|
+
const showFlash = (side) => {
|
|
880
|
+
setFlash((prev) => ({ side, count: prev?.side === side ? prev.count + 1 : 1 }));
|
|
881
|
+
clearTimeout(flashTimer.current);
|
|
882
|
+
flashTimer.current = setTimeout(() => setFlash(null), 650);
|
|
883
|
+
};
|
|
884
|
+
const handleTouchTap = (event) => {
|
|
885
|
+
const now = performance.now();
|
|
886
|
+
const { revealOnly, lastTap } = pointer.current;
|
|
887
|
+
if (doubleTapSeek > 0 && now - lastTap < DOUBLE_TAP_MS) {
|
|
888
|
+
clearTimeout(tapTimer.current);
|
|
889
|
+
pointer.current.lastTap = now;
|
|
890
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
891
|
+
const side = event.clientX - rect.left < rect.width / 2 ? "back" : "forward";
|
|
892
|
+
store.seekBy(side === "back" ? -doubleTapSeek : doubleTapSeek);
|
|
893
|
+
showFlash(side);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
pointer.current.lastTap = now;
|
|
897
|
+
if (revealOnly) return;
|
|
898
|
+
clearTimeout(tapTimer.current);
|
|
899
|
+
tapTimer.current = setTimeout(() => store.toggle(), doubleTapSeek > 0 ? DOUBLE_TAP_MS : 0);
|
|
900
|
+
};
|
|
901
|
+
return /* @__PURE__ */ jsxs2(
|
|
902
|
+
"button",
|
|
903
|
+
{
|
|
904
|
+
type: "button",
|
|
905
|
+
"data-slot": "media-surface",
|
|
906
|
+
"aria-label": paused ? "Play" : "Pause",
|
|
907
|
+
className: cn("absolute inset-0 cursor-[inherit] outline-none", className),
|
|
908
|
+
onPointerDown: (event) => {
|
|
909
|
+
onPointerDown?.(event);
|
|
910
|
+
const root = event.currentTarget.closest("[data-slot='media-root']");
|
|
911
|
+
pointer.current.type = event.pointerType;
|
|
912
|
+
pointer.current.revealOnly = event.pointerType !== "mouse" && !store.getState().paused && !root?.hasAttribute("data-active");
|
|
913
|
+
},
|
|
914
|
+
onClick: (event) => {
|
|
915
|
+
onClick?.(event);
|
|
916
|
+
if (event.detail !== 0 && pointer.current.type !== "mouse") {
|
|
917
|
+
handleTouchTap(event);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
store.toggle();
|
|
921
|
+
},
|
|
922
|
+
onDoubleClick: (event) => {
|
|
923
|
+
onDoubleClick?.(event);
|
|
924
|
+
if (doubleClickFullscreen && pointer.current.type === "mouse") store.toggleFullscreen();
|
|
925
|
+
},
|
|
926
|
+
...props,
|
|
927
|
+
children: [
|
|
928
|
+
doubleTapSeek > 0 && /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 flex items-center justify-between px-[8%]", children: ["back", "forward"].map((side) => /* @__PURE__ */ jsxs2(
|
|
929
|
+
"span",
|
|
930
|
+
{
|
|
931
|
+
"data-show": flash?.side === side || void 0,
|
|
932
|
+
className: cn(
|
|
933
|
+
"flex items-center gap-1.5 rounded-full px-3 py-1.5 font-mono text-xs tabular-nums opacity-0 scale-90 transition-[opacity,scale] duration-200 ease-media data-show:scale-100 data-show:opacity-100 motion-reduce:transition-none",
|
|
934
|
+
side === "forward" && "flex-row-reverse",
|
|
935
|
+
indicatorClassName
|
|
936
|
+
),
|
|
937
|
+
children: [
|
|
938
|
+
/* @__PURE__ */ jsx2(SeekIcon, { direction: side, className: "size-3.5" }),
|
|
939
|
+
`${(flash?.side === side ? flash.count : 1) * doubleTapSeek}s`
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
side
|
|
943
|
+
)) }),
|
|
944
|
+
children
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
function MediaSeek({
|
|
950
|
+
className,
|
|
951
|
+
step = 5,
|
|
952
|
+
preview = true,
|
|
953
|
+
previewClassName,
|
|
954
|
+
...props
|
|
955
|
+
}) {
|
|
956
|
+
const store = useMediaStore();
|
|
957
|
+
const duration = useMedia((s) => s.duration);
|
|
958
|
+
const bufferedEnd = useMedia((s) => s.bufferedEnd);
|
|
959
|
+
const currentTime = useMedia((s) => Math.floor(s.currentTime));
|
|
960
|
+
const fillRef = React2.useRef(null);
|
|
961
|
+
const scrub = React2.useRef(null);
|
|
962
|
+
const [hover, setHover] = React2.useState(null);
|
|
963
|
+
React2.useEffect(() => {
|
|
964
|
+
let frame = 0;
|
|
965
|
+
const draw = () => {
|
|
966
|
+
cancelAnimationFrame(frame);
|
|
967
|
+
const media = store.getMedia();
|
|
968
|
+
const { duration: duration2, paused } = store.getState();
|
|
969
|
+
if (!scrub.current && fillRef.current) {
|
|
970
|
+
const progress = duration2 ? clamp((media?.currentTime ?? 0) / duration2) : 0;
|
|
971
|
+
fillRef.current.style.transform = `scaleX(${progress})`;
|
|
972
|
+
}
|
|
973
|
+
if (!paused) frame = requestAnimationFrame(draw);
|
|
974
|
+
};
|
|
975
|
+
draw();
|
|
976
|
+
const unsubscribe = store.subscribe(draw);
|
|
977
|
+
return () => {
|
|
978
|
+
unsubscribe();
|
|
979
|
+
cancelAnimationFrame(frame);
|
|
980
|
+
};
|
|
981
|
+
}, [store]);
|
|
982
|
+
const ratioFromEvent = (event) => {
|
|
983
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
984
|
+
return clamp((event.clientX - rect.left) / rect.width);
|
|
985
|
+
};
|
|
986
|
+
const seekToRatio = (ratio) => {
|
|
987
|
+
if (fillRef.current) fillRef.current.style.transform = `scaleX(${ratio})`;
|
|
988
|
+
store.seek(ratio * store.getState().duration);
|
|
989
|
+
};
|
|
990
|
+
const endScrub = (event) => {
|
|
991
|
+
if (!scrub.current) return;
|
|
992
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
993
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
994
|
+
}
|
|
995
|
+
const { resume } = scrub.current;
|
|
996
|
+
scrub.current = null;
|
|
997
|
+
if (resume) store.play();
|
|
998
|
+
};
|
|
999
|
+
return /* @__PURE__ */ jsxs2(
|
|
1000
|
+
"div",
|
|
1001
|
+
{
|
|
1002
|
+
role: "slider",
|
|
1003
|
+
tabIndex: 0,
|
|
1004
|
+
"aria-label": "Seek",
|
|
1005
|
+
"aria-valuemin": 0,
|
|
1006
|
+
"aria-valuemax": Math.floor(duration),
|
|
1007
|
+
"aria-valuenow": currentTime,
|
|
1008
|
+
"aria-valuetext": `${formatTime(currentTime)} of ${formatTime(duration)}`,
|
|
1009
|
+
"data-slot": "media-seek",
|
|
1010
|
+
className: cn(
|
|
1011
|
+
/* on touch, an invisible 44px-tall hit area around the 20px bar */
|
|
1012
|
+
"group/seek relative flex h-5 min-w-0 grow cursor-pointer touch-none items-center rounded-full outline-none select-none pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:-inset-y-3 pointer-coarse:before:content-['']",
|
|
1013
|
+
className
|
|
1014
|
+
),
|
|
1015
|
+
onPointerDown: (event) => {
|
|
1016
|
+
if (event.button !== 0) return;
|
|
1017
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1018
|
+
scrub.current = { resume: !store.getState().paused };
|
|
1019
|
+
store.pause();
|
|
1020
|
+
seekToRatio(ratioFromEvent(event));
|
|
1021
|
+
},
|
|
1022
|
+
onPointerMove: (event) => {
|
|
1023
|
+
const ratio = ratioFromEvent(event);
|
|
1024
|
+
if (event.pointerType === "mouse") setHover(ratio);
|
|
1025
|
+
if (scrub.current) seekToRatio(ratio);
|
|
1026
|
+
},
|
|
1027
|
+
onPointerUp: endScrub,
|
|
1028
|
+
onPointerCancel: endScrub,
|
|
1029
|
+
onPointerLeave: () => setHover(null),
|
|
1030
|
+
onKeyDown: (event) => {
|
|
1031
|
+
const { currentTime: currentTime2, duration: duration2 } = store.getState();
|
|
1032
|
+
const moves = {
|
|
1033
|
+
ArrowLeft: currentTime2 - step,
|
|
1034
|
+
ArrowDown: currentTime2 - step,
|
|
1035
|
+
ArrowRight: currentTime2 + step,
|
|
1036
|
+
ArrowUp: currentTime2 + step,
|
|
1037
|
+
PageDown: currentTime2 - duration2 / 10,
|
|
1038
|
+
PageUp: currentTime2 + duration2 / 10,
|
|
1039
|
+
Home: 0,
|
|
1040
|
+
End: duration2
|
|
1041
|
+
};
|
|
1042
|
+
if (!(event.key in moves)) return;
|
|
1043
|
+
event.preventDefault();
|
|
1044
|
+
event.stopPropagation();
|
|
1045
|
+
store.seek(moves[event.key]);
|
|
1046
|
+
},
|
|
1047
|
+
...props,
|
|
1048
|
+
children: [
|
|
1049
|
+
/* @__PURE__ */ jsxs2("div", { className: "relative h-1 w-full overflow-hidden rounded-full bg-current/25 transition-[height] duration-150 ease-media group-hover/seek:h-1.5 group-focus-visible/seek:h-1.5 group-active/seek:h-1.5 motion-reduce:transition-none", children: [
|
|
1050
|
+
/* @__PURE__ */ jsx2(
|
|
1051
|
+
"div",
|
|
1052
|
+
{
|
|
1053
|
+
className: "absolute inset-0 origin-left bg-current/25 transition-transform duration-300 ease-media",
|
|
1054
|
+
style: { transform: `scaleX(${duration ? clamp(bufferedEnd / duration) : 0})` }
|
|
1055
|
+
}
|
|
1056
|
+
),
|
|
1057
|
+
/* @__PURE__ */ jsx2("div", { ref: fillRef, className: "absolute inset-0 origin-left bg-current", style: { transform: "scaleX(0)" } })
|
|
1058
|
+
] }),
|
|
1059
|
+
/* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute inset-0 rounded-full ring-current/60 group-focus-visible/seek:ring-2" }),
|
|
1060
|
+
preview && hover !== null && duration > 0 && /* @__PURE__ */ jsx2(
|
|
1061
|
+
"span",
|
|
1062
|
+
{
|
|
1063
|
+
"aria-hidden": true,
|
|
1064
|
+
className: cn(
|
|
1065
|
+
"pointer-events-none absolute bottom-full mb-1.5 -translate-x-1/2 rounded-md bg-foreground px-1.5 py-0.5 font-mono text-[11px] leading-4 text-background tabular-nums",
|
|
1066
|
+
previewClassName
|
|
1067
|
+
),
|
|
1068
|
+
style: { left: `${hover * 100}%` },
|
|
1069
|
+
children: formatTime(hover * duration, duration)
|
|
1070
|
+
}
|
|
1071
|
+
)
|
|
1072
|
+
]
|
|
1073
|
+
}
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
function MediaVolume({ orientation = "horizontal", className, ...props }) {
|
|
1077
|
+
const store = useMediaStore();
|
|
1078
|
+
const volume = useMedia((s) => s.muted ? 0 : s.volume);
|
|
1079
|
+
const dragging = React2.useRef(false);
|
|
1080
|
+
const vertical = orientation === "vertical";
|
|
1081
|
+
const setFromEvent = (event) => {
|
|
1082
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1083
|
+
store.setVolume(
|
|
1084
|
+
clamp(vertical ? (rect.bottom - event.clientY) / rect.height : (event.clientX - rect.left) / rect.width)
|
|
1085
|
+
);
|
|
1086
|
+
};
|
|
1087
|
+
return /* @__PURE__ */ jsxs2(
|
|
1088
|
+
"div",
|
|
1089
|
+
{
|
|
1090
|
+
role: "slider",
|
|
1091
|
+
tabIndex: 0,
|
|
1092
|
+
"aria-label": "Volume",
|
|
1093
|
+
"aria-valuemin": 0,
|
|
1094
|
+
"aria-valuemax": 100,
|
|
1095
|
+
"aria-valuenow": Math.round(volume * 100),
|
|
1096
|
+
"aria-valuetext": `${Math.round(volume * 100)}%`,
|
|
1097
|
+
"aria-orientation": orientation,
|
|
1098
|
+
"data-slot": "media-volume",
|
|
1099
|
+
className: cn(
|
|
1100
|
+
"group/volume relative flex shrink-0 cursor-pointer touch-none outline-none select-none",
|
|
1101
|
+
vertical ? "h-16 w-5 justify-center" : "h-5 w-16 items-center",
|
|
1102
|
+
className
|
|
1103
|
+
),
|
|
1104
|
+
onPointerDown: (event) => {
|
|
1105
|
+
if (event.button !== 0) return;
|
|
1106
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1107
|
+
dragging.current = true;
|
|
1108
|
+
setFromEvent(event);
|
|
1109
|
+
},
|
|
1110
|
+
onPointerMove: (event) => dragging.current && setFromEvent(event),
|
|
1111
|
+
onPointerUp: (event) => {
|
|
1112
|
+
dragging.current = false;
|
|
1113
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1114
|
+
},
|
|
1115
|
+
onPointerCancel: () => {
|
|
1116
|
+
dragging.current = false;
|
|
1117
|
+
},
|
|
1118
|
+
onKeyDown: (event) => {
|
|
1119
|
+
const moves = {
|
|
1120
|
+
ArrowLeft: volume - 0.1,
|
|
1121
|
+
ArrowDown: volume - 0.1,
|
|
1122
|
+
ArrowRight: volume + 0.1,
|
|
1123
|
+
ArrowUp: volume + 0.1,
|
|
1124
|
+
PageDown: volume - 0.25,
|
|
1125
|
+
PageUp: volume + 0.25,
|
|
1126
|
+
Home: 0,
|
|
1127
|
+
End: 1
|
|
1128
|
+
};
|
|
1129
|
+
if (!(event.key in moves)) return;
|
|
1130
|
+
event.preventDefault();
|
|
1131
|
+
event.stopPropagation();
|
|
1132
|
+
store.setVolume(moves[event.key]);
|
|
1133
|
+
},
|
|
1134
|
+
...props,
|
|
1135
|
+
children: [
|
|
1136
|
+
/* @__PURE__ */ jsx2(
|
|
1137
|
+
"div",
|
|
1138
|
+
{
|
|
1139
|
+
className: cn(
|
|
1140
|
+
"relative overflow-hidden rounded-full bg-current/25 duration-150 ease-media motion-reduce:transition-none",
|
|
1141
|
+
vertical ? "h-full w-1 transition-[width] group-hover/volume:w-1.5 group-focus-visible/volume:w-1.5" : "h-1 w-full transition-[height] group-hover/volume:h-1.5 group-focus-visible/volume:h-1.5"
|
|
1142
|
+
),
|
|
1143
|
+
children: /* @__PURE__ */ jsx2(
|
|
1144
|
+
"div",
|
|
1145
|
+
{
|
|
1146
|
+
className: cn("absolute inset-0 bg-current", vertical ? "origin-bottom" : "origin-left"),
|
|
1147
|
+
style: { transform: vertical ? `scaleY(${volume})` : `scaleX(${volume})` }
|
|
1148
|
+
}
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
/* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute inset-0 rounded-full ring-current/60 group-focus-visible/volume:ring-2" })
|
|
1153
|
+
]
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
var timeNames = { current: "elapsed", duration: "duration", remaining: "remaining" };
|
|
1158
|
+
function MediaTime({ type = "current", toggle = false, className, ...props }) {
|
|
1159
|
+
const [picked, setPicked] = React2.useState(type);
|
|
1160
|
+
const shown = toggle ? picked : type;
|
|
1161
|
+
const duration = useMedia((s) => s.duration);
|
|
1162
|
+
const value = useMedia((s) => {
|
|
1163
|
+
if (shown === "duration") return Math.floor(s.duration) || Math.ceil(s.duration);
|
|
1164
|
+
if (shown === "remaining") return Math.ceil(Math.max(s.duration - s.currentTime, 0));
|
|
1165
|
+
return Math.floor(s.currentTime);
|
|
1166
|
+
});
|
|
1167
|
+
const time = formatTime(value, duration);
|
|
1168
|
+
const text = shown === "remaining" ? `-${time}` : time;
|
|
1169
|
+
const other = type === "remaining" ? "current" : "remaining";
|
|
1170
|
+
const next = shown === type ? other : type;
|
|
1171
|
+
return /* @__PURE__ */ jsx2("span", { "data-slot": "media-time", dir: "ltr", className: cn("font-mono text-xs tabular-nums", className), ...props, children: toggle ? /* @__PURE__ */ jsxs2(
|
|
1172
|
+
"button",
|
|
1173
|
+
{
|
|
1174
|
+
type: "button",
|
|
1175
|
+
"aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${time}`,
|
|
1176
|
+
onClick: () => setPicked(next),
|
|
1177
|
+
className: "group/media-button relative cursor-pointer rounded-sm outline-none transition-colors duration-150 ease-media hover:text-current focus-visible:ring-2 focus-visible:ring-current/60",
|
|
1178
|
+
children: [
|
|
1179
|
+
text,
|
|
1180
|
+
/* @__PURE__ */ jsx2(MediaTooltip, { label: `Show ${timeNames[next]}` })
|
|
1181
|
+
]
|
|
1182
|
+
}
|
|
1183
|
+
) : text });
|
|
1184
|
+
}
|
|
1185
|
+
function MediaLoading({ className, ...props }) {
|
|
1186
|
+
const waiting = useMedia((s) => s.waiting && !s.paused);
|
|
1187
|
+
return /* @__PURE__ */ jsx2(
|
|
1188
|
+
"span",
|
|
1189
|
+
{
|
|
1190
|
+
role: "status",
|
|
1191
|
+
"aria-label": waiting ? "Loading" : void 0,
|
|
1192
|
+
"data-slot": "media-loading",
|
|
1193
|
+
className: cn(
|
|
1194
|
+
"pointer-events-none size-6 rounded-full border-2 border-current/25 border-t-current transition-opacity duration-200 ease-media",
|
|
1195
|
+
waiting ? "animate-spin opacity-100 delay-300" : "opacity-0",
|
|
1196
|
+
className
|
|
1197
|
+
),
|
|
1198
|
+
...props
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
function MediaCaptions({ className, ...props }) {
|
|
1203
|
+
const cues = useMedia((s) => s.textTrack === -1 ? emptyCues : s.cues);
|
|
1204
|
+
if (!cues.length) return null;
|
|
1205
|
+
return /* @__PURE__ */ jsx2(
|
|
1206
|
+
"div",
|
|
1207
|
+
{
|
|
1208
|
+
"data-slot": "media-captions",
|
|
1209
|
+
className: cn("pointer-events-none flex flex-col items-center gap-1 text-center", className),
|
|
1210
|
+
...props,
|
|
1211
|
+
children: cues.map((cue, i) => /* @__PURE__ */ jsx2("span", { dir: "auto", className: "whitespace-pre-line text-balance rounded-md px-2 py-0.5 box-decoration-clone", children: cue }, `${i}-${cue}`))
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
var emptyCues = [];
|
|
1216
|
+
function MediaCaptionsButton({ onClick, ...props }) {
|
|
1217
|
+
const store = useMediaStore();
|
|
1218
|
+
const available = useMedia((s) => s.textTracks.length > 0);
|
|
1219
|
+
const on = useMedia((s) => s.textTrack !== -1);
|
|
1220
|
+
if (!available) return null;
|
|
1221
|
+
return /* @__PURE__ */ jsx2(
|
|
1222
|
+
MediaButton,
|
|
1223
|
+
{
|
|
1224
|
+
"aria-label": "Captions",
|
|
1225
|
+
"aria-pressed": on,
|
|
1226
|
+
tooltip: on ? "Captions off" : "Captions",
|
|
1227
|
+
shortcut: "C",
|
|
1228
|
+
onClick: (event) => {
|
|
1229
|
+
onClick?.(event);
|
|
1230
|
+
store.toggleCaptions();
|
|
1231
|
+
},
|
|
1232
|
+
...props,
|
|
1233
|
+
children: /* @__PURE__ */ jsx2(CaptionsIcon, { active: on })
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelClassName, ...props }) {
|
|
1238
|
+
const store = useMediaStore();
|
|
1239
|
+
const rate = useMedia((s) => s.playbackRate);
|
|
1240
|
+
const tracks = useMedia((s) => s.textTracks);
|
|
1241
|
+
const textTrack = useMedia((s) => s.textTrack);
|
|
1242
|
+
const [open, setOpen] = React2.useState(false);
|
|
1243
|
+
const rootEl = React2.useRef(null);
|
|
1244
|
+
const triggerEl = React2.useRef(null);
|
|
1245
|
+
const panelEl = React2.useRef(null);
|
|
1246
|
+
const listEl = React2.useRef(null);
|
|
1247
|
+
const panelId = React2.useId();
|
|
1248
|
+
React2.useLayoutEffect(() => {
|
|
1249
|
+
const panel = panelEl.current;
|
|
1250
|
+
const list = listEl.current;
|
|
1251
|
+
const trigger = triggerEl.current;
|
|
1252
|
+
const player = rootEl.current?.closest("[data-slot='media-root']");
|
|
1253
|
+
if (!open || !panel || !list || !trigger || !player) return;
|
|
1254
|
+
const edge = () => {
|
|
1255
|
+
const left = Math.min(24, list.scrollHeight - list.clientHeight - list.scrollTop);
|
|
1256
|
+
list.style.maskImage = left > 1 ? `linear-gradient(to bottom, black calc(100% - ${left}px), rgb(0 0 0 / 0.3))` : "";
|
|
1257
|
+
};
|
|
1258
|
+
const fit = () => {
|
|
1259
|
+
const space = trigger.getBoundingClientRect().top - player.getBoundingClientRect().top - 16;
|
|
1260
|
+
panel.style.maxHeight = `${Math.max(96, Math.min(288, space))}px`;
|
|
1261
|
+
edge();
|
|
1262
|
+
};
|
|
1263
|
+
fit();
|
|
1264
|
+
const observer = new ResizeObserver(fit);
|
|
1265
|
+
observer.observe(player);
|
|
1266
|
+
list.addEventListener("scroll", edge, { passive: true });
|
|
1267
|
+
return () => {
|
|
1268
|
+
observer.disconnect();
|
|
1269
|
+
list.removeEventListener("scroll", edge);
|
|
1270
|
+
};
|
|
1271
|
+
}, [open]);
|
|
1272
|
+
React2.useEffect(() => {
|
|
1273
|
+
if (!open) return;
|
|
1274
|
+
const onPointerDown = (event) => {
|
|
1275
|
+
if (!rootEl.current?.contains(event.target)) setOpen(false);
|
|
1276
|
+
};
|
|
1277
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
1278
|
+
rootEl.current?.querySelector("[aria-checked='true']")?.focus();
|
|
1279
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
1280
|
+
}, [open]);
|
|
1281
|
+
const close = () => {
|
|
1282
|
+
setOpen(false);
|
|
1283
|
+
triggerEl.current?.focus();
|
|
1284
|
+
};
|
|
1285
|
+
const onPanelKeyDown = (event) => {
|
|
1286
|
+
if (event.key === "Escape") {
|
|
1287
|
+
event.stopPropagation();
|
|
1288
|
+
close();
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
1292
|
+
if (!step) return;
|
|
1293
|
+
event.preventDefault();
|
|
1294
|
+
event.stopPropagation();
|
|
1295
|
+
const items = Array.from(rootEl.current?.querySelectorAll("[role='menuitemradio']") ?? []);
|
|
1296
|
+
const index = items.indexOf(document.activeElement);
|
|
1297
|
+
const next = items[(index + step + items.length) % items.length];
|
|
1298
|
+
next?.focus();
|
|
1299
|
+
};
|
|
1300
|
+
const option = (label, checked, select) => /* @__PURE__ */ jsxs2(
|
|
1301
|
+
"button",
|
|
1302
|
+
{
|
|
1303
|
+
type: "button",
|
|
1304
|
+
role: "menuitemradio",
|
|
1305
|
+
"aria-checked": checked,
|
|
1306
|
+
tabIndex: checked ? 0 : -1,
|
|
1307
|
+
onClick: () => {
|
|
1308
|
+
select();
|
|
1309
|
+
close();
|
|
1310
|
+
},
|
|
1311
|
+
className: "flex min-h-8 w-full shrink-0 cursor-pointer items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm leading-5 outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:bg-current/15",
|
|
1312
|
+
children: [
|
|
1313
|
+
/* @__PURE__ */ jsx2("span", { dir: "auto", className: "min-w-0", children: label }),
|
|
1314
|
+
/* @__PURE__ */ jsx2(CheckIcon, { className: cn("size-3.5 shrink-0 transition-opacity duration-150", checked ? "opacity-100" : "opacity-0") })
|
|
1315
|
+
]
|
|
1316
|
+
},
|
|
1317
|
+
label
|
|
1318
|
+
);
|
|
1319
|
+
return /* @__PURE__ */ jsxs2(
|
|
1320
|
+
"div",
|
|
1321
|
+
{
|
|
1322
|
+
ref: rootEl,
|
|
1323
|
+
"data-slot": "media-settings",
|
|
1324
|
+
"data-open": open || void 0,
|
|
1325
|
+
className: cn("relative", className),
|
|
1326
|
+
...props,
|
|
1327
|
+
children: [
|
|
1328
|
+
/* @__PURE__ */ jsx2(
|
|
1329
|
+
MediaButton,
|
|
1330
|
+
{
|
|
1331
|
+
ref: triggerEl,
|
|
1332
|
+
"aria-label": "Settings",
|
|
1333
|
+
tooltip: "Settings",
|
|
1334
|
+
"aria-haspopup": "menu",
|
|
1335
|
+
"aria-expanded": open,
|
|
1336
|
+
"aria-controls": open ? panelId : void 0,
|
|
1337
|
+
onClick: () => setOpen((o) => !o),
|
|
1338
|
+
children: /* @__PURE__ */ jsx2(SettingsIcon, { className: cn("transition-transform duration-300 ease-media motion-reduce:transition-none", open && "rotate-90") })
|
|
1339
|
+
}
|
|
1340
|
+
),
|
|
1341
|
+
open && /* @__PURE__ */ jsx2(
|
|
1342
|
+
"div",
|
|
1343
|
+
{
|
|
1344
|
+
ref: panelEl,
|
|
1345
|
+
id: panelId,
|
|
1346
|
+
role: "menu",
|
|
1347
|
+
"aria-label": "Playback settings",
|
|
1348
|
+
onKeyDown: onPanelKeyDown,
|
|
1349
|
+
className: cn(
|
|
1350
|
+
"absolute right-0 bottom-full z-10 mb-2 flex max-h-72 w-48 origin-bottom-right flex-col overflow-hidden rounded-xl",
|
|
1351
|
+
"transition-[opacity,translate,scale] duration-150 ease-media starting:translate-y-1 starting:scale-95 starting:opacity-0 motion-reduce:transition-none",
|
|
1352
|
+
panelClassName
|
|
1353
|
+
),
|
|
1354
|
+
children: /* @__PURE__ */ jsxs2(
|
|
1355
|
+
"div",
|
|
1356
|
+
{
|
|
1357
|
+
ref: listEl,
|
|
1358
|
+
className: "flex min-h-0 flex-col gap-2 overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
1359
|
+
children: [
|
|
1360
|
+
/* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Speed", className: "flex flex-col", children: [
|
|
1361
|
+
/* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Speed" }),
|
|
1362
|
+
/* @__PURE__ */ jsx2("div", { className: "grid grid-cols-3 gap-1", children: rates.map((r) => /* @__PURE__ */ jsx2(
|
|
1363
|
+
"button",
|
|
1364
|
+
{
|
|
1365
|
+
type: "button",
|
|
1366
|
+
role: "menuitemradio",
|
|
1367
|
+
"aria-checked": rate === r,
|
|
1368
|
+
"aria-label": r === 1 ? "Normal speed" : `${r}\xD7 speed`,
|
|
1369
|
+
tabIndex: rate === r ? 0 : -1,
|
|
1370
|
+
onClick: () => {
|
|
1371
|
+
store.setPlaybackRate(r);
|
|
1372
|
+
close();
|
|
1373
|
+
},
|
|
1374
|
+
className: cn(
|
|
1375
|
+
"h-8 cursor-pointer rounded-md text-sm tabular-nums outline-none transition-colors duration-150 ease-media hover:bg-current/10 focus-visible:ring-1 focus-visible:ring-current/40",
|
|
1376
|
+
rate === r && "bg-current/15"
|
|
1377
|
+
),
|
|
1378
|
+
children: `${r}\xD7`
|
|
1379
|
+
},
|
|
1380
|
+
r
|
|
1381
|
+
)) })
|
|
1382
|
+
] }),
|
|
1383
|
+
tracks.length > 0 && /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": "Captions", className: "flex flex-col", children: [
|
|
1384
|
+
/* @__PURE__ */ jsx2("span", { className: "px-2 pt-1 pb-1.5 text-xs opacity-60", children: "Captions" }),
|
|
1385
|
+
option("Off", textTrack === -1, () => store.setTextTrack(-1)),
|
|
1386
|
+
tracks.map((track, i) => option(track.label, textTrack === i, () => store.setTextTrack(i)))
|
|
1387
|
+
] })
|
|
1388
|
+
]
|
|
1389
|
+
}
|
|
1390
|
+
)
|
|
1391
|
+
}
|
|
1392
|
+
)
|
|
1393
|
+
]
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
function MediaErrorMessage({ className, message, ...props }) {
|
|
1398
|
+
const store = useMediaStore();
|
|
1399
|
+
const error = useMedia((s) => s.error);
|
|
1400
|
+
if (!error) return null;
|
|
1401
|
+
return /* @__PURE__ */ jsxs2(
|
|
1402
|
+
"div",
|
|
1403
|
+
{
|
|
1404
|
+
role: "alert",
|
|
1405
|
+
"data-slot": "media-error",
|
|
1406
|
+
className: cn("flex flex-col items-center justify-center gap-3 text-center", className),
|
|
1407
|
+
...props,
|
|
1408
|
+
children: [
|
|
1409
|
+
/* @__PURE__ */ jsx2("p", { className: "text-pretty text-sm", children: message ? message(error) : describeMediaError(error) }),
|
|
1410
|
+
/* @__PURE__ */ jsxs2(
|
|
1411
|
+
"button",
|
|
1412
|
+
{
|
|
1413
|
+
type: "button",
|
|
1414
|
+
onClick: () => store.retry(),
|
|
1415
|
+
className: "inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm whitespace-nowrap outline-none ring-1 ring-current/25 transition-[background-color,scale] duration-150 ease-media hover:bg-current/10 focus-visible:ring-2 focus-visible:ring-current/60 motion-safe:active:scale-[0.96]",
|
|
1416
|
+
children: [
|
|
1417
|
+
/* @__PURE__ */ jsx2(RetryIcon, { className: "size-3.5" }),
|
|
1418
|
+
"Try again"
|
|
1419
|
+
]
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
]
|
|
1423
|
+
}
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// ../../components/ui/video-player.tsx
|
|
1428
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1429
|
+
var reveal = "transition-[opacity,scale] duration-200 ease-media motion-reduce:transition-none";
|
|
1430
|
+
var visibleWithControls = "group-data-active/media:opacity-100 group-has-focus-visible/media:opacity-100 group-data-paused/media:group-data-started/media:opacity-100";
|
|
1431
|
+
var glass = "bg-media-scrim/70 text-media-foreground ring-1 ring-media-foreground/10 backdrop-blur-xl";
|
|
1432
|
+
var volumeOpen = "group-hover/volume-menu:pointer-events-auto group-hover/volume-menu:translate-y-0 group-hover/volume-menu:opacity-100 group-has-focus-visible/volume-menu:pointer-events-auto group-has-focus-visible/volume-menu:translate-y-0 group-has-focus-visible/volume-menu:opacity-100";
|
|
1433
|
+
function VideoPlayer({
|
|
1434
|
+
className,
|
|
1435
|
+
frameClassName,
|
|
1436
|
+
tracks,
|
|
1437
|
+
loader,
|
|
1438
|
+
metadata,
|
|
1439
|
+
persistKey,
|
|
1440
|
+
tooltips = true,
|
|
1441
|
+
children,
|
|
1442
|
+
crossOrigin,
|
|
1443
|
+
...props
|
|
1444
|
+
}) {
|
|
1445
|
+
return /* @__PURE__ */ jsxs3(
|
|
1446
|
+
MediaRoot,
|
|
1447
|
+
{
|
|
1448
|
+
persistKey,
|
|
1449
|
+
metadata,
|
|
1450
|
+
tooltips,
|
|
1451
|
+
className: cn(
|
|
1452
|
+
"@container/player aspect-video w-full overflow-hidden rounded-xl bg-media-scrim text-media-foreground data-fullscreen:rounded-none",
|
|
1453
|
+
frameClassName
|
|
1454
|
+
),
|
|
1455
|
+
children: [
|
|
1456
|
+
/* @__PURE__ */ jsxs3(
|
|
1457
|
+
MediaVideo,
|
|
1458
|
+
{
|
|
1459
|
+
className: cn("size-full object-contain", className),
|
|
1460
|
+
loader,
|
|
1461
|
+
crossOrigin: crossOrigin ?? (tracks?.length ? "anonymous" : void 0),
|
|
1462
|
+
...props,
|
|
1463
|
+
children: [
|
|
1464
|
+
tracks?.map((track) => /* @__PURE__ */ jsx3(
|
|
1465
|
+
"track",
|
|
1466
|
+
{
|
|
1467
|
+
src: track.src,
|
|
1468
|
+
label: track.label,
|
|
1469
|
+
srcLang: track.srcLang,
|
|
1470
|
+
kind: track.kind ?? "subtitles",
|
|
1471
|
+
default: track.default
|
|
1472
|
+
},
|
|
1473
|
+
track.src
|
|
1474
|
+
)),
|
|
1475
|
+
children
|
|
1476
|
+
]
|
|
1477
|
+
}
|
|
1478
|
+
),
|
|
1479
|
+
/* @__PURE__ */ jsx3(MediaSurface, { indicatorClassName: glass }),
|
|
1480
|
+
/* @__PURE__ */ jsx3(
|
|
1481
|
+
"span",
|
|
1482
|
+
{
|
|
1483
|
+
"aria-hidden": true,
|
|
1484
|
+
className: cn(
|
|
1485
|
+
"pointer-events-none absolute top-1/2 left-1/2 grid size-12 -translate-1/2 place-items-center rounded-full bg-media-scrim/40 backdrop-blur-md",
|
|
1486
|
+
reveal,
|
|
1487
|
+
"group-data-started/media:scale-75 group-data-started/media:opacity-0"
|
|
1488
|
+
),
|
|
1489
|
+
children: /* @__PURE__ */ jsx3(PlayIcon, { className: "size-5" })
|
|
1490
|
+
}
|
|
1491
|
+
),
|
|
1492
|
+
/* @__PURE__ */ jsx3("span", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: /* @__PURE__ */ jsx3(MediaLoading, {}) }),
|
|
1493
|
+
/* @__PURE__ */ jsx3(
|
|
1494
|
+
MediaCaptions,
|
|
1495
|
+
{
|
|
1496
|
+
className: cn(
|
|
1497
|
+
/* sized by the player, not the viewport, so a small player keeps its cues */
|
|
1498
|
+
"absolute inset-x-4 bottom-4 text-xs leading-snug transition-transform duration-200 ease-media motion-reduce:transition-none @xs/player:text-sm @2xl/player:text-base",
|
|
1499
|
+
"group-data-active/media:-translate-y-10 group-has-focus-visible/media:-translate-y-10 group-data-paused/media:group-data-started/media:-translate-y-10",
|
|
1500
|
+
"[&>span]:bg-media-scrim/75 [&>span]:text-media-foreground"
|
|
1501
|
+
)
|
|
1502
|
+
}
|
|
1503
|
+
),
|
|
1504
|
+
/* @__PURE__ */ jsx3(
|
|
1505
|
+
"div",
|
|
1506
|
+
{
|
|
1507
|
+
"aria-hidden": true,
|
|
1508
|
+
className: cn(
|
|
1509
|
+
"pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-linear-to-t from-media-scrim/60 to-transparent opacity-0",
|
|
1510
|
+
reveal,
|
|
1511
|
+
visibleWithControls
|
|
1512
|
+
)
|
|
1513
|
+
}
|
|
1514
|
+
),
|
|
1515
|
+
/* @__PURE__ */ jsxs3(
|
|
1516
|
+
MediaControls,
|
|
1517
|
+
{
|
|
1518
|
+
dir: "ltr",
|
|
1519
|
+
className: "absolute inset-x-2 bottom-2 h-9 gap-1 px-1 **:data-[slot=media-tooltip]:bg-media-foreground **:data-[slot=media-tooltip]:text-media-scrim",
|
|
1520
|
+
children: [
|
|
1521
|
+
/* @__PURE__ */ jsx3(MediaPlayButton, { tooltipAlign: "start" }),
|
|
1522
|
+
/* @__PURE__ */ jsx3(MediaSeek, { className: "mx-2", previewClassName: "bg-media-foreground text-media-scrim" }),
|
|
1523
|
+
/* @__PURE__ */ jsx3(MediaTime, { type: "remaining", toggle: true, className: "hidden min-w-10 text-right text-media-foreground/80 @xs/player:inline" }),
|
|
1524
|
+
/* @__PURE__ */ jsxs3("div", { className: "group/volume-menu relative", children: [
|
|
1525
|
+
/* @__PURE__ */ jsx3(MediaMuteButton, { tooltip: false }),
|
|
1526
|
+
/* @__PURE__ */ jsx3(
|
|
1527
|
+
"div",
|
|
1528
|
+
{
|
|
1529
|
+
className: cn(
|
|
1530
|
+
"pointer-events-none absolute bottom-full left-1/2 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
|
|
1531
|
+
"transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
|
|
1532
|
+
volumeOpen
|
|
1533
|
+
),
|
|
1534
|
+
children: /* @__PURE__ */ jsx3("div", { className: cn("flex h-24 w-7 justify-center rounded-full py-3", glass), children: /* @__PURE__ */ jsx3(MediaVolume, { orientation: "vertical", className: "h-full" }) })
|
|
1535
|
+
}
|
|
1536
|
+
)
|
|
1537
|
+
] }),
|
|
1538
|
+
/* @__PURE__ */ jsx3(MediaCaptionsButton, {}),
|
|
1539
|
+
/* @__PURE__ */ jsx3(MediaSettings, { panelClassName: glass }),
|
|
1540
|
+
/* @__PURE__ */ jsx3(MediaPictureInPictureButton, { tooltipAlign: "end", className: "hidden @sm/player:inline-grid" }),
|
|
1541
|
+
/* @__PURE__ */ jsx3(MediaFullscreenButton, { tooltipAlign: "end" })
|
|
1542
|
+
]
|
|
1543
|
+
}
|
|
1544
|
+
),
|
|
1545
|
+
/* @__PURE__ */ jsx3(MediaErrorMessage, { className: "absolute inset-0 bg-media-scrim/85 px-6 text-media-foreground backdrop-blur-sm" })
|
|
1546
|
+
]
|
|
1547
|
+
}
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// ../../components/ui/audio-player.tsx
|
|
1552
|
+
import * as React3 from "react";
|
|
1553
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1554
|
+
var muted = "text-muted-foreground hover:text-foreground";
|
|
1555
|
+
var volumeOpen2 = "group-hover/volume-menu:pointer-events-auto group-hover/volume-menu:translate-y-0 group-hover/volume-menu:opacity-100 group-has-focus-visible/volume-menu:pointer-events-auto group-has-focus-visible/volume-menu:translate-y-0 group-has-focus-visible/volume-menu:opacity-100";
|
|
1556
|
+
function AudioPlayer({
|
|
1557
|
+
title,
|
|
1558
|
+
description,
|
|
1559
|
+
artwork,
|
|
1560
|
+
showSpeed,
|
|
1561
|
+
tooltips = true,
|
|
1562
|
+
className,
|
|
1563
|
+
frameClassName,
|
|
1564
|
+
loader,
|
|
1565
|
+
metadata,
|
|
1566
|
+
persistKey,
|
|
1567
|
+
...props
|
|
1568
|
+
}) {
|
|
1569
|
+
const sessionInfo = metadata ?? (typeof title === "string" ? {
|
|
1570
|
+
title,
|
|
1571
|
+
artist: typeof description === "string" ? description : void 0,
|
|
1572
|
+
artwork: artwork ? [{ src: artwork }] : void 0
|
|
1573
|
+
} : void 0);
|
|
1574
|
+
const hasInfo = Boolean(title || description);
|
|
1575
|
+
const [failedArtwork, setFailedArtwork] = React3.useState(null);
|
|
1576
|
+
const showArtwork = artwork && failedArtwork !== artwork;
|
|
1577
|
+
return /* @__PURE__ */ jsxs4(
|
|
1578
|
+
MediaRoot,
|
|
1579
|
+
{
|
|
1580
|
+
persistKey,
|
|
1581
|
+
metadata: sessionInfo,
|
|
1582
|
+
tooltips,
|
|
1583
|
+
className: cn(
|
|
1584
|
+
"@container/audio grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 rounded-xl border bg-card p-2.5 pr-3 text-card-foreground",
|
|
1585
|
+
frameClassName
|
|
1586
|
+
),
|
|
1587
|
+
children: [
|
|
1588
|
+
/* @__PURE__ */ jsx4(MediaAudio, { className, loader, ...props }),
|
|
1589
|
+
/* @__PURE__ */ jsx4("div", { className: cn("flex items-center", hasInfo && "row-span-2 @max-sm/audio:row-span-1"), children: /* @__PURE__ */ jsxs4("span", { className: "relative shrink-0", children: [
|
|
1590
|
+
showArtwork ? (
|
|
1591
|
+
/**
|
|
1592
|
+
* The tile presses as one piece. Scaling the button alone pulled
|
|
1593
|
+
* its scrim in from the edges and bared a band of the image. No
|
|
1594
|
+
* overflow-hidden, so the play tooltip is not clipped.
|
|
1595
|
+
*/
|
|
1596
|
+
/* @__PURE__ */ jsxs4("span", { className: "relative block size-11 rounded-lg bg-muted transition-[scale] duration-150 ease-media motion-safe:has-[button:enabled:active]:scale-[0.96] motion-reduce:transition-none", children: [
|
|
1597
|
+
/* @__PURE__ */ jsx4(
|
|
1598
|
+
"img",
|
|
1599
|
+
{
|
|
1600
|
+
src: artwork,
|
|
1601
|
+
alt: "",
|
|
1602
|
+
draggable: false,
|
|
1603
|
+
onError: () => setFailedArtwork(artwork),
|
|
1604
|
+
ref: (img) => {
|
|
1605
|
+
if (img?.complete && img.naturalWidth === 0) setFailedArtwork(artwork);
|
|
1606
|
+
},
|
|
1607
|
+
className: "size-full rounded-lg object-cover outline select-none outline-black/10 -outline-offset-1 dark:outline-white/10"
|
|
1608
|
+
}
|
|
1609
|
+
),
|
|
1610
|
+
/* @__PURE__ */ jsx4(
|
|
1611
|
+
MediaPlayButton,
|
|
1612
|
+
{
|
|
1613
|
+
tooltipAlign: "start",
|
|
1614
|
+
className: "absolute inset-0 size-full rounded-lg motion-safe:enabled:active:scale-100 bg-media-scrim/15 text-media-foreground hover:bg-media-scrim/35 focus-visible:bg-media-scrim/35 group-data-playing/media:bg-media-scrim/35 [&_svg]:size-4 [&_svg]:drop-shadow-sm"
|
|
1615
|
+
}
|
|
1616
|
+
)
|
|
1617
|
+
] })
|
|
1618
|
+
) : /* @__PURE__ */ jsx4(
|
|
1619
|
+
MediaPlayButton,
|
|
1620
|
+
{
|
|
1621
|
+
tooltipAlign: "start",
|
|
1622
|
+
className: "size-10 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 [&_svg]:size-4"
|
|
1623
|
+
}
|
|
1624
|
+
),
|
|
1625
|
+
/* @__PURE__ */ jsx4(
|
|
1626
|
+
MediaLoading,
|
|
1627
|
+
{
|
|
1628
|
+
className: showArtwork ? "absolute inset-0 m-auto size-5 text-background" : "absolute -inset-1 size-auto border-current/15 border-t-current text-foreground"
|
|
1629
|
+
}
|
|
1630
|
+
)
|
|
1631
|
+
] }) }),
|
|
1632
|
+
hasInfo && /* @__PURE__ */ jsxs4("div", { className: "col-start-2 flex min-w-0 items-baseline gap-2 self-end text-sm leading-5 @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-2 @max-sm/audio:pt-1", children: [
|
|
1633
|
+
title && /* @__PURE__ */ jsx4("span", { dir: "auto", title: typeof title === "string" ? title : void 0, className: "truncate font-medium", children: title }),
|
|
1634
|
+
description && /* @__PURE__ */ jsx4(
|
|
1635
|
+
"span",
|
|
1636
|
+
{
|
|
1637
|
+
dir: "auto",
|
|
1638
|
+
title: typeof description === "string" ? description : void 0,
|
|
1639
|
+
className: "min-w-12 shrink-[4] truncate text-muted-foreground",
|
|
1640
|
+
children: description
|
|
1641
|
+
}
|
|
1642
|
+
)
|
|
1643
|
+
] }),
|
|
1644
|
+
/* @__PURE__ */ jsxs4(
|
|
1645
|
+
"div",
|
|
1646
|
+
{
|
|
1647
|
+
dir: "ltr",
|
|
1648
|
+
className: cn(
|
|
1649
|
+
"col-start-2 flex items-center gap-2.5 text-muted-foreground",
|
|
1650
|
+
hasInfo && "row-start-2 self-start @max-sm/audio:col-span-3 @max-sm/audio:col-start-1 @max-sm/audio:row-start-3"
|
|
1651
|
+
),
|
|
1652
|
+
children: [
|
|
1653
|
+
/* @__PURE__ */ jsx4(MediaTime, { className: "min-w-9" }),
|
|
1654
|
+
/* @__PURE__ */ jsx4(MediaSeek, { className: "text-foreground" }),
|
|
1655
|
+
/* @__PURE__ */ jsx4(MediaTime, { type: "duration", toggle: true, className: "min-w-9 text-right" })
|
|
1656
|
+
]
|
|
1657
|
+
}
|
|
1658
|
+
),
|
|
1659
|
+
/* @__PURE__ */ jsxs4("div", { className: cn("col-start-3 flex items-center gap-0.5", hasInfo && "row-span-2 row-start-1 @max-sm/audio:row-span-1"), children: [
|
|
1660
|
+
showSpeed && /* @__PURE__ */ jsx4(MediaPlaybackRateButton, { rates: [1, 1.5, 2, 0.75], className: muted }),
|
|
1661
|
+
/* @__PURE__ */ jsxs4("div", { className: "group/volume-menu relative", children: [
|
|
1662
|
+
/* @__PURE__ */ jsx4(MediaMuteButton, { tooltip: false, className: muted }),
|
|
1663
|
+
/* @__PURE__ */ jsx4(
|
|
1664
|
+
"div",
|
|
1665
|
+
{
|
|
1666
|
+
className: cn(
|
|
1667
|
+
"pointer-events-none absolute bottom-full left-1/2 z-10 hidden -translate-x-1/2 translate-y-1 pb-2 opacity-0 pointer-fine:block",
|
|
1668
|
+
"transition-[opacity,translate] duration-150 ease-media motion-reduce:transition-none",
|
|
1669
|
+
volumeOpen2
|
|
1670
|
+
),
|
|
1671
|
+
children: /* @__PURE__ */ jsx4("div", { className: "flex h-24 w-7 justify-center rounded-full bg-popover py-3 text-popover-foreground shadow-md ring-1 ring-foreground/10", children: /* @__PURE__ */ jsx4(MediaVolume, { orientation: "vertical", className: "h-full" }) })
|
|
1672
|
+
}
|
|
1673
|
+
)
|
|
1674
|
+
] })
|
|
1675
|
+
] }),
|
|
1676
|
+
/* @__PURE__ */ jsx4(MediaErrorMessage, { className: "absolute inset-0 flex-row gap-4 rounded-[inherit] bg-card/95 px-4 text-card-foreground" })
|
|
1677
|
+
]
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
export {
|
|
1682
|
+
AudioPlayer,
|
|
1683
|
+
CaptionsIcon,
|
|
1684
|
+
CheckIcon,
|
|
1685
|
+
EnterFullscreenIcon,
|
|
1686
|
+
ExitFullscreenIcon,
|
|
1687
|
+
MediaAudio,
|
|
1688
|
+
MediaButton,
|
|
1689
|
+
MediaCaptions,
|
|
1690
|
+
MediaCaptionsButton,
|
|
1691
|
+
MediaControls,
|
|
1692
|
+
MediaErrorMessage,
|
|
1693
|
+
MediaFullscreenButton,
|
|
1694
|
+
MediaLoading,
|
|
1695
|
+
MediaMuteButton,
|
|
1696
|
+
MediaPictureInPictureButton,
|
|
1697
|
+
MediaPlayButton,
|
|
1698
|
+
MediaPlaybackRateButton,
|
|
1699
|
+
MediaRoot,
|
|
1700
|
+
MediaSeek,
|
|
1701
|
+
MediaSettings,
|
|
1702
|
+
MediaSurface,
|
|
1703
|
+
MediaTime,
|
|
1704
|
+
MediaVideo,
|
|
1705
|
+
MediaVolume,
|
|
1706
|
+
PauseIcon,
|
|
1707
|
+
PictureInPictureIcon,
|
|
1708
|
+
PlayIcon,
|
|
1709
|
+
RetryIcon,
|
|
1710
|
+
SeekIcon,
|
|
1711
|
+
SettingsIcon,
|
|
1712
|
+
VideoPlayer,
|
|
1713
|
+
VolumeIcon,
|
|
1714
|
+
createMediaStore,
|
|
1715
|
+
describeMediaError,
|
|
1716
|
+
formatTime,
|
|
1717
|
+
useMedia,
|
|
1718
|
+
useMediaStore
|
|
1719
|
+
};
|
|
1720
|
+
//# sourceMappingURL=index.js.map
|