mikuru 1.0.31 → 1.0.32
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 +5 -1
- package/README.md +3 -3
- package/components/MikuruAudioPlayer.mikuru +157 -21
- package/components/MikuruToast.mikuru +52 -4
- package/components/MikuruVideoPlayer.mikuru +61 -32
- 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 +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
3
|
+
## 1.0.32 - 2026-05-16
|
|
4
4
|
|
|
5
|
+
- Added `controls` and `live` props to `MikuruVideoPlayer` and `MikuruAudioPlayer` so callers can choose visible controls and render live-stream UI without seek controls.
|
|
6
|
+
- Removed the `MikuruVideoPlayer` stop control so video playback uses the same play/pause-only primary transport as the center control.
|
|
7
|
+
- Updated `MikuruAudioPlayer` controls to use icon buttons for play, skip, mute, and volume-facing actions, matching the video player control style.
|
|
8
|
+
- Added timed auto-dismiss to `MikuruToast` with stack-level and per-toast `duration` controls.
|
|
5
9
|
- Exported `MikuruVideoPlayer` media events so parent components can listen for playback, timing, seeking, volume, and playback-rate changes with typed media state payloads.
|
|
6
10
|
- Exported matching `MikuruAudioPlayer` media events and documented the shared media player event payload.
|
|
7
11
|
|
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 control visibility, live mode, 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>
|
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
|
|
32
32
|
<div class="top-bar">
|
|
33
33
|
<strong>{{ title }}</strong>
|
|
34
|
-
<span>{{
|
|
34
|
+
<span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
|
-
<button class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
37
|
+
<button m-if="showPlayControl" class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
38
38
|
<span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
|
|
39
39
|
</button>
|
|
40
40
|
|
|
41
|
-
<div class="control-shelf">
|
|
41
|
+
<div m-if="showControlShelf" class="control-shelf">
|
|
42
42
|
<div
|
|
43
|
+
m-if="showSeekControl"
|
|
43
44
|
class="seek"
|
|
44
45
|
role="slider"
|
|
45
46
|
tabindex="0"
|
|
@@ -62,25 +63,22 @@
|
|
|
62
63
|
|
|
63
64
|
<div class="control-row">
|
|
64
65
|
<div class="left-controls">
|
|
65
|
-
<button class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
66
|
+
<button m-if="showPlayControl" class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
66
67
|
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
67
68
|
</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">
|
|
69
|
+
<button m-if="showMuteControl" class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
72
70
|
<span class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
73
71
|
</button>
|
|
74
|
-
<label class="volume">
|
|
72
|
+
<label m-if="showVolumeControl" class="volume">
|
|
75
73
|
<span>Volume</span>
|
|
76
74
|
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
77
75
|
</label>
|
|
78
|
-
<span class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
76
|
+
<span m-if="showTimeControl" class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
79
77
|
</div>
|
|
80
78
|
|
|
81
79
|
<div class="right-controls">
|
|
82
|
-
<button class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
|
|
83
|
-
<button class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
|
|
80
|
+
<button m-if="showRateControl" class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
|
|
81
|
+
<button m-if="showFullscreenControl" class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
|
|
84
82
|
<span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
|
|
85
83
|
</button>
|
|
86
84
|
</div>
|
|
@@ -98,13 +96,17 @@ const {
|
|
|
98
96
|
poster = "",
|
|
99
97
|
title = "Mikuru Video",
|
|
100
98
|
subtitle = "Original player component",
|
|
101
|
-
preload = "metadata"
|
|
99
|
+
preload = "metadata",
|
|
100
|
+
controls,
|
|
101
|
+
live = false
|
|
102
102
|
} = defineProps({
|
|
103
103
|
src: String,
|
|
104
104
|
poster: String,
|
|
105
105
|
title: String,
|
|
106
106
|
subtitle: String,
|
|
107
|
-
preload: String
|
|
107
|
+
preload: String,
|
|
108
|
+
controls: Array,
|
|
109
|
+
live: Boolean
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
const emit = defineEmits([
|
|
@@ -130,6 +132,13 @@ const isFullscreen = ref(false);
|
|
|
130
132
|
const isSeeking = ref(false);
|
|
131
133
|
const pointerInside = ref(false);
|
|
132
134
|
const controlsVisible = ref(true);
|
|
135
|
+
const allControls = ["play", "seek", "time", "mute", "volume", "rate", "fullscreen"];
|
|
136
|
+
const liveHiddenControls = ["seek", "time", "rate"];
|
|
137
|
+
const activeControls = computed(() => {
|
|
138
|
+
const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
|
|
139
|
+
return new Set(configuredControls);
|
|
140
|
+
});
|
|
141
|
+
const isLive = computed(() => live.value === true);
|
|
133
142
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
134
143
|
const seekProgress = computed(() => {
|
|
135
144
|
if (safeDuration.value <= 0) return 0;
|
|
@@ -141,6 +150,24 @@ const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video")
|
|
|
141
150
|
const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
|
|
142
151
|
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
143
152
|
const fullscreenIconClass = computed(() => isFullscreen.value ? "icon-minimize" : "icon-maximize");
|
|
153
|
+
const statusText = computed(() => isLive.value ? "LIVE" : subtitle.value);
|
|
154
|
+
const showPlayControl = computed(() => hasControl("play"));
|
|
155
|
+
const showSeekControl = computed(() => hasControl("seek"));
|
|
156
|
+
const showTimeControl = computed(() => hasControl("time"));
|
|
157
|
+
const showMuteControl = computed(() => hasControl("mute"));
|
|
158
|
+
const showVolumeControl = computed(() => hasControl("volume"));
|
|
159
|
+
const showRateControl = computed(() => hasControl("rate"));
|
|
160
|
+
const showFullscreenControl = computed(() => hasControl("fullscreen"));
|
|
161
|
+
const showControlShelf = computed(() => (
|
|
162
|
+
showPlayControl.value ||
|
|
163
|
+
showSeekControl.value ||
|
|
164
|
+
showTimeControl.value ||
|
|
165
|
+
showMuteControl.value ||
|
|
166
|
+
showVolumeControl.value ||
|
|
167
|
+
showRateControl.value ||
|
|
168
|
+
showFullscreenControl.value ||
|
|
169
|
+
isLive.value
|
|
170
|
+
));
|
|
144
171
|
const rates = [1, 1.25, 1.5, 2, 0.75];
|
|
145
172
|
let playbackRate = 1;
|
|
146
173
|
let rateIndex = 0;
|
|
@@ -175,6 +202,11 @@ function shouldIgnoreMediaEvent() {
|
|
|
175
202
|
return performance.now() < ignoreMediaEventsUntil;
|
|
176
203
|
}
|
|
177
204
|
|
|
205
|
+
function hasControl(name) {
|
|
206
|
+
if (isLive.value && liveHiddenControls.includes(name)) return false;
|
|
207
|
+
return activeControls.value.has(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
178
210
|
function syncMedia() {
|
|
179
211
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
180
212
|
const media = getMedia();
|
|
@@ -362,18 +394,6 @@ function seekWithKeyboard(event) {
|
|
|
362
394
|
seekTo(nextTime);
|
|
363
395
|
}
|
|
364
396
|
|
|
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
397
|
function setVolume(event) {
|
|
378
398
|
if (isDisposed) return;
|
|
379
399
|
const nextVolume = Number(event.target.value);
|
|
@@ -513,6 +533,20 @@ function formatTime(seconds) {
|
|
|
513
533
|
font-size: 0.88rem;
|
|
514
534
|
}
|
|
515
535
|
|
|
536
|
+
.live-badge {
|
|
537
|
+
color: #ffffff;
|
|
538
|
+
font-size: 0.78rem;
|
|
539
|
+
font-weight: 800;
|
|
540
|
+
letter-spacing: 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.live-badge {
|
|
544
|
+
flex: 0 0 auto;
|
|
545
|
+
padding: 4px 8px;
|
|
546
|
+
border-radius: 999px;
|
|
547
|
+
background: #dc2626;
|
|
548
|
+
}
|
|
549
|
+
|
|
516
550
|
.center-toggle {
|
|
517
551
|
position: absolute;
|
|
518
552
|
top: 50%;
|
|
@@ -707,8 +741,7 @@ button:hover {
|
|
|
707
741
|
transform: translateX(3px);
|
|
708
742
|
}
|
|
709
743
|
|
|
710
|
-
.center-toggle .icon-pause
|
|
711
|
-
.center-toggle .icon-stop {
|
|
744
|
+
.center-toggle .icon-pause {
|
|
712
745
|
transform: translateX(0);
|
|
713
746
|
}
|
|
714
747
|
|
|
@@ -720,10 +753,6 @@ button:hover {
|
|
|
720
753
|
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
754
|
}
|
|
722
755
|
|
|
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
756
|
.icon-maximize {
|
|
728
757
|
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
758
|
}
|
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,23 @@ 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
|
+
| "fullscreen";
|
|
34
|
+
|
|
26
35
|
export type MikuruVideoPlayerProps = {
|
|
27
36
|
src: string;
|
|
28
37
|
poster?: string;
|
|
29
38
|
title?: string;
|
|
30
39
|
subtitle?: string;
|
|
31
40
|
preload?: string;
|
|
41
|
+
controls?: MikuruVideoPlayerControl[];
|
|
42
|
+
live?: boolean;
|
|
32
43
|
} & MikuruVideoPlayerEvents;
|
|
33
44
|
|
|
34
45
|
declare const component: MikuruComponent<MikuruVideoPlayerProps>;
|