mikuru 1.0.31 → 1.0.33
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/CHANGELOG.md +10 -1
- package/README.md +3 -3
- package/components/MikuruAudioPlayer.mikuru +157 -21
- package/components/MikuruToast.mikuru +52 -4
- package/components/MikuruVideoPlayer.mikuru +317 -48
- package/package.json +1 -1
- package/types/components/MikuruAudioPlayer.d.ts +10 -0
- package/types/components/MikuruToast.d.ts +2 -0
- package/types/components/MikuruVideoPlayer.d.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
3
|
+
## 1.0.33 - 2026-05-16
|
|
4
4
|
|
|
5
|
+
- Added `MikuruVideoPlayer` sizing props for width, height, and aspect ratio.
|
|
6
|
+
- Added a `MikuruVideoPlayer` settings menu for quality selection, playback speed, and keyboard skip seconds.
|
|
7
|
+
|
|
8
|
+
## 1.0.32 - 2026-05-16
|
|
9
|
+
|
|
10
|
+
- Added `controls` and `live` props to `MikuruVideoPlayer` and `MikuruAudioPlayer` so callers can choose visible controls and render live-stream UI without seek controls.
|
|
11
|
+
- Removed the `MikuruVideoPlayer` stop control so video playback uses the same play/pause-only primary transport as the center control.
|
|
12
|
+
- Updated `MikuruAudioPlayer` controls to use icon buttons for play, skip, mute, and volume-facing actions, matching the video player control style.
|
|
13
|
+
- Added timed auto-dismiss to `MikuruToast` with stack-level and per-toast `duration` controls.
|
|
5
14
|
- Exported `MikuruVideoPlayer` media events so parent components can listen for playback, timing, seeking, volume, and playback-rate changes with typed media state payloads.
|
|
6
15
|
- Exported matching `MikuruAudioPlayer` media events and documented the shared media player event payload.
|
|
7
16
|
|
package/README.md
CHANGED
|
@@ -270,12 +270,12 @@ npm run dev:mikuru-vue-like
|
|
|
270
270
|
|
|
271
271
|
The package also includes original Mikuru components:
|
|
272
272
|
|
|
273
|
-
- `MikuruVideoPlayer.mikuru`: overlay video controls, div-based seeking, volume/mute, playback rate,
|
|
274
|
-
- `MikuruAudioPlayer.mikuru`: audio playback with seeking, skip controls, volume, and mute.
|
|
273
|
+
- `MikuruVideoPlayer.mikuru`: overlay video controls, configurable sizing, configurable control visibility, live mode, settings menu, div-based seeking, volume/mute, playback rate, and fullscreen controls.
|
|
274
|
+
- `MikuruAudioPlayer.mikuru`: audio playback with configurable control visibility, live mode, seeking, skip controls, volume, and mute.
|
|
275
275
|
- `MikuruImageViewer.mikuru`: image zoom, pan, rotate, reset, and fullscreen controls.
|
|
276
276
|
- `MikuruModal.mikuru`: accessible modal shell with backdrop, Escape close, slots, and close events.
|
|
277
277
|
- `MikuruCarousel.mikuru`: image carousel with arrows, dots, keyboard navigation, and optional autoplay.
|
|
278
|
-
- `MikuruToast.mikuru`: fixed notification stack with dismiss events and tone variants.
|
|
278
|
+
- `MikuruToast.mikuru`: fixed notification stack with timed auto-dismiss, dismiss events, and tone variants.
|
|
279
279
|
- `MikuruDropdown.mikuru`: menu button with outside-click close, Escape handling, and select events.
|
|
280
280
|
- `MikuruToolTip.mikuru`: hover/focus tooltip with configurable placement.
|
|
281
281
|
- `MikuruProgress.mikuru`: determinate and indeterminate progress indicator.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="mikuru-audio" :class="{ 'is-playing': isPlaying }" :aria-label="title">
|
|
2
|
+
<section class="mikuru-audio" :class="{ 'is-playing': isPlaying, 'is-live': isLive }" :aria-label="title">
|
|
3
3
|
<audio
|
|
4
4
|
ref="mediaEl"
|
|
5
5
|
:src="src"
|
|
@@ -22,28 +22,33 @@
|
|
|
22
22
|
<div class="body">
|
|
23
23
|
<div class="identity">
|
|
24
24
|
<strong>{{ title }}</strong>
|
|
25
|
-
<span>{{
|
|
25
|
+
<span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
|
-
<div class="timeline">
|
|
29
|
-
<span>{{ formatTime(currentTime) }}</span>
|
|
30
|
-
<input type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
|
|
31
|
-
<span>{{ formatTime(duration) }}</span>
|
|
28
|
+
<div m-if="showTimeline" class="timeline" :class="timelineClass">
|
|
29
|
+
<span m-if="showTimeControl">{{ formatTime(currentTime) }}</span>
|
|
30
|
+
<input m-if="showSeekControl" type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
|
|
31
|
+
<span m-if="showTimeControl">{{ formatTime(duration) }}</span>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<div class="controls">
|
|
35
|
-
<button type="button" @click="skipBackward"
|
|
36
|
-
|
|
37
|
-
<span>{{ playText }}</span>
|
|
34
|
+
<div m-if="showControls" class="controls">
|
|
35
|
+
<button m-if="showSkipControl" class="icon-button" type="button" @click="skipBackward" aria-label="Back 10 seconds">
|
|
36
|
+
<span class="fa-icon icon-backward" aria-hidden="true"></span>
|
|
38
37
|
</button>
|
|
39
|
-
<button type="button" @click="
|
|
40
|
-
|
|
41
|
-
<span>Sound</span>
|
|
38
|
+
<button m-if="showPlayControl" class="icon-button primary" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
39
|
+
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
42
40
|
</button>
|
|
43
|
-
<
|
|
41
|
+
<button m-if="showSkipControl" class="icon-button" type="button" @click="skipForward" aria-label="Forward 10 seconds">
|
|
42
|
+
<span class="fa-icon icon-forward" aria-hidden="true"></span>
|
|
43
|
+
</button>
|
|
44
|
+
<button m-if="showMuteControl" class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
45
|
+
<span class="fa-icon" :class="muteIconClass" aria-hidden="true"></span>
|
|
46
|
+
</button>
|
|
47
|
+
<label m-if="showVolumeControl" class="volume">
|
|
44
48
|
<span>Volume</span>
|
|
45
49
|
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
46
50
|
</label>
|
|
51
|
+
<span m-if="isLive" class="live-clock">LIVE</span>
|
|
47
52
|
</div>
|
|
48
53
|
</div>
|
|
49
54
|
</section>
|
|
@@ -56,12 +61,16 @@ const {
|
|
|
56
61
|
src,
|
|
57
62
|
title = "Mikuru Audio",
|
|
58
63
|
artist = "Original player component",
|
|
59
|
-
preload = "metadata"
|
|
64
|
+
preload = "metadata",
|
|
65
|
+
controls,
|
|
66
|
+
live = false
|
|
60
67
|
} = defineProps({
|
|
61
68
|
src: String,
|
|
62
69
|
title: String,
|
|
63
70
|
artist: String,
|
|
64
|
-
preload: String
|
|
71
|
+
preload: String,
|
|
72
|
+
controls: Array,
|
|
73
|
+
live: Boolean
|
|
65
74
|
});
|
|
66
75
|
|
|
67
76
|
const emit = defineEmits([
|
|
@@ -82,10 +91,37 @@ const duration = ref(0);
|
|
|
82
91
|
const volume = ref(0.75);
|
|
83
92
|
const muted = ref(false);
|
|
84
93
|
const isPlaying = ref(false);
|
|
94
|
+
const allControls = ["play", "seek", "time", "skip", "mute", "volume"];
|
|
95
|
+
const liveHiddenControls = ["seek", "time", "skip"];
|
|
96
|
+
const activeControls = computed(() => {
|
|
97
|
+
const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
|
|
98
|
+
return new Set(configuredControls);
|
|
99
|
+
});
|
|
100
|
+
const isLive = computed(() => live.value === true);
|
|
85
101
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
86
102
|
const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
|
|
87
103
|
const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
|
|
88
|
-
const
|
|
104
|
+
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
105
|
+
const muteIconClass = computed(() => muted.value ? "icon-mute" : "icon-volume");
|
|
106
|
+
const statusText = computed(() => isLive.value ? "LIVE" : artist.value);
|
|
107
|
+
const showPlayControl = computed(() => hasControl("play"));
|
|
108
|
+
const showSeekControl = computed(() => hasControl("seek"));
|
|
109
|
+
const showTimeControl = computed(() => hasControl("time"));
|
|
110
|
+
const showSkipControl = computed(() => hasControl("skip"));
|
|
111
|
+
const showMuteControl = computed(() => hasControl("mute"));
|
|
112
|
+
const showVolumeControl = computed(() => hasControl("volume"));
|
|
113
|
+
const showTimeline = computed(() => showSeekControl.value || showTimeControl.value);
|
|
114
|
+
const timelineClass = computed(() => ({
|
|
115
|
+
"timeline-seek-only": showSeekControl.value && !showTimeControl.value,
|
|
116
|
+
"timeline-time-only": showTimeControl.value && !showSeekControl.value
|
|
117
|
+
}));
|
|
118
|
+
const showControls = computed(() => (
|
|
119
|
+
showPlayControl.value ||
|
|
120
|
+
showSkipControl.value ||
|
|
121
|
+
showMuteControl.value ||
|
|
122
|
+
showVolumeControl.value ||
|
|
123
|
+
isLive.value
|
|
124
|
+
));
|
|
89
125
|
const initials = computed(() => {
|
|
90
126
|
const words = title.value.split(" ").filter(Boolean);
|
|
91
127
|
return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
|
|
@@ -114,6 +150,11 @@ function shouldIgnoreMediaEvent() {
|
|
|
114
150
|
return performance.now() < ignoreMediaEventsUntil;
|
|
115
151
|
}
|
|
116
152
|
|
|
153
|
+
function hasControl(name) {
|
|
154
|
+
if (isLive.value && liveHiddenControls.includes(name)) return false;
|
|
155
|
+
return activeControls.value.has(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
117
158
|
function syncMedia() {
|
|
118
159
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
119
160
|
const media = getMedia();
|
|
@@ -122,6 +163,14 @@ function syncMedia() {
|
|
|
122
163
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
123
164
|
}
|
|
124
165
|
|
|
166
|
+
function syncVolumeState() {
|
|
167
|
+
if (isDisposed) return;
|
|
168
|
+
const media = getMedia();
|
|
169
|
+
if (!media) return;
|
|
170
|
+
volume.value = media.volume;
|
|
171
|
+
muted.value = media.muted;
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
function createMediaPayload(event) {
|
|
126
175
|
const media = getMedia();
|
|
127
176
|
if (!media) return null;
|
|
@@ -166,6 +215,7 @@ function handleSeeked(event) {
|
|
|
166
215
|
|
|
167
216
|
function handleVolumeChange(event) {
|
|
168
217
|
syncMedia();
|
|
218
|
+
syncVolumeState();
|
|
169
219
|
emitMediaPayload(event, (payload) => emit("volumechange", payload));
|
|
170
220
|
}
|
|
171
221
|
|
|
@@ -235,19 +285,19 @@ function setVolume(event) {
|
|
|
235
285
|
if (!media) return;
|
|
236
286
|
media.volume = nextVolume;
|
|
237
287
|
media.muted = nextVolume === 0;
|
|
288
|
+
volume.value = nextVolume;
|
|
289
|
+
muted.value = media.muted;
|
|
238
290
|
}, 0);
|
|
239
291
|
}
|
|
240
292
|
|
|
241
|
-
function toggleMute(
|
|
293
|
+
function toggleMute() {
|
|
242
294
|
if (isDisposed) return;
|
|
243
|
-
const button = event.currentTarget;
|
|
244
295
|
window.setTimeout(() => {
|
|
245
296
|
if (isDisposed) return;
|
|
246
297
|
const media = getMedia();
|
|
247
298
|
if (!media) return;
|
|
248
299
|
media.muted = !media.muted;
|
|
249
|
-
|
|
250
|
-
button.setAttribute("aria-label", media.muted ? "Unmute audio" : "Mute audio");
|
|
300
|
+
muted.value = media.muted;
|
|
251
301
|
}, 0);
|
|
252
302
|
}
|
|
253
303
|
|
|
@@ -318,6 +368,32 @@ function formatTime(seconds) {
|
|
|
318
368
|
font-size: 0.9rem;
|
|
319
369
|
}
|
|
320
370
|
|
|
371
|
+
.live-badge,
|
|
372
|
+
.live-clock {
|
|
373
|
+
color: #ffffff;
|
|
374
|
+
font-size: 0.78rem;
|
|
375
|
+
font-weight: 800;
|
|
376
|
+
letter-spacing: 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.live-badge {
|
|
380
|
+
display: inline-flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
width: max-content;
|
|
383
|
+
padding: 4px 8px;
|
|
384
|
+
border-radius: 999px;
|
|
385
|
+
background: #dc2626;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.live-clock {
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
min-height: 24px;
|
|
392
|
+
padding: 0 8px;
|
|
393
|
+
border-radius: 999px;
|
|
394
|
+
background: #dc2626;
|
|
395
|
+
}
|
|
396
|
+
|
|
321
397
|
.timeline {
|
|
322
398
|
display: grid;
|
|
323
399
|
grid-template-columns: 42px minmax(0, 1fr) 42px;
|
|
@@ -325,6 +401,15 @@ function formatTime(seconds) {
|
|
|
325
401
|
align-items: center;
|
|
326
402
|
}
|
|
327
403
|
|
|
404
|
+
.timeline-seek-only {
|
|
405
|
+
grid-template-columns: minmax(0, 1fr);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.timeline-time-only {
|
|
409
|
+
grid-template-columns: repeat(2, 42px);
|
|
410
|
+
justify-content: space-between;
|
|
411
|
+
}
|
|
412
|
+
|
|
328
413
|
.timeline span:last-child {
|
|
329
414
|
text-align: right;
|
|
330
415
|
}
|
|
@@ -342,6 +427,14 @@ function formatTime(seconds) {
|
|
|
342
427
|
gap: 8px;
|
|
343
428
|
}
|
|
344
429
|
|
|
430
|
+
.volume span {
|
|
431
|
+
position: absolute;
|
|
432
|
+
width: 1px;
|
|
433
|
+
height: 1px;
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
clip: rect(0 0 0 0);
|
|
436
|
+
}
|
|
437
|
+
|
|
345
438
|
input[type="range"] {
|
|
346
439
|
accent-color: #0f766e;
|
|
347
440
|
}
|
|
@@ -367,6 +460,49 @@ button:hover {
|
|
|
367
460
|
background: #0f766e;
|
|
368
461
|
}
|
|
369
462
|
|
|
463
|
+
.icon-button {
|
|
464
|
+
display: inline-grid;
|
|
465
|
+
place-items: center;
|
|
466
|
+
width: 38px;
|
|
467
|
+
padding: 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.fa-icon {
|
|
471
|
+
display: block;
|
|
472
|
+
width: 18px;
|
|
473
|
+
height: 18px;
|
|
474
|
+
background: currentColor;
|
|
475
|
+
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.28));
|
|
476
|
+
mask-position: center;
|
|
477
|
+
mask-repeat: no-repeat;
|
|
478
|
+
mask-size: contain;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.icon-play {
|
|
482
|
+
transform: translateX(1px);
|
|
483
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M187.2 100.9C174.8 94.1 159.8 94.4 147.6 101.6C135.4 108.8 128 121.9 128 136L128 504C128 518.1 135.5 531.2 147.6 538.4C159.7 545.6 174.8 545.9 187.2 539.1L523.2 355.1C536 348.1 544 334.6 544 320C544 305.4 536 291.9 523.2 284.9L187.2 100.9z"/></svg>');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.icon-pause {
|
|
487
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.icon-backward {
|
|
491
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M288 128L96 320L288 512L288 128zM544 128L352 320L544 512L544 128z"/></svg>');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.icon-forward {
|
|
495
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M96 128L288 320L96 512L96 128zM352 128L544 320L352 512L352 128z"/></svg>');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.icon-volume {
|
|
499
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M112 416L160 416L294.1 535.2C300.5 540.9 308.7 544 317.2 544C336.4 544 352 528.4 352 509.2L352 130.8C352 111.6 336.4 96 317.2 96C308.7 96 300.5 99.1 294.1 104.8L160 224L112 224C85.5 224 64 245.5 64 272L64 368C64 394.5 85.5 416 112 416zM505.1 171C494.8 162.6 479.7 164.2 471.3 174.5C462.9 184.8 464.5 199.9 474.8 208.3C507.3 234.7 528 274.9 528 320C528 365.1 507.3 405.3 474.8 431.8C464.5 440.2 463 455.3 471.3 465.6C479.6 475.9 494.8 477.4 505.1 469.1C548.3 433.9 576 380.2 576 320.1C576 260 548.3 206.3 505.1 171.1zM444.6 245.5C434.3 237.1 419.2 238.7 410.8 249C402.4 259.3 404 274.4 414.3 282.8C425.1 291.6 432 305 432 320C432 335 425.1 348.4 414.3 357.3C404 365.7 402.5 380.8 410.8 391.1C419.1 401.4 434.3 402.9 444.6 394.6C466.1 376.9 480 350.1 480 320C480 289.9 466.1 263.1 444.5 245.5z"/></svg>');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.icon-mute {
|
|
503
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M80 416L128 416L262.1 535.2C268.5 540.9 276.7 544 285.2 544C304.4 544 320 528.4 320 509.2L320 130.8C320 111.6 304.4 96 285.2 96C276.7 96 268.5 99.1 262.1 104.8L128 224L80 224C53.5 224 32 245.5 32 272L32 368C32 394.5 53.5 416 80 416zM399 239C389.6 248.4 389.6 263.6 399 272.9L446 319.9L399 366.9C389.6 376.3 389.6 391.5 399 400.8C408.4 410.1 423.6 410.2 432.9 400.8L479.9 353.8L526.9 400.8C536.3 410.2 551.5 410.2 560.8 400.8C570.1 391.4 570.2 376.2 560.8 366.9L513.8 319.9L560.8 272.9C570.2 263.5 570.2 248.3 560.8 239C551.4 229.7 536.2 229.6 526.9 239L479.9 286L432.9 239C423.5 229.6 408.3 229.6 399 239z"/></svg>');
|
|
504
|
+
}
|
|
505
|
+
|
|
370
506
|
@media (max-width: 620px) {
|
|
371
507
|
.mikuru-audio {
|
|
372
508
|
grid-template-columns: 1fr;
|
|
@@ -17,36 +17,84 @@
|
|
|
17
17
|
</template>
|
|
18
18
|
|
|
19
19
|
<script>
|
|
20
|
-
import { computed } from "mikuru";
|
|
20
|
+
import { computed, onMounted, onUnmounted, watch } from "mikuru";
|
|
21
21
|
|
|
22
22
|
const {
|
|
23
23
|
toasts = [],
|
|
24
|
-
position = "bottom-right"
|
|
24
|
+
position = "bottom-right",
|
|
25
|
+
duration = 5000
|
|
25
26
|
} = defineProps({
|
|
26
27
|
toasts: Array,
|
|
27
|
-
position: String
|
|
28
|
+
position: String,
|
|
29
|
+
duration: Number
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
const emit = defineEmits(["dismiss"]);
|
|
33
|
+
const timers = new Map();
|
|
31
34
|
const positionClass = computed(() => `position-${position.value}`);
|
|
32
35
|
const normalizedToasts = computed(() => {
|
|
33
36
|
const source = Array.isArray(toasts.value) ? toasts.value : [];
|
|
34
37
|
return source.map((toast, index) => {
|
|
35
38
|
const id = toast.id ?? index;
|
|
36
39
|
const tone = toast.tone || "info";
|
|
40
|
+
const toastDuration = typeof toast.duration === "number" ? toast.duration : duration.value;
|
|
37
41
|
return {
|
|
38
42
|
id,
|
|
39
43
|
title: toast.title || "Notification",
|
|
40
44
|
message: toast.message || "",
|
|
41
45
|
toneClass: `tone-${tone}`,
|
|
42
|
-
dismissLabel: `Dismiss ${toast.title || "notification"}
|
|
46
|
+
dismissLabel: `Dismiss ${toast.title || "notification"}`,
|
|
47
|
+
duration: toastDuration
|
|
43
48
|
};
|
|
44
49
|
});
|
|
45
50
|
});
|
|
46
51
|
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
syncToastTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
onUnmounted(() => {
|
|
57
|
+
clearToastTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
watch(toasts, () => {
|
|
61
|
+
syncToastTimers();
|
|
62
|
+
});
|
|
63
|
+
|
|
47
64
|
function dismissToast(id) {
|
|
65
|
+
clearToastTimer(id);
|
|
48
66
|
emit("dismiss", id);
|
|
49
67
|
}
|
|
68
|
+
|
|
69
|
+
function syncToastTimers() {
|
|
70
|
+
const visibleIds = new Set(normalizedToasts.value.map((toast) => toast.id));
|
|
71
|
+
Array.from(timers.keys()).forEach((id) => {
|
|
72
|
+
if (!visibleIds.has(id)) {
|
|
73
|
+
clearToastTimer(id);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
normalizedToasts.value.forEach((toast) => {
|
|
78
|
+
if (timers.has(toast.id)) return;
|
|
79
|
+
if (!Number.isFinite(toast.duration) || toast.duration <= 0) return;
|
|
80
|
+
const timer = window.setTimeout(() => {
|
|
81
|
+
timers.delete(toast.id);
|
|
82
|
+
emit("dismiss", toast.id);
|
|
83
|
+
}, toast.duration);
|
|
84
|
+
timers.set(toast.id, timer);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearToastTimer(id) {
|
|
89
|
+
const timer = timers.get(id);
|
|
90
|
+
if (!timer) return;
|
|
91
|
+
window.clearTimeout(timer);
|
|
92
|
+
timers.delete(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clearToastTimers() {
|
|
96
|
+
Array.from(timers.keys()).forEach((id) => clearToastTimer(id));
|
|
97
|
+
}
|
|
50
98
|
</script>
|
|
51
99
|
|
|
52
100
|
<style scoped>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<section
|
|
3
3
|
class="mikuru-video"
|
|
4
4
|
:class="{ 'is-playing': isPlaying, 'controls-visible': controlsVisible }"
|
|
5
|
+
:style="playerStyle"
|
|
5
6
|
:aria-label="title"
|
|
6
7
|
@mouseenter="showControls"
|
|
7
8
|
@mouseleave="hideControls"
|
|
@@ -9,12 +10,12 @@
|
|
|
9
10
|
@pointerleave="hideControls"
|
|
10
11
|
@focusin="showControls"
|
|
11
12
|
>
|
|
12
|
-
<div class="screen" ref="screenEl">
|
|
13
|
+
<div class="screen" ref="screenEl" :style="screenStyle">
|
|
13
14
|
<video
|
|
14
15
|
ref="mediaEl"
|
|
15
16
|
class="media"
|
|
16
|
-
:src="
|
|
17
|
-
:poster="
|
|
17
|
+
:src="selectedVideoSrc"
|
|
18
|
+
:poster="selectedPoster"
|
|
18
19
|
:preload="preload"
|
|
19
20
|
playsinline
|
|
20
21
|
@loadedmetadata="handleLoadedMetadata"
|
|
@@ -31,15 +32,16 @@
|
|
|
31
32
|
|
|
32
33
|
<div class="top-bar">
|
|
33
34
|
<strong>{{ title }}</strong>
|
|
34
|
-
<span>{{
|
|
35
|
+
<span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
|
|
35
36
|
</div>
|
|
36
37
|
|
|
37
|
-
<button class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
38
|
+
<button m-if="showPlayControl" class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
38
39
|
<span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
|
|
39
40
|
</button>
|
|
40
41
|
|
|
41
|
-
<div class="control-shelf">
|
|
42
|
+
<div m-if="showControlShelf" class="control-shelf">
|
|
42
43
|
<div
|
|
44
|
+
m-if="showSeekControl"
|
|
43
45
|
class="seek"
|
|
44
46
|
role="slider"
|
|
45
47
|
tabindex="0"
|
|
@@ -62,25 +64,76 @@
|
|
|
62
64
|
|
|
63
65
|
<div class="control-row">
|
|
64
66
|
<div class="left-controls">
|
|
65
|
-
<button class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
67
|
+
<button m-if="showPlayControl" class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
66
68
|
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
67
69
|
</button>
|
|
68
|
-
<button class="icon-button" type="button" @click="
|
|
69
|
-
<span class="fa-icon icon-stop" aria-hidden="true"></span>
|
|
70
|
-
</button>
|
|
71
|
-
<button class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
70
|
+
<button m-if="showMuteControl" class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
72
71
|
<span class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
73
72
|
</button>
|
|
74
|
-
<label class="volume">
|
|
73
|
+
<label m-if="showVolumeControl" class="volume">
|
|
75
74
|
<span>Volume</span>
|
|
76
75
|
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
77
76
|
</label>
|
|
78
|
-
<span class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
77
|
+
<span m-if="showTimeControl" class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
79
78
|
</div>
|
|
80
79
|
|
|
81
80
|
<div class="right-controls">
|
|
82
|
-
<
|
|
83
|
-
|
|
81
|
+
<div m-if="showSettingsControl" class="settings" ref="settingsEl" @click="stopEvent">
|
|
82
|
+
<button
|
|
83
|
+
class="icon-button settings-button"
|
|
84
|
+
type="button"
|
|
85
|
+
@click="toggleSettings"
|
|
86
|
+
aria-label="Video settings"
|
|
87
|
+
:aria-expanded="settingsOpen"
|
|
88
|
+
>
|
|
89
|
+
<span class="fa-icon icon-settings" aria-hidden="true"></span>
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
<div m-if="settingsOpen" class="settings-menu" role="menu" aria-label="Video settings">
|
|
93
|
+
<div class="settings-group">
|
|
94
|
+
<span class="settings-label">Quality</span>
|
|
95
|
+
<button
|
|
96
|
+
m-for="quality in normalizedQualityOptions"
|
|
97
|
+
:key="quality.id"
|
|
98
|
+
type="button"
|
|
99
|
+
class="settings-option"
|
|
100
|
+
:class="{ active: quality.id === activeQualityId }"
|
|
101
|
+
@click="selectQuality(quality.id)"
|
|
102
|
+
>
|
|
103
|
+
{{ quality.label }}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="settings-group">
|
|
108
|
+
<span class="settings-label">Speed</span>
|
|
109
|
+
<button
|
|
110
|
+
m-for="rate in rateOptions"
|
|
111
|
+
:key="rate.value"
|
|
112
|
+
type="button"
|
|
113
|
+
class="settings-option"
|
|
114
|
+
:class="{ active: rate.value === playbackRateValue }"
|
|
115
|
+
@click="setPlaybackRate(rate.value)"
|
|
116
|
+
>
|
|
117
|
+
{{ rate.label }}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="settings-group">
|
|
122
|
+
<span class="settings-label">Skip</span>
|
|
123
|
+
<button
|
|
124
|
+
m-for="option in skipOptions"
|
|
125
|
+
:key="option.value"
|
|
126
|
+
type="button"
|
|
127
|
+
class="settings-option"
|
|
128
|
+
:class="{ active: option.value === skipSeconds }"
|
|
129
|
+
@click="setSkipSeconds(option.value)"
|
|
130
|
+
>
|
|
131
|
+
{{ option.label }}
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<button m-if="showFullscreenControl" class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
|
|
84
137
|
<span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
|
|
85
138
|
</button>
|
|
86
139
|
</div>
|
|
@@ -98,13 +151,25 @@ const {
|
|
|
98
151
|
poster = "",
|
|
99
152
|
title = "Mikuru Video",
|
|
100
153
|
subtitle = "Original player component",
|
|
101
|
-
preload = "metadata"
|
|
154
|
+
preload = "metadata",
|
|
155
|
+
width,
|
|
156
|
+
height,
|
|
157
|
+
aspectRatio,
|
|
158
|
+
qualityOptions = [],
|
|
159
|
+
controls,
|
|
160
|
+
live = false
|
|
102
161
|
} = defineProps({
|
|
103
162
|
src: String,
|
|
104
163
|
poster: String,
|
|
105
164
|
title: String,
|
|
106
165
|
subtitle: String,
|
|
107
|
-
preload: String
|
|
166
|
+
preload: String,
|
|
167
|
+
width: String,
|
|
168
|
+
height: String,
|
|
169
|
+
aspectRatio: String,
|
|
170
|
+
qualityOptions: Array,
|
|
171
|
+
controls: Array,
|
|
172
|
+
live: Boolean
|
|
108
173
|
});
|
|
109
174
|
|
|
110
175
|
const emit = defineEmits([
|
|
@@ -128,8 +193,47 @@ const muted = ref(false);
|
|
|
128
193
|
const isPlaying = ref(false);
|
|
129
194
|
const isFullscreen = ref(false);
|
|
130
195
|
const isSeeking = ref(false);
|
|
196
|
+
const settingsOpen = ref(false);
|
|
197
|
+
const selectedQualityId = ref("auto");
|
|
198
|
+
const playbackRateValue = ref(1);
|
|
199
|
+
const skipSeconds = ref(5);
|
|
131
200
|
const pointerInside = ref(false);
|
|
132
201
|
const controlsVisible = ref(true);
|
|
202
|
+
const settingsEl = ref(null);
|
|
203
|
+
const allControls = ["play", "seek", "time", "mute", "volume", "settings", "fullscreen"];
|
|
204
|
+
const liveHiddenControls = ["seek", "time"];
|
|
205
|
+
const activeControls = computed(() => {
|
|
206
|
+
const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
|
|
207
|
+
return new Set(configuredControls);
|
|
208
|
+
});
|
|
209
|
+
const normalizedQualityOptions = computed(() => {
|
|
210
|
+
const source = Array.isArray(qualityOptions.value) ? qualityOptions.value : [];
|
|
211
|
+
const options = source
|
|
212
|
+
.map((option, index) => normalizeQualityOption(option, index))
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
return options.length > 0
|
|
215
|
+
? options
|
|
216
|
+
: [{ id: "auto", label: "Auto", src: src.value, poster: poster.value }];
|
|
217
|
+
});
|
|
218
|
+
const selectedQuality = computed(() =>
|
|
219
|
+
normalizedQualityOptions.value.find((option) => option.id === selectedQualityId.value) ?? normalizedQualityOptions.value[0]
|
|
220
|
+
);
|
|
221
|
+
const activeQualityId = computed(() => selectedQuality.value?.id);
|
|
222
|
+
const selectedVideoSrc = computed(() => selectedQuality.value?.src || src.value);
|
|
223
|
+
const selectedPoster = computed(() => selectedQuality.value?.poster ?? poster.value);
|
|
224
|
+
const playerStyle = computed(() => {
|
|
225
|
+
const normalizedWidth = normalizeCssSize(width.value);
|
|
226
|
+
return normalizedWidth ? { width: normalizedWidth } : null;
|
|
227
|
+
});
|
|
228
|
+
const screenStyle = computed(() => {
|
|
229
|
+
const normalizedHeight = normalizeCssSize(height.value);
|
|
230
|
+
const normalizedAspectRatio = normalizeAspectRatio(aspectRatio.value);
|
|
231
|
+
return {
|
|
232
|
+
height: normalizedHeight || null,
|
|
233
|
+
aspectRatio: normalizedAspectRatio || null
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
const isLive = computed(() => live.value === true);
|
|
133
237
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
134
238
|
const seekProgress = computed(() => {
|
|
135
239
|
if (safeDuration.value <= 0) return 0;
|
|
@@ -141,15 +245,44 @@ const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video")
|
|
|
141
245
|
const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
|
|
142
246
|
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
143
247
|
const fullscreenIconClass = computed(() => isFullscreen.value ? "icon-minimize" : "icon-maximize");
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
248
|
+
const statusText = computed(() => isLive.value ? "LIVE" : subtitle.value);
|
|
249
|
+
const rateOptions = [
|
|
250
|
+
{ value: 0.75, label: "0.75x" },
|
|
251
|
+
{ value: 1, label: "1x" },
|
|
252
|
+
{ value: 1.25, label: "1.25x" },
|
|
253
|
+
{ value: 1.5, label: "1.5x" },
|
|
254
|
+
{ value: 2, label: "2x" }
|
|
255
|
+
];
|
|
256
|
+
const skipOptions = [
|
|
257
|
+
{ value: 5, label: "5s" },
|
|
258
|
+
{ value: 10, label: "10s" },
|
|
259
|
+
{ value: 15, label: "15s" },
|
|
260
|
+
{ value: 30, label: "30s" }
|
|
261
|
+
];
|
|
262
|
+
const showPlayControl = computed(() => hasControl("play"));
|
|
263
|
+
const showSeekControl = computed(() => hasControl("seek"));
|
|
264
|
+
const showTimeControl = computed(() => hasControl("time"));
|
|
265
|
+
const showMuteControl = computed(() => hasControl("mute"));
|
|
266
|
+
const showVolumeControl = computed(() => hasControl("volume"));
|
|
267
|
+
const showSettingsControl = computed(() => hasControl("settings") || hasControl("rate"));
|
|
268
|
+
const showFullscreenControl = computed(() => hasControl("fullscreen"));
|
|
269
|
+
const showControlShelf = computed(() => (
|
|
270
|
+
showPlayControl.value ||
|
|
271
|
+
showSeekControl.value ||
|
|
272
|
+
showTimeControl.value ||
|
|
273
|
+
showMuteControl.value ||
|
|
274
|
+
showVolumeControl.value ||
|
|
275
|
+
showSettingsControl.value ||
|
|
276
|
+
showFullscreenControl.value ||
|
|
277
|
+
isLive.value
|
|
278
|
+
));
|
|
147
279
|
let isDisposed = false;
|
|
148
280
|
let ignoreMediaEventsUntil = 0;
|
|
149
281
|
|
|
150
282
|
onMounted(() => {
|
|
151
283
|
applyAudioSettings();
|
|
152
284
|
document.addEventListener("fullscreenchange", updateFullscreen);
|
|
285
|
+
document.addEventListener("pointerdown", handleDocumentPointerDown);
|
|
153
286
|
});
|
|
154
287
|
|
|
155
288
|
onBeforeUnmount(() => {
|
|
@@ -160,6 +293,7 @@ onBeforeUnmount(() => {
|
|
|
160
293
|
|
|
161
294
|
onUnmounted(() => {
|
|
162
295
|
document.removeEventListener("fullscreenchange", updateFullscreen);
|
|
296
|
+
document.removeEventListener("pointerdown", handleDocumentPointerDown);
|
|
163
297
|
});
|
|
164
298
|
|
|
165
299
|
function getMedia() {
|
|
@@ -175,6 +309,46 @@ function shouldIgnoreMediaEvent() {
|
|
|
175
309
|
return performance.now() < ignoreMediaEventsUntil;
|
|
176
310
|
}
|
|
177
311
|
|
|
312
|
+
function normalizeQualityOption(option, index) {
|
|
313
|
+
if (typeof option === "string") {
|
|
314
|
+
return { id: option || `quality-${index}`, label: option || `Quality ${index + 1}`, src: src.value, poster: poster.value };
|
|
315
|
+
}
|
|
316
|
+
if (!option || typeof option !== "object") {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const optionSrc = typeof option.src === "string" && option.src ? option.src : src.value;
|
|
320
|
+
const label = typeof option.label === "string" && option.label ? option.label : `Quality ${index + 1}`;
|
|
321
|
+
return {
|
|
322
|
+
id: typeof option.id === "string" || typeof option.id === "number" ? String(option.id) : `${label}-${index}`,
|
|
323
|
+
label,
|
|
324
|
+
src: optionSrc,
|
|
325
|
+
poster: typeof option.poster === "string" ? option.poster : poster.value
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizeCssSize(value) {
|
|
330
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
331
|
+
return value >= 0 ? value + "px" : "";
|
|
332
|
+
}
|
|
333
|
+
if (typeof value !== "string") return "";
|
|
334
|
+
const trimmed = value.trim();
|
|
335
|
+
if (!trimmed) return "";
|
|
336
|
+
return /^\d+(\.\d+)?$/.test(trimmed) ? trimmed + "px" : trimmed;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeAspectRatio(value) {
|
|
340
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
341
|
+
return value > 0 ? String(value) : "";
|
|
342
|
+
}
|
|
343
|
+
if (typeof value !== "string") return "";
|
|
344
|
+
return value.trim();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function hasControl(name) {
|
|
348
|
+
if (isLive.value && liveHiddenControls.includes(name)) return false;
|
|
349
|
+
return activeControls.value.has(name);
|
|
350
|
+
}
|
|
351
|
+
|
|
178
352
|
function syncMedia() {
|
|
179
353
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
180
354
|
const media = getMedia();
|
|
@@ -241,7 +415,7 @@ function applyAudioSettings() {
|
|
|
241
415
|
if (!media) return;
|
|
242
416
|
media.volume = volume.value;
|
|
243
417
|
media.muted = muted.value;
|
|
244
|
-
media.playbackRate =
|
|
418
|
+
media.playbackRate = playbackRateValue.value;
|
|
245
419
|
}
|
|
246
420
|
|
|
247
421
|
function markPlaying(event) {
|
|
@@ -357,23 +531,11 @@ function seekWithKeyboard(event) {
|
|
|
357
531
|
return;
|
|
358
532
|
}
|
|
359
533
|
|
|
360
|
-
const offset = event.key === "ArrowRight" ?
|
|
534
|
+
const offset = event.key === "ArrowRight" ? skipSeconds.value : -skipSeconds.value;
|
|
361
535
|
const nextTime = Math.min(Math.max(currentTime.value + offset, 0), safeDuration.value);
|
|
362
536
|
seekTo(nextTime);
|
|
363
537
|
}
|
|
364
538
|
|
|
365
|
-
function stopPlayback() {
|
|
366
|
-
if (isDisposed) return;
|
|
367
|
-
window.setTimeout(() => {
|
|
368
|
-
if (isDisposed) return;
|
|
369
|
-
const media = getMedia();
|
|
370
|
-
if (!media) return;
|
|
371
|
-
ignoreMediaEventsFor(300);
|
|
372
|
-
media.pause();
|
|
373
|
-
media.currentTime = 0;
|
|
374
|
-
}, 0);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
539
|
function setVolume(event) {
|
|
378
540
|
if (isDisposed) return;
|
|
379
541
|
const nextVolume = Number(event.target.value);
|
|
@@ -401,21 +563,68 @@ function toggleMute(event) {
|
|
|
401
563
|
}, 0);
|
|
402
564
|
}
|
|
403
565
|
|
|
404
|
-
function
|
|
566
|
+
function toggleSettings() {
|
|
405
567
|
if (isDisposed) return;
|
|
406
|
-
|
|
568
|
+
settingsOpen.value = !settingsOpen.value;
|
|
569
|
+
controlsVisible.value = true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function selectQuality(id) {
|
|
573
|
+
if (isDisposed) return;
|
|
574
|
+
const media = getMedia();
|
|
575
|
+
const wasPlaying = media ? !media.paused : false;
|
|
576
|
+
const previousTime = media?.currentTime || 0;
|
|
577
|
+
selectedQualityId.value = id;
|
|
578
|
+
settingsOpen.value = false;
|
|
579
|
+
if (!media) return;
|
|
407
580
|
window.setTimeout(() => {
|
|
408
581
|
if (isDisposed) return;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
582
|
+
const nextMedia = getMedia();
|
|
583
|
+
if (!nextMedia) return;
|
|
584
|
+
if (Number.isFinite(previousTime) && previousTime > 0 && safeDuration.value > 0) {
|
|
585
|
+
nextMedia.currentTime = Math.min(previousTime, safeDuration.value);
|
|
586
|
+
currentTime.value = nextMedia.currentTime;
|
|
587
|
+
}
|
|
588
|
+
nextMedia.playbackRate = playbackRateValue.value;
|
|
589
|
+
if (wasPlaying) {
|
|
590
|
+
nextMedia.play().catch((error) => {
|
|
591
|
+
if (error?.name !== "AbortError") {
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
414
595
|
}
|
|
415
|
-
button.textContent = playbackRate + "x";
|
|
416
596
|
}, 0);
|
|
417
597
|
}
|
|
418
598
|
|
|
599
|
+
function setPlaybackRate(nextRate) {
|
|
600
|
+
if (isDisposed) return;
|
|
601
|
+
playbackRateValue.value = nextRate;
|
|
602
|
+
settingsOpen.value = false;
|
|
603
|
+
window.setTimeout(() => {
|
|
604
|
+
if (isDisposed) return;
|
|
605
|
+
const media = getMedia();
|
|
606
|
+
if (!media) return;
|
|
607
|
+
media.playbackRate = nextRate;
|
|
608
|
+
}, 0);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function setSkipSeconds(nextSeconds) {
|
|
612
|
+
if (isDisposed) return;
|
|
613
|
+
skipSeconds.value = nextSeconds;
|
|
614
|
+
settingsOpen.value = false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function handleDocumentPointerDown(event) {
|
|
618
|
+
if (isDisposed || !settingsOpen.value) return;
|
|
619
|
+
const menu = settingsEl.value;
|
|
620
|
+
if (menu?.contains?.(event.target)) return;
|
|
621
|
+
settingsOpen.value = false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function stopEvent(event) {
|
|
625
|
+
event.stopPropagation();
|
|
626
|
+
}
|
|
627
|
+
|
|
419
628
|
function toggleFullscreen() {
|
|
420
629
|
if (isDisposed) return;
|
|
421
630
|
if (document.fullscreenElement) {
|
|
@@ -513,6 +722,20 @@ function formatTime(seconds) {
|
|
|
513
722
|
font-size: 0.88rem;
|
|
514
723
|
}
|
|
515
724
|
|
|
725
|
+
.live-badge {
|
|
726
|
+
color: #ffffff;
|
|
727
|
+
font-size: 0.78rem;
|
|
728
|
+
font-weight: 800;
|
|
729
|
+
letter-spacing: 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.live-badge {
|
|
733
|
+
flex: 0 0 auto;
|
|
734
|
+
padding: 4px 8px;
|
|
735
|
+
border-radius: 999px;
|
|
736
|
+
background: #dc2626;
|
|
737
|
+
}
|
|
738
|
+
|
|
516
739
|
.center-toggle {
|
|
517
740
|
position: absolute;
|
|
518
741
|
top: 50%;
|
|
@@ -662,6 +885,53 @@ function formatTime(seconds) {
|
|
|
662
885
|
font-size: 0.84rem;
|
|
663
886
|
}
|
|
664
887
|
|
|
888
|
+
.settings {
|
|
889
|
+
position: relative;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.settings-menu {
|
|
893
|
+
position: absolute;
|
|
894
|
+
right: 0;
|
|
895
|
+
bottom: calc(100% + 10px);
|
|
896
|
+
z-index: 4;
|
|
897
|
+
display: grid;
|
|
898
|
+
gap: 12px;
|
|
899
|
+
width: min(240px, calc(100vw - 32px));
|
|
900
|
+
padding: 12px;
|
|
901
|
+
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
902
|
+
border-radius: 8px;
|
|
903
|
+
color: #ffffff;
|
|
904
|
+
background: rgba(3, 7, 18, 0.94);
|
|
905
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.36);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.settings-group {
|
|
909
|
+
display: grid;
|
|
910
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
911
|
+
gap: 6px;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.settings-label {
|
|
915
|
+
grid-column: 1 / -1;
|
|
916
|
+
color: rgba(255, 255, 255, 0.72);
|
|
917
|
+
font-size: 0.78rem;
|
|
918
|
+
font-weight: 700;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.settings-option {
|
|
922
|
+
min-height: 30px;
|
|
923
|
+
padding: 0 8px;
|
|
924
|
+
border-radius: 6px;
|
|
925
|
+
font-size: 0.8rem;
|
|
926
|
+
white-space: nowrap;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.settings-option.active {
|
|
930
|
+
border-color: #ef4444;
|
|
931
|
+
color: #ffffff;
|
|
932
|
+
background: #ef4444;
|
|
933
|
+
}
|
|
934
|
+
|
|
665
935
|
button {
|
|
666
936
|
min-height: 36px;
|
|
667
937
|
padding: 0 12px;
|
|
@@ -707,8 +977,7 @@ button:hover {
|
|
|
707
977
|
transform: translateX(3px);
|
|
708
978
|
}
|
|
709
979
|
|
|
710
|
-
.center-toggle .icon-pause
|
|
711
|
-
.center-toggle .icon-stop {
|
|
980
|
+
.center-toggle .icon-pause {
|
|
712
981
|
transform: translateX(0);
|
|
713
982
|
}
|
|
714
983
|
|
|
@@ -720,10 +989,6 @@ button:hover {
|
|
|
720
989
|
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>');
|
|
721
990
|
}
|
|
722
991
|
|
|
723
|
-
.icon-stop {
|
|
724
|
-
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>');
|
|
725
|
-
}
|
|
726
|
-
|
|
727
992
|
.icon-maximize {
|
|
728
993
|
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M264 96L120 96C106.7 96 96 106.7 96 120L96 264C96 273.7 101.8 282.5 110.8 286.2C119.8 289.9 130.1 287.8 137 281L177 241L256 320L177 399L137 359C130.1 352.1 119.8 350.1 110.8 353.8C101.8 357.5 96 366.3 96 376L96 520C96 533.3 106.7 544 120 544L264 544C273.7 544 282.5 538.2 286.2 529.2C289.9 520.2 287.9 509.9 281 503L241 463L320 384L399 463L359 503C352.1 509.9 350.1 520.2 353.8 529.2C357.5 538.2 366.3 544 376 544L520 544C533.3 544 544 533.3 544 520L544 376C544 366.3 538.2 357.5 529.2 353.8C520.2 350.1 509.9 352.1 503 359L463 399L384 320L463 241L503 281C509.9 287.9 520.2 289.9 529.2 286.2C538.2 282.5 544 273.7 544 264L544 120C544 106.7 533.3 96 520 96L376 96C366.3 96 357.5 101.8 353.8 110.8C350.1 119.8 352.2 130.1 359 137L399 177L320 256L241 177L281 137C287.9 130.1 289.9 119.8 286.2 110.8C282.5 101.8 273.7 96 264 96z"/></svg>');
|
|
729
994
|
}
|
|
@@ -740,6 +1005,10 @@ button:hover {
|
|
|
740
1005
|
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M80 416L128 416L262.1 535.2C268.5 540.9 276.7 544 285.2 544C304.4 544 320 528.4 320 509.2L320 130.8C320 111.6 304.4 96 285.2 96C276.7 96 268.5 99.1 262.1 104.8L128 224L80 224C53.5 224 32 245.5 32 272L32 368C32 394.5 53.5 416 80 416zM399 239C389.6 248.4 389.6 263.6 399 272.9L446 319.9L399 366.9C389.6 376.3 389.6 391.5 399 400.8C408.4 410.1 423.6 410.2 432.9 400.8L479.9 353.8L526.9 400.8C536.3 410.2 551.5 410.2 560.8 400.8C570.1 391.4 570.2 376.2 560.8 366.9L513.8 319.9L560.8 272.9C570.2 263.5 570.2 248.3 560.8 239C551.4 229.7 536.2 229.6 526.9 239L479.9 286L432.9 239C423.5 229.6 408.3 229.6 399 239z"/></svg>');
|
|
741
1006
|
}
|
|
742
1007
|
|
|
1008
|
+
.icon-settings {
|
|
1009
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M259.1 73.5C262.1 59 274.9 48 290 48L350 48C365.1 48 377.9 59 380.9 73.5L389.8 116.8C404.9 122.5 419.2 130 432.5 139.1L474.3 125.1C488.6 120.3 504.4 126.1 512 139.1L542 190.9C549.6 203.9 546.8 220.5 535.5 230.5L502.7 259.8C504 267.7 504.7 275.8 504.7 284C504.7 292.2 504 300.3 502.7 308.2L535.5 337.5C546.8 347.5 549.6 364.1 542 377.1L512 428.9C504.4 441.9 488.6 447.7 474.3 442.9L432.5 428.9C419.2 438 404.9 445.5 389.8 451.2L380.9 494.5C377.9 509 365.1 520 350 520L290 520C274.9 520 262.1 509 259.1 494.5L250.2 451.2C235.1 445.5 220.8 438 207.5 428.9L165.7 442.9C151.4 447.7 135.6 441.9 128 428.9L98 377.1C90.4 364.1 93.2 347.5 104.5 337.5L137.3 308.2C136 300.3 135.3 292.2 135.3 284C135.3 275.8 136 267.7 137.3 259.8L104.5 230.5C93.2 220.5 90.4 203.9 98 190.9L128 139.1C135.6 126.1 151.4 120.3 165.7 125.1L207.5 139.1C220.8 130 235.1 122.5 250.2 116.8L259.1 73.5zM320 356C359.8 356 392 323.8 392 284C392 244.2 359.8 212 320 212C280.2 212 248 244.2 248 284C248 323.8 280.2 356 320 356z"/></svg>');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
743
1012
|
@media (max-width: 620px) {
|
|
744
1013
|
.top-bar {
|
|
745
1014
|
align-items: flex-start;
|
package/package.json
CHANGED
|
@@ -23,11 +23,21 @@ export type MikuruAudioPlayerEvents = {
|
|
|
23
23
|
onRatechange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
export type MikuruAudioPlayerControl =
|
|
27
|
+
| "play"
|
|
28
|
+
| "seek"
|
|
29
|
+
| "time"
|
|
30
|
+
| "skip"
|
|
31
|
+
| "mute"
|
|
32
|
+
| "volume";
|
|
33
|
+
|
|
26
34
|
export type MikuruAudioPlayerProps = {
|
|
27
35
|
src: string;
|
|
28
36
|
title?: string;
|
|
29
37
|
artist?: string;
|
|
30
38
|
preload?: string;
|
|
39
|
+
controls?: MikuruAudioPlayerControl[];
|
|
40
|
+
live?: boolean;
|
|
31
41
|
} & MikuruAudioPlayerEvents;
|
|
32
42
|
|
|
33
43
|
declare const component: MikuruComponent<MikuruAudioPlayerProps>;
|
|
@@ -5,11 +5,13 @@ export type MikuruToastItem = {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
message?: string;
|
|
7
7
|
tone?: "info" | "success" | "warning" | "danger" | string;
|
|
8
|
+
duration?: number;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export type MikuruToastProps = {
|
|
11
12
|
toasts?: MikuruToastItem[];
|
|
12
13
|
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left" | string;
|
|
14
|
+
duration?: number;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
declare const component: MikuruComponent<MikuruToastProps>;
|
|
@@ -23,12 +23,35 @@ export type MikuruVideoPlayerEvents = {
|
|
|
23
23
|
onRatechange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
export type MikuruVideoPlayerControl =
|
|
27
|
+
| "play"
|
|
28
|
+
| "seek"
|
|
29
|
+
| "time"
|
|
30
|
+
| "mute"
|
|
31
|
+
| "volume"
|
|
32
|
+
| "rate"
|
|
33
|
+
| "settings"
|
|
34
|
+
| "fullscreen";
|
|
35
|
+
|
|
36
|
+
export type MikuruVideoPlayerQualityOption = {
|
|
37
|
+
id?: string | number;
|
|
38
|
+
label: string;
|
|
39
|
+
src: string;
|
|
40
|
+
poster?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
26
43
|
export type MikuruVideoPlayerProps = {
|
|
27
44
|
src: string;
|
|
28
45
|
poster?: string;
|
|
29
46
|
title?: string;
|
|
30
47
|
subtitle?: string;
|
|
31
48
|
preload?: string;
|
|
49
|
+
width?: string | number;
|
|
50
|
+
height?: string | number;
|
|
51
|
+
aspectRatio?: string | number;
|
|
52
|
+
qualityOptions?: MikuruVideoPlayerQualityOption[];
|
|
53
|
+
controls?: MikuruVideoPlayerControl[];
|
|
54
|
+
live?: boolean;
|
|
32
55
|
} & MikuruVideoPlayerEvents;
|
|
33
56
|
|
|
34
57
|
declare const component: MikuruComponent<MikuruVideoPlayerProps>;
|