mikuru 1.0.30 → 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 +9 -0
- package/README.md +3 -3
- package/components/MikuruAudioPlayer.mikuru +238 -27
- package/components/MikuruToast.mikuru +52 -4
- package/components/MikuruVideoPlayer.mikuru +143 -38
- package/package.json +1 -1
- package/types/components/MikuruAudioPlayer.d.ts +34 -1
- package/types/components/MikuruToast.d.ts +2 -0
- package/types/components/MikuruVideoPlayer.d.ts +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.32 - 2026-05-16
|
|
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.
|
|
9
|
+
- Exported `MikuruVideoPlayer` media events so parent components can listen for playback, timing, seeking, volume, and playback-rate changes with typed media state payloads.
|
|
10
|
+
- Exported matching `MikuruAudioPlayer` media events and documented the shared media player event payload.
|
|
11
|
+
|
|
3
12
|
## 1.0.30 - 2026-05-15
|
|
4
13
|
|
|
5
14
|
- Hardened `MikuruVideoPlayer` controls so stop, mute, playback speed, seeking, and modal close operations do not trigger recursive updates while browser media events are firing.
|
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,15 +1,18 @@
|
|
|
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"
|
|
6
6
|
:preload="preload"
|
|
7
|
-
@loadedmetadata="
|
|
8
|
-
@timeupdate="
|
|
9
|
-
@durationchange="
|
|
7
|
+
@loadedmetadata="handleLoadedMetadata"
|
|
8
|
+
@timeupdate="handleTimeUpdate"
|
|
9
|
+
@durationchange="handleDurationChange"
|
|
10
10
|
@play="markPlaying"
|
|
11
11
|
@pause="markPaused"
|
|
12
|
-
@ended="
|
|
12
|
+
@ended="markEnded"
|
|
13
|
+
@seeked="handleSeeked"
|
|
14
|
+
@volumechange="handleVolumeChange"
|
|
15
|
+
@ratechange="handleRateChange"
|
|
13
16
|
></audio>
|
|
14
17
|
|
|
15
18
|
<div class="art">
|
|
@@ -19,28 +22,33 @@
|
|
|
19
22
|
<div class="body">
|
|
20
23
|
<div class="identity">
|
|
21
24
|
<strong>{{ title }}</strong>
|
|
22
|
-
<span>{{
|
|
25
|
+
<span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
|
|
23
26
|
</div>
|
|
24
27
|
|
|
25
|
-
<div class="timeline">
|
|
26
|
-
<span>{{ formatTime(currentTime) }}</span>
|
|
27
|
-
<input type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
|
|
28
|
-
<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>
|
|
29
32
|
</div>
|
|
30
33
|
|
|
31
|
-
<div class="controls">
|
|
32
|
-
<button type="button" @click="skipBackward"
|
|
33
|
-
|
|
34
|
-
<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>
|
|
35
37
|
</button>
|
|
36
|
-
<button type="button" @click="
|
|
37
|
-
|
|
38
|
-
<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>
|
|
39
40
|
</button>
|
|
40
|
-
<
|
|
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">
|
|
41
48
|
<span>Volume</span>
|
|
42
49
|
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
43
50
|
</label>
|
|
51
|
+
<span m-if="isLive" class="live-clock">LIVE</span>
|
|
44
52
|
</div>
|
|
45
53
|
</div>
|
|
46
54
|
</section>
|
|
@@ -53,24 +61,67 @@ const {
|
|
|
53
61
|
src,
|
|
54
62
|
title = "Mikuru Audio",
|
|
55
63
|
artist = "Original player component",
|
|
56
|
-
preload = "metadata"
|
|
64
|
+
preload = "metadata",
|
|
65
|
+
controls,
|
|
66
|
+
live = false
|
|
57
67
|
} = defineProps({
|
|
58
68
|
src: String,
|
|
59
69
|
title: String,
|
|
60
70
|
artist: String,
|
|
61
|
-
preload: String
|
|
71
|
+
preload: String,
|
|
72
|
+
controls: Array,
|
|
73
|
+
live: Boolean
|
|
62
74
|
});
|
|
63
75
|
|
|
76
|
+
const emit = defineEmits([
|
|
77
|
+
"loadedmetadata",
|
|
78
|
+
"timeupdate",
|
|
79
|
+
"durationchange",
|
|
80
|
+
"play",
|
|
81
|
+
"pause",
|
|
82
|
+
"ended",
|
|
83
|
+
"seeked",
|
|
84
|
+
"volumechange",
|
|
85
|
+
"ratechange"
|
|
86
|
+
]);
|
|
87
|
+
|
|
64
88
|
const mediaEl = ref(null);
|
|
65
89
|
const currentTime = ref(0);
|
|
66
90
|
const duration = ref(0);
|
|
67
91
|
const volume = ref(0.75);
|
|
68
92
|
const muted = ref(false);
|
|
69
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);
|
|
70
101
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
71
102
|
const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
|
|
72
103
|
const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
|
|
73
|
-
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
|
+
));
|
|
74
125
|
const initials = computed(() => {
|
|
75
126
|
const words = title.value.split(" ").filter(Boolean);
|
|
76
127
|
return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
|
|
@@ -99,6 +150,11 @@ function shouldIgnoreMediaEvent() {
|
|
|
99
150
|
return performance.now() < ignoreMediaEventsUntil;
|
|
100
151
|
}
|
|
101
152
|
|
|
153
|
+
function hasControl(name) {
|
|
154
|
+
if (isLive.value && liveHiddenControls.includes(name)) return false;
|
|
155
|
+
return activeControls.value.has(name);
|
|
156
|
+
}
|
|
157
|
+
|
|
102
158
|
function syncMedia() {
|
|
103
159
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
104
160
|
const media = getMedia();
|
|
@@ -107,6 +163,67 @@ function syncMedia() {
|
|
|
107
163
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
108
164
|
}
|
|
109
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
|
+
|
|
174
|
+
function createMediaPayload(event) {
|
|
175
|
+
const media = getMedia();
|
|
176
|
+
if (!media) return null;
|
|
177
|
+
return {
|
|
178
|
+
currentTime: media.currentTime || 0,
|
|
179
|
+
duration: Number.isFinite(media.duration) ? media.duration : 0,
|
|
180
|
+
paused: media.paused,
|
|
181
|
+
ended: media.ended,
|
|
182
|
+
muted: media.muted,
|
|
183
|
+
volume: media.volume,
|
|
184
|
+
playbackRate: media.playbackRate,
|
|
185
|
+
nativeEvent: event
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function emitMediaPayload(event, dispatch) {
|
|
190
|
+
const payload = createMediaPayload(event);
|
|
191
|
+
if (payload) {
|
|
192
|
+
dispatch(payload);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleLoadedMetadata(event) {
|
|
197
|
+
syncMedia();
|
|
198
|
+
emitMediaPayload(event, (payload) => emit("loadedmetadata", payload));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function handleTimeUpdate(event) {
|
|
202
|
+
syncMedia();
|
|
203
|
+
emitMediaPayload(event, (payload) => emit("timeupdate", payload));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleDurationChange(event) {
|
|
207
|
+
syncMedia();
|
|
208
|
+
emitMediaPayload(event, (payload) => emit("durationchange", payload));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleSeeked(event) {
|
|
212
|
+
syncMedia();
|
|
213
|
+
emitMediaPayload(event, (payload) => emit("seeked", payload));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleVolumeChange(event) {
|
|
217
|
+
syncMedia();
|
|
218
|
+
syncVolumeState();
|
|
219
|
+
emitMediaPayload(event, (payload) => emit("volumechange", payload));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function handleRateChange(event) {
|
|
223
|
+
syncMedia();
|
|
224
|
+
emitMediaPayload(event, (payload) => emit("ratechange", payload));
|
|
225
|
+
}
|
|
226
|
+
|
|
110
227
|
function applyAudioSettings() {
|
|
111
228
|
if (isDisposed) return;
|
|
112
229
|
const media = getMedia();
|
|
@@ -115,14 +232,22 @@ function applyAudioSettings() {
|
|
|
115
232
|
media.muted = muted.value;
|
|
116
233
|
}
|
|
117
234
|
|
|
118
|
-
function markPlaying() {
|
|
235
|
+
function markPlaying(event) {
|
|
119
236
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
120
237
|
isPlaying.value = true;
|
|
238
|
+
emitMediaPayload(event, (payload) => emit("play", payload));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function markPaused(event) {
|
|
242
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
243
|
+
isPlaying.value = false;
|
|
244
|
+
emitMediaPayload(event, (payload) => emit("pause", payload));
|
|
121
245
|
}
|
|
122
246
|
|
|
123
|
-
function
|
|
247
|
+
function markEnded(event) {
|
|
124
248
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
125
249
|
isPlaying.value = false;
|
|
250
|
+
emitMediaPayload(event, (payload) => emit("ended", payload));
|
|
126
251
|
}
|
|
127
252
|
|
|
128
253
|
async function togglePlayback() {
|
|
@@ -160,19 +285,19 @@ function setVolume(event) {
|
|
|
160
285
|
if (!media) return;
|
|
161
286
|
media.volume = nextVolume;
|
|
162
287
|
media.muted = nextVolume === 0;
|
|
288
|
+
volume.value = nextVolume;
|
|
289
|
+
muted.value = media.muted;
|
|
163
290
|
}, 0);
|
|
164
291
|
}
|
|
165
292
|
|
|
166
|
-
function toggleMute(
|
|
293
|
+
function toggleMute() {
|
|
167
294
|
if (isDisposed) return;
|
|
168
|
-
const button = event.currentTarget;
|
|
169
295
|
window.setTimeout(() => {
|
|
170
296
|
if (isDisposed) return;
|
|
171
297
|
const media = getMedia();
|
|
172
298
|
if (!media) return;
|
|
173
299
|
media.muted = !media.muted;
|
|
174
|
-
|
|
175
|
-
button.setAttribute("aria-label", media.muted ? "Unmute audio" : "Mute audio");
|
|
300
|
+
muted.value = media.muted;
|
|
176
301
|
}, 0);
|
|
177
302
|
}
|
|
178
303
|
|
|
@@ -243,6 +368,32 @@ function formatTime(seconds) {
|
|
|
243
368
|
font-size: 0.9rem;
|
|
244
369
|
}
|
|
245
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
|
+
|
|
246
397
|
.timeline {
|
|
247
398
|
display: grid;
|
|
248
399
|
grid-template-columns: 42px minmax(0, 1fr) 42px;
|
|
@@ -250,6 +401,15 @@ function formatTime(seconds) {
|
|
|
250
401
|
align-items: center;
|
|
251
402
|
}
|
|
252
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
|
+
|
|
253
413
|
.timeline span:last-child {
|
|
254
414
|
text-align: right;
|
|
255
415
|
}
|
|
@@ -267,6 +427,14 @@ function formatTime(seconds) {
|
|
|
267
427
|
gap: 8px;
|
|
268
428
|
}
|
|
269
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
|
+
|
|
270
438
|
input[type="range"] {
|
|
271
439
|
accent-color: #0f766e;
|
|
272
440
|
}
|
|
@@ -292,6 +460,49 @@ button:hover {
|
|
|
292
460
|
background: #0f766e;
|
|
293
461
|
}
|
|
294
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
|
+
|
|
295
506
|
@media (max-width: 620px) {
|
|
296
507
|
.mikuru-audio {
|
|
297
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>
|
|
@@ -17,26 +17,30 @@
|
|
|
17
17
|
:poster="poster"
|
|
18
18
|
:preload="preload"
|
|
19
19
|
playsinline
|
|
20
|
-
@loadedmetadata="
|
|
21
|
-
@timeupdate="
|
|
22
|
-
@durationchange="
|
|
20
|
+
@loadedmetadata="handleLoadedMetadata"
|
|
21
|
+
@timeupdate="handleTimeUpdate"
|
|
22
|
+
@durationchange="handleDurationChange"
|
|
23
23
|
@play="markPlaying"
|
|
24
24
|
@pause="markPaused"
|
|
25
|
-
@ended="
|
|
25
|
+
@ended="markEnded"
|
|
26
|
+
@seeked="handleSeeked"
|
|
27
|
+
@volumechange="handleVolumeChange"
|
|
28
|
+
@ratechange="handleRateChange"
|
|
26
29
|
@click="togglePlayback"
|
|
27
30
|
></video>
|
|
28
31
|
|
|
29
32
|
<div class="top-bar">
|
|
30
33
|
<strong>{{ title }}</strong>
|
|
31
|
-
<span>{{
|
|
34
|
+
<span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
|
|
32
35
|
</div>
|
|
33
36
|
|
|
34
|
-
<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">
|
|
35
38
|
<span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
|
|
36
39
|
</button>
|
|
37
40
|
|
|
38
|
-
<div class="control-shelf">
|
|
41
|
+
<div m-if="showControlShelf" class="control-shelf">
|
|
39
42
|
<div
|
|
43
|
+
m-if="showSeekControl"
|
|
40
44
|
class="seek"
|
|
41
45
|
role="slider"
|
|
42
46
|
tabindex="0"
|
|
@@ -59,25 +63,22 @@
|
|
|
59
63
|
|
|
60
64
|
<div class="control-row">
|
|
61
65
|
<div class="left-controls">
|
|
62
|
-
<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">
|
|
63
67
|
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
64
68
|
</button>
|
|
65
|
-
<button class="icon-button" type="button" @click="
|
|
66
|
-
<span class="fa-icon icon-stop" aria-hidden="true"></span>
|
|
67
|
-
</button>
|
|
68
|
-
<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">
|
|
69
70
|
<span class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
70
71
|
</button>
|
|
71
|
-
<label class="volume">
|
|
72
|
+
<label m-if="showVolumeControl" class="volume">
|
|
72
73
|
<span>Volume</span>
|
|
73
74
|
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
74
75
|
</label>
|
|
75
|
-
<span class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
76
|
+
<span m-if="showTimeControl" class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
76
77
|
</div>
|
|
77
78
|
|
|
78
79
|
<div class="right-controls">
|
|
79
|
-
<button class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
|
|
80
|
-
<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">
|
|
81
82
|
<span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
|
|
82
83
|
</button>
|
|
83
84
|
</div>
|
|
@@ -95,15 +96,31 @@ const {
|
|
|
95
96
|
poster = "",
|
|
96
97
|
title = "Mikuru Video",
|
|
97
98
|
subtitle = "Original player component",
|
|
98
|
-
preload = "metadata"
|
|
99
|
+
preload = "metadata",
|
|
100
|
+
controls,
|
|
101
|
+
live = false
|
|
99
102
|
} = defineProps({
|
|
100
103
|
src: String,
|
|
101
104
|
poster: String,
|
|
102
105
|
title: String,
|
|
103
106
|
subtitle: String,
|
|
104
|
-
preload: String
|
|
107
|
+
preload: String,
|
|
108
|
+
controls: Array,
|
|
109
|
+
live: Boolean
|
|
105
110
|
});
|
|
106
111
|
|
|
112
|
+
const emit = defineEmits([
|
|
113
|
+
"loadedmetadata",
|
|
114
|
+
"timeupdate",
|
|
115
|
+
"durationchange",
|
|
116
|
+
"play",
|
|
117
|
+
"pause",
|
|
118
|
+
"ended",
|
|
119
|
+
"seeked",
|
|
120
|
+
"volumechange",
|
|
121
|
+
"ratechange"
|
|
122
|
+
]);
|
|
123
|
+
|
|
107
124
|
const mediaEl = ref(null);
|
|
108
125
|
const screenEl = ref(null);
|
|
109
126
|
const currentTime = ref(0);
|
|
@@ -115,6 +132,13 @@ const isFullscreen = ref(false);
|
|
|
115
132
|
const isSeeking = ref(false);
|
|
116
133
|
const pointerInside = ref(false);
|
|
117
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);
|
|
118
142
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
119
143
|
const seekProgress = computed(() => {
|
|
120
144
|
if (safeDuration.value <= 0) return 0;
|
|
@@ -126,6 +150,24 @@ const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video")
|
|
|
126
150
|
const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
|
|
127
151
|
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
128
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
|
+
));
|
|
129
171
|
const rates = [1, 1.25, 1.5, 2, 0.75];
|
|
130
172
|
let playbackRate = 1;
|
|
131
173
|
let rateIndex = 0;
|
|
@@ -160,6 +202,11 @@ function shouldIgnoreMediaEvent() {
|
|
|
160
202
|
return performance.now() < ignoreMediaEventsUntil;
|
|
161
203
|
}
|
|
162
204
|
|
|
205
|
+
function hasControl(name) {
|
|
206
|
+
if (isLive.value && liveHiddenControls.includes(name)) return false;
|
|
207
|
+
return activeControls.value.has(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
163
210
|
function syncMedia() {
|
|
164
211
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
165
212
|
const media = getMedia();
|
|
@@ -168,6 +215,58 @@ function syncMedia() {
|
|
|
168
215
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
169
216
|
}
|
|
170
217
|
|
|
218
|
+
function createMediaPayload(event) {
|
|
219
|
+
const media = getMedia();
|
|
220
|
+
if (!media) return null;
|
|
221
|
+
return {
|
|
222
|
+
currentTime: media.currentTime || 0,
|
|
223
|
+
duration: Number.isFinite(media.duration) ? media.duration : 0,
|
|
224
|
+
paused: media.paused,
|
|
225
|
+
ended: media.ended,
|
|
226
|
+
muted: media.muted,
|
|
227
|
+
volume: media.volume,
|
|
228
|
+
playbackRate: media.playbackRate,
|
|
229
|
+
nativeEvent: event
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emitMediaPayload(event, dispatch) {
|
|
234
|
+
const payload = createMediaPayload(event);
|
|
235
|
+
if (payload) {
|
|
236
|
+
dispatch(payload);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function handleLoadedMetadata(event) {
|
|
241
|
+
syncMedia();
|
|
242
|
+
emitMediaPayload(event, (payload) => emit("loadedmetadata", payload));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleTimeUpdate(event) {
|
|
246
|
+
syncMedia();
|
|
247
|
+
emitMediaPayload(event, (payload) => emit("timeupdate", payload));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function handleDurationChange(event) {
|
|
251
|
+
syncMedia();
|
|
252
|
+
emitMediaPayload(event, (payload) => emit("durationchange", payload));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleSeeked(event) {
|
|
256
|
+
syncMedia();
|
|
257
|
+
emitMediaPayload(event, (payload) => emit("seeked", payload));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handleVolumeChange(event) {
|
|
261
|
+
syncMedia();
|
|
262
|
+
emitMediaPayload(event, (payload) => emit("volumechange", payload));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function handleRateChange(event) {
|
|
266
|
+
syncMedia();
|
|
267
|
+
emitMediaPayload(event, (payload) => emit("ratechange", payload));
|
|
268
|
+
}
|
|
269
|
+
|
|
171
270
|
function applyAudioSettings() {
|
|
172
271
|
if (isDisposed) return;
|
|
173
272
|
const media = getMedia();
|
|
@@ -177,16 +276,25 @@ function applyAudioSettings() {
|
|
|
177
276
|
media.playbackRate = playbackRate;
|
|
178
277
|
}
|
|
179
278
|
|
|
180
|
-
function markPlaying() {
|
|
279
|
+
function markPlaying(event) {
|
|
181
280
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
182
281
|
isPlaying.value = true;
|
|
183
282
|
controlsVisible.value = pointerInside.value;
|
|
283
|
+
emitMediaPayload(event, (payload) => emit("play", payload));
|
|
184
284
|
}
|
|
185
285
|
|
|
186
|
-
function markPaused() {
|
|
286
|
+
function markPaused(event) {
|
|
187
287
|
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
188
288
|
isPlaying.value = false;
|
|
189
289
|
controlsVisible.value = true;
|
|
290
|
+
emitMediaPayload(event, (payload) => emit("pause", payload));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function markEnded(event) {
|
|
294
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
295
|
+
isPlaying.value = false;
|
|
296
|
+
controlsVisible.value = true;
|
|
297
|
+
emitMediaPayload(event, (payload) => emit("ended", payload));
|
|
190
298
|
}
|
|
191
299
|
|
|
192
300
|
function updateFullscreen() {
|
|
@@ -286,18 +394,6 @@ function seekWithKeyboard(event) {
|
|
|
286
394
|
seekTo(nextTime);
|
|
287
395
|
}
|
|
288
396
|
|
|
289
|
-
function stopPlayback() {
|
|
290
|
-
if (isDisposed) return;
|
|
291
|
-
window.setTimeout(() => {
|
|
292
|
-
if (isDisposed) return;
|
|
293
|
-
const media = getMedia();
|
|
294
|
-
if (!media) return;
|
|
295
|
-
ignoreMediaEventsFor(300);
|
|
296
|
-
media.pause();
|
|
297
|
-
media.currentTime = 0;
|
|
298
|
-
}, 0);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
397
|
function setVolume(event) {
|
|
302
398
|
if (isDisposed) return;
|
|
303
399
|
const nextVolume = Number(event.target.value);
|
|
@@ -437,6 +533,20 @@ function formatTime(seconds) {
|
|
|
437
533
|
font-size: 0.88rem;
|
|
438
534
|
}
|
|
439
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
|
+
|
|
440
550
|
.center-toggle {
|
|
441
551
|
position: absolute;
|
|
442
552
|
top: 50%;
|
|
@@ -631,8 +741,7 @@ button:hover {
|
|
|
631
741
|
transform: translateX(3px);
|
|
632
742
|
}
|
|
633
743
|
|
|
634
|
-
.center-toggle .icon-pause
|
|
635
|
-
.center-toggle .icon-stop {
|
|
744
|
+
.center-toggle .icon-pause {
|
|
636
745
|
transform: translateX(0);
|
|
637
746
|
}
|
|
638
747
|
|
|
@@ -644,10 +753,6 @@ button:hover {
|
|
|
644
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>');
|
|
645
754
|
}
|
|
646
755
|
|
|
647
|
-
.icon-stop {
|
|
648
|
-
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>');
|
|
649
|
-
}
|
|
650
|
-
|
|
651
756
|
.icon-maximize {
|
|
652
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>');
|
|
653
758
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import type { MikuruComponent } from "../env";
|
|
2
2
|
|
|
3
|
+
export type MikuruAudioPlayerEventPayload = {
|
|
4
|
+
currentTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
ended: boolean;
|
|
8
|
+
muted: boolean;
|
|
9
|
+
volume: number;
|
|
10
|
+
playbackRate: number;
|
|
11
|
+
nativeEvent?: Event;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MikuruAudioPlayerEvents = {
|
|
15
|
+
onLoadedmetadata?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
16
|
+
onTimeupdate?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
17
|
+
onDurationchange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
18
|
+
onPlay?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
19
|
+
onPause?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
20
|
+
onEnded?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
21
|
+
onSeeked?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
22
|
+
onVolumechange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
23
|
+
onRatechange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MikuruAudioPlayerControl =
|
|
27
|
+
| "play"
|
|
28
|
+
| "seek"
|
|
29
|
+
| "time"
|
|
30
|
+
| "skip"
|
|
31
|
+
| "mute"
|
|
32
|
+
| "volume";
|
|
33
|
+
|
|
3
34
|
export type MikuruAudioPlayerProps = {
|
|
4
35
|
src: string;
|
|
5
36
|
title?: string;
|
|
6
37
|
artist?: string;
|
|
7
38
|
preload?: string;
|
|
8
|
-
|
|
39
|
+
controls?: MikuruAudioPlayerControl[];
|
|
40
|
+
live?: boolean;
|
|
41
|
+
} & MikuruAudioPlayerEvents;
|
|
9
42
|
|
|
10
43
|
declare const component: MikuruComponent<MikuruAudioPlayerProps>;
|
|
11
44
|
export default component;
|
|
@@ -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>;
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
import type { MikuruComponent } from "../env";
|
|
2
2
|
|
|
3
|
+
export type MikuruVideoPlayerEventPayload = {
|
|
4
|
+
currentTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
ended: boolean;
|
|
8
|
+
muted: boolean;
|
|
9
|
+
volume: number;
|
|
10
|
+
playbackRate: number;
|
|
11
|
+
nativeEvent?: Event;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MikuruVideoPlayerEvents = {
|
|
15
|
+
onLoadedmetadata?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
16
|
+
onTimeupdate?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
17
|
+
onDurationchange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
18
|
+
onPlay?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
19
|
+
onPause?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
20
|
+
onEnded?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
21
|
+
onSeeked?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
22
|
+
onVolumechange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
23
|
+
onRatechange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MikuruVideoPlayerControl =
|
|
27
|
+
| "play"
|
|
28
|
+
| "seek"
|
|
29
|
+
| "time"
|
|
30
|
+
| "mute"
|
|
31
|
+
| "volume"
|
|
32
|
+
| "rate"
|
|
33
|
+
| "fullscreen";
|
|
34
|
+
|
|
3
35
|
export type MikuruVideoPlayerProps = {
|
|
4
36
|
src: string;
|
|
5
37
|
poster?: string;
|
|
6
38
|
title?: string;
|
|
7
39
|
subtitle?: string;
|
|
8
40
|
preload?: string;
|
|
9
|
-
|
|
41
|
+
controls?: MikuruVideoPlayerControl[];
|
|
42
|
+
live?: boolean;
|
|
43
|
+
} & MikuruVideoPlayerEvents;
|
|
10
44
|
|
|
11
45
|
declare const component: MikuruComponent<MikuruVideoPlayerProps>;
|
|
12
46
|
export default component;
|