bobinejs 0.1.0 → 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/README.md +114 -108
- package/dist/core.d.ts +9 -2
- package/dist/core.js +18 -6
- package/dist/core.js.map +1 -1
- package/dist/hls.d.ts +5 -2
- package/dist/hls.js +5 -4
- package/dist/hls.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +148 -91
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// src/utils.ts
|
|
4
4
|
import { clsx } from "clsx";
|
|
5
|
-
import {
|
|
6
|
-
var twMerge = extendTailwindMerge({
|
|
7
|
-
extend: { theme: { text: ["display", "title", "body", "action", "meta", "code"] } }
|
|
8
|
-
});
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
9
6
|
function cn(...inputs) {
|
|
10
7
|
return twMerge(clsx(inputs));
|
|
11
8
|
}
|
|
@@ -135,6 +132,7 @@ var MEDIA_EVENTS = [
|
|
|
135
132
|
"volumechange",
|
|
136
133
|
"ratechange",
|
|
137
134
|
"emptied",
|
|
135
|
+
"loadstart",
|
|
138
136
|
"error",
|
|
139
137
|
"enterpictureinpicture",
|
|
140
138
|
"leavepictureinpicture"
|
|
@@ -154,6 +152,7 @@ var initialState = {
|
|
|
154
152
|
fullscreen: false,
|
|
155
153
|
pictureInPicture: false,
|
|
156
154
|
error: null,
|
|
155
|
+
hasSource: false,
|
|
157
156
|
textTracks: [],
|
|
158
157
|
textTrack: -1,
|
|
159
158
|
cues: []
|
|
@@ -174,7 +173,13 @@ function captionTracks(media) {
|
|
|
174
173
|
);
|
|
175
174
|
}
|
|
176
175
|
function cueText(cue) {
|
|
177
|
-
|
|
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;
|
|
178
183
|
}
|
|
179
184
|
var sameList = (a, b) => a.length === b.length && a.every((value, i) => Object.is(value, b[i]));
|
|
180
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);
|
|
@@ -191,6 +196,7 @@ function createMediaStore({ persistKey } = {}) {
|
|
|
191
196
|
let container = null;
|
|
192
197
|
let cueTrack = null;
|
|
193
198
|
let metadata = null;
|
|
199
|
+
let trackChosen = false;
|
|
194
200
|
const listeners = /* @__PURE__ */ new Set();
|
|
195
201
|
const set = (patch) => {
|
|
196
202
|
let changed = false;
|
|
@@ -220,7 +226,8 @@ function createMediaStore({ persistKey } = {}) {
|
|
|
220
226
|
muted: media.muted,
|
|
221
227
|
playbackRate: media.playbackRate,
|
|
222
228
|
pictureInPicture: typeof document !== "undefined" && document.pictureInPictureElement === media,
|
|
223
|
-
error: media.error
|
|
229
|
+
error: media.error,
|
|
230
|
+
hasSource: Boolean(media.src || media.currentSrc || media.querySelector("source[src]"))
|
|
224
231
|
});
|
|
225
232
|
};
|
|
226
233
|
const readCues = () => {
|
|
@@ -235,7 +242,7 @@ function createMediaStore({ persistKey } = {}) {
|
|
|
235
242
|
};
|
|
236
243
|
const syncTracks = (requested = state.textTrack) => {
|
|
237
244
|
const tracks = captionTracks(media);
|
|
238
|
-
let active = requested;
|
|
245
|
+
let active = requested === -1 && !trackChosen ? defaultTrack(media, tracks) : requested;
|
|
239
246
|
tracks.forEach((track, i) => {
|
|
240
247
|
if (track.mode === "showing") active = i;
|
|
241
248
|
});
|
|
@@ -444,6 +451,7 @@ function createMediaStore({ persistKey } = {}) {
|
|
|
444
451
|
}
|
|
445
452
|
};
|
|
446
453
|
const syncTracksExplicit = (index) => {
|
|
454
|
+
trackChosen = true;
|
|
447
455
|
captionTracks(media).forEach((track, i) => {
|
|
448
456
|
if (i !== index && track.mode === "showing") track.mode = "disabled";
|
|
449
457
|
});
|
|
@@ -462,13 +470,14 @@ function createMediaStore({ persistKey } = {}) {
|
|
|
462
470
|
...actions
|
|
463
471
|
};
|
|
464
472
|
}
|
|
465
|
-
function formatTime(seconds) {
|
|
473
|
+
function formatTime(seconds, reference = seconds) {
|
|
466
474
|
if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
|
|
467
475
|
const s = Math.floor(seconds % 60);
|
|
468
476
|
const m = Math.floor(seconds / 60 % 60);
|
|
469
477
|
const h = Math.floor(seconds / 3600);
|
|
470
478
|
const ss = s.toString().padStart(2, "0");
|
|
471
|
-
|
|
479
|
+
const hours = h > 0 || Number.isFinite(reference) && reference >= 3600;
|
|
480
|
+
return hours ? `${h}:${m.toString().padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
472
481
|
}
|
|
473
482
|
function describeMediaError(error) {
|
|
474
483
|
switch (error?.code) {
|
|
@@ -479,7 +488,7 @@ function describeMediaError(error) {
|
|
|
479
488
|
case 3:
|
|
480
489
|
return "This media could not be decoded.";
|
|
481
490
|
case 4:
|
|
482
|
-
return "This
|
|
491
|
+
return "This media couldn't be loaded.";
|
|
483
492
|
default:
|
|
484
493
|
return "Something went wrong during playback.";
|
|
485
494
|
}
|
|
@@ -502,8 +511,8 @@ function useMedia(selector) {
|
|
|
502
511
|
() => selector(store.getServerState())
|
|
503
512
|
);
|
|
504
513
|
}
|
|
505
|
-
function useComposedRefs(
|
|
506
|
-
return React2.useCallback(composeRefs(
|
|
514
|
+
function useComposedRefs(a, b, c) {
|
|
515
|
+
return React2.useCallback((node) => composeRefs(a, b, c)(node), [a, b, c]);
|
|
507
516
|
}
|
|
508
517
|
function composeRefs(...refs) {
|
|
509
518
|
return (node) => {
|
|
@@ -643,14 +652,14 @@ function useMediaElement(ref, src, loader) {
|
|
|
643
652
|
if (!loader || !src || !media) return;
|
|
644
653
|
return loader(media, src) ?? void 0;
|
|
645
654
|
}, [store, loader, src]);
|
|
646
|
-
return
|
|
655
|
+
return mediaRef;
|
|
647
656
|
}
|
|
648
657
|
function MediaVideo({ className, ref, src, loader, ...props }) {
|
|
649
|
-
const
|
|
658
|
+
const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
|
|
650
659
|
return /* @__PURE__ */ jsx2(
|
|
651
660
|
"video",
|
|
652
661
|
{
|
|
653
|
-
ref:
|
|
662
|
+
ref: mediaRef,
|
|
654
663
|
"data-slot": "media-video",
|
|
655
664
|
playsInline: true,
|
|
656
665
|
preload: "metadata",
|
|
@@ -662,11 +671,11 @@ function MediaVideo({ className, ref, src, loader, ...props }) {
|
|
|
662
671
|
);
|
|
663
672
|
}
|
|
664
673
|
function MediaAudio({ ref, src, loader, ...props }) {
|
|
665
|
-
const
|
|
674
|
+
const mediaRef = useMediaElement(ref, typeof src === "string" ? src : void 0, loader);
|
|
666
675
|
return /* @__PURE__ */ jsx2(
|
|
667
676
|
"audio",
|
|
668
677
|
{
|
|
669
|
-
ref:
|
|
678
|
+
ref: mediaRef,
|
|
670
679
|
"data-slot": "media-audio",
|
|
671
680
|
preload: "metadata",
|
|
672
681
|
...props,
|
|
@@ -717,12 +726,14 @@ function IconSwap({ show, a, b }) {
|
|
|
717
726
|
function MediaPlayButton({ onClick, ...props }) {
|
|
718
727
|
const store = useMediaStore();
|
|
719
728
|
const paused = useMedia((s) => s.paused);
|
|
729
|
+
const hasSource = useMedia((s) => s.hasSource);
|
|
720
730
|
return /* @__PURE__ */ jsx2(
|
|
721
731
|
MediaButton,
|
|
722
732
|
{
|
|
723
733
|
"aria-label": paused ? "Play" : "Pause",
|
|
724
734
|
tooltip: paused ? "Play" : "Pause",
|
|
725
735
|
shortcut: "K",
|
|
736
|
+
disabled: !hasSource,
|
|
726
737
|
onClick: (event) => {
|
|
727
738
|
onClick?.(event);
|
|
728
739
|
store.toggle();
|
|
@@ -810,6 +821,7 @@ function MediaPlaybackRateButton({
|
|
|
810
821
|
{
|
|
811
822
|
"aria-label": `Playback speed ${rate}x`,
|
|
812
823
|
tooltip: "Playback speed",
|
|
824
|
+
dir: "ltr",
|
|
813
825
|
className: cn("w-(--media-rate-width) min-w-7 px-1.5 font-mono text-xs tabular-nums", className),
|
|
814
826
|
style: { "--media-rate-width": `calc(${chars}ch + 0.75rem)`, ...style },
|
|
815
827
|
onClick: (event) => {
|
|
@@ -1054,7 +1066,7 @@ function MediaSeek({
|
|
|
1054
1066
|
previewClassName
|
|
1055
1067
|
),
|
|
1056
1068
|
style: { left: `${hover * 100}%` },
|
|
1057
|
-
children: formatTime(hover * duration)
|
|
1069
|
+
children: formatTime(hover * duration, duration)
|
|
1058
1070
|
}
|
|
1059
1071
|
)
|
|
1060
1072
|
]
|
|
@@ -1146,19 +1158,21 @@ var timeNames = { current: "elapsed", duration: "duration", remaining: "remainin
|
|
|
1146
1158
|
function MediaTime({ type = "current", toggle = false, className, ...props }) {
|
|
1147
1159
|
const [picked, setPicked] = React2.useState(type);
|
|
1148
1160
|
const shown = toggle ? picked : type;
|
|
1161
|
+
const duration = useMedia((s) => s.duration);
|
|
1149
1162
|
const value = useMedia((s) => {
|
|
1150
|
-
if (shown === "duration") return Math.floor(s.duration);
|
|
1163
|
+
if (shown === "duration") return Math.floor(s.duration) || Math.ceil(s.duration);
|
|
1151
1164
|
if (shown === "remaining") return Math.ceil(Math.max(s.duration - s.currentTime, 0));
|
|
1152
1165
|
return Math.floor(s.currentTime);
|
|
1153
1166
|
});
|
|
1154
|
-
const
|
|
1167
|
+
const time = formatTime(value, duration);
|
|
1168
|
+
const text = shown === "remaining" ? `-${time}` : time;
|
|
1155
1169
|
const other = type === "remaining" ? "current" : "remaining";
|
|
1156
1170
|
const next = shown === type ? other : type;
|
|
1157
|
-
return /* @__PURE__ */ jsx2("span", { "data-slot": "media-time", className: cn("font-mono text-xs tabular-nums", className), ...props, children: toggle ? /* @__PURE__ */ jsxs2(
|
|
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(
|
|
1158
1172
|
"button",
|
|
1159
1173
|
{
|
|
1160
1174
|
type: "button",
|
|
1161
|
-
"aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${
|
|
1175
|
+
"aria-label": `${shown === "remaining" ? "Time remaining" : shown === "duration" ? "Duration" : "Time elapsed"} ${time}`,
|
|
1162
1176
|
onClick: () => setPicked(next),
|
|
1163
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",
|
|
1164
1178
|
children: [
|
|
@@ -1194,7 +1208,7 @@ function MediaCaptions({ className, ...props }) {
|
|
|
1194
1208
|
"data-slot": "media-captions",
|
|
1195
1209
|
className: cn("pointer-events-none flex flex-col items-center gap-1 text-center", className),
|
|
1196
1210
|
...props,
|
|
1197
|
-
children: cues.map((cue, i) => /* @__PURE__ */ jsx2("span", { className: "whitespace-pre-line text-balance rounded-md px-2 py-0.5 box-decoration-clone", children: cue }, `${i}-${cue}`))
|
|
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}`))
|
|
1198
1212
|
}
|
|
1199
1213
|
);
|
|
1200
1214
|
}
|
|
@@ -1229,20 +1243,31 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
|
|
|
1229
1243
|
const rootEl = React2.useRef(null);
|
|
1230
1244
|
const triggerEl = React2.useRef(null);
|
|
1231
1245
|
const panelEl = React2.useRef(null);
|
|
1246
|
+
const listEl = React2.useRef(null);
|
|
1232
1247
|
const panelId = React2.useId();
|
|
1233
1248
|
React2.useLayoutEffect(() => {
|
|
1234
1249
|
const panel = panelEl.current;
|
|
1250
|
+
const list = listEl.current;
|
|
1235
1251
|
const trigger = triggerEl.current;
|
|
1236
1252
|
const player = rootEl.current?.closest("[data-slot='media-root']");
|
|
1237
|
-
if (!open || !panel || !trigger || !player) return;
|
|
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
|
+
};
|
|
1238
1258
|
const fit = () => {
|
|
1239
1259
|
const space = trigger.getBoundingClientRect().top - player.getBoundingClientRect().top - 16;
|
|
1240
1260
|
panel.style.maxHeight = `${Math.max(96, Math.min(288, space))}px`;
|
|
1261
|
+
edge();
|
|
1241
1262
|
};
|
|
1242
1263
|
fit();
|
|
1243
1264
|
const observer = new ResizeObserver(fit);
|
|
1244
1265
|
observer.observe(player);
|
|
1245
|
-
|
|
1266
|
+
list.addEventListener("scroll", edge, { passive: true });
|
|
1267
|
+
return () => {
|
|
1268
|
+
observer.disconnect();
|
|
1269
|
+
list.removeEventListener("scroll", edge);
|
|
1270
|
+
};
|
|
1246
1271
|
}, [open]);
|
|
1247
1272
|
React2.useEffect(() => {
|
|
1248
1273
|
if (!open) return;
|
|
@@ -1283,10 +1308,10 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
|
|
|
1283
1308
|
select();
|
|
1284
1309
|
close();
|
|
1285
1310
|
},
|
|
1286
|
-
className: "flex h-8 w-full cursor-pointer items-center justify-between gap-
|
|
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",
|
|
1287
1312
|
children: [
|
|
1288
|
-
/* @__PURE__ */ jsx2("span", { children: label }),
|
|
1289
|
-
/* @__PURE__ */ jsx2(CheckIcon, { className: cn("size-3.5 transition-opacity duration-150", checked ? "opacity-100" : "opacity-0") })
|
|
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") })
|
|
1290
1315
|
]
|
|
1291
1316
|
},
|
|
1292
1317
|
label
|
|
@@ -1313,7 +1338,7 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
|
|
|
1313
1338
|
children: /* @__PURE__ */ jsx2(SettingsIcon, { className: cn("transition-transform duration-300 ease-media motion-reduce:transition-none", open && "rotate-90") })
|
|
1314
1339
|
}
|
|
1315
1340
|
),
|
|
1316
|
-
open && /* @__PURE__ */
|
|
1341
|
+
open && /* @__PURE__ */ jsx2(
|
|
1317
1342
|
"div",
|
|
1318
1343
|
{
|
|
1319
1344
|
ref: panelEl,
|
|
@@ -1322,40 +1347,47 @@ function MediaSettings({ rates = [0.5, 0.75, 1, 1.25, 1.5, 2], className, panelC
|
|
|
1322
1347
|
"aria-label": "Playback settings",
|
|
1323
1348
|
onKeyDown: onPanelKeyDown,
|
|
1324
1349
|
className: cn(
|
|
1325
|
-
"absolute right-0 bottom-full z-10 mb-2 flex max-h-72 w-
|
|
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",
|
|
1326
1351
|
"transition-[opacity,translate,scale] duration-150 ease-media starting:translate-y-1 starting:scale-95 starting:opacity-0 motion-reduce:transition-none",
|
|
1327
1352
|
panelClassName
|
|
1328
1353
|
),
|
|
1329
|
-
children:
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
"
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
+
)
|
|
1359
1391
|
}
|
|
1360
1392
|
)
|
|
1361
1393
|
]
|
|
@@ -1380,7 +1412,7 @@ function MediaErrorMessage({ className, message, ...props }) {
|
|
|
1380
1412
|
{
|
|
1381
1413
|
type: "button",
|
|
1382
1414
|
onClick: () => store.retry(),
|
|
1383
|
-
className: "inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm 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]",
|
|
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]",
|
|
1384
1416
|
children: [
|
|
1385
1417
|
/* @__PURE__ */ jsx2(RetryIcon, { className: "size-3.5" }),
|
|
1386
1418
|
"Try again"
|
|
@@ -1462,7 +1494,8 @@ function VideoPlayer({
|
|
|
1462
1494
|
MediaCaptions,
|
|
1463
1495
|
{
|
|
1464
1496
|
className: cn(
|
|
1465
|
-
|
|
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",
|
|
1466
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",
|
|
1467
1500
|
"[&>span]:bg-media-scrim/75 [&>span]:text-media-foreground"
|
|
1468
1501
|
)
|
|
@@ -1479,29 +1512,36 @@ function VideoPlayer({
|
|
|
1479
1512
|
)
|
|
1480
1513
|
}
|
|
1481
1514
|
),
|
|
1482
|
-
/* @__PURE__ */ jsxs3(
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
"
|
|
1490
|
-
{
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
+
),
|
|
1505
1545
|
/* @__PURE__ */ jsx3(MediaErrorMessage, { className: "absolute inset-0 bg-media-scrim/85 px-6 text-media-foreground backdrop-blur-sm" })
|
|
1506
1546
|
]
|
|
1507
1547
|
}
|
|
@@ -1509,6 +1549,7 @@ function VideoPlayer({
|
|
|
1509
1549
|
}
|
|
1510
1550
|
|
|
1511
1551
|
// ../../components/ui/audio-player.tsx
|
|
1552
|
+
import * as React3 from "react";
|
|
1512
1553
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1513
1554
|
var muted = "text-muted-foreground hover:text-foreground";
|
|
1514
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";
|
|
@@ -1531,6 +1572,8 @@ function AudioPlayer({
|
|
|
1531
1572
|
artwork: artwork ? [{ src: artwork }] : void 0
|
|
1532
1573
|
} : void 0);
|
|
1533
1574
|
const hasInfo = Boolean(title || description);
|
|
1575
|
+
const [failedArtwork, setFailedArtwork] = React3.useState(null);
|
|
1576
|
+
const showArtwork = artwork && failedArtwork !== artwork;
|
|
1534
1577
|
return /* @__PURE__ */ jsxs4(
|
|
1535
1578
|
MediaRoot,
|
|
1536
1579
|
{
|
|
@@ -1544,7 +1587,7 @@ function AudioPlayer({
|
|
|
1544
1587
|
children: [
|
|
1545
1588
|
/* @__PURE__ */ jsx4(MediaAudio, { className, loader, ...props }),
|
|
1546
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: [
|
|
1547
|
-
|
|
1590
|
+
showArtwork ? (
|
|
1548
1591
|
/**
|
|
1549
1592
|
* The tile presses as one piece. Scaling the button alone pulled
|
|
1550
1593
|
* its scrim in from the edges and bared a band of the image. No
|
|
@@ -1556,7 +1599,12 @@ function AudioPlayer({
|
|
|
1556
1599
|
{
|
|
1557
1600
|
src: artwork,
|
|
1558
1601
|
alt: "",
|
|
1559
|
-
|
|
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"
|
|
1560
1608
|
}
|
|
1561
1609
|
),
|
|
1562
1610
|
/* @__PURE__ */ jsx4(
|
|
@@ -1577,17 +1625,26 @@ function AudioPlayer({
|
|
|
1577
1625
|
/* @__PURE__ */ jsx4(
|
|
1578
1626
|
MediaLoading,
|
|
1579
1627
|
{
|
|
1580
|
-
className:
|
|
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"
|
|
1581
1629
|
}
|
|
1582
1630
|
)
|
|
1583
1631
|
] }) }),
|
|
1584
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: [
|
|
1585
|
-
title && /* @__PURE__ */ jsx4("span", { className: "truncate font-medium", children: title }),
|
|
1586
|
-
description && /* @__PURE__ */ jsx4(
|
|
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
|
+
)
|
|
1587
1643
|
] }),
|
|
1588
1644
|
/* @__PURE__ */ jsxs4(
|
|
1589
1645
|
"div",
|
|
1590
1646
|
{
|
|
1647
|
+
dir: "ltr",
|
|
1591
1648
|
className: cn(
|
|
1592
1649
|
"col-start-2 flex items-center gap-2.5 text-muted-foreground",
|
|
1593
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"
|