mikuru 1.0.29 → 1.0.30
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
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.30 - 2026-05-15
|
|
4
|
+
|
|
5
|
+
- Hardened `MikuruVideoPlayer` controls so stop, mute, playback speed, seeking, and modal close operations do not trigger recursive updates while browser media events are firing.
|
|
6
|
+
- Kept media button UI stable by avoiding reactive branch swaps for player control icons and deferring non-play media operations out of the click event stack.
|
|
7
|
+
|
|
3
8
|
## 1.0.29 - 2026-05-15
|
|
4
9
|
|
|
5
10
|
- Fixed `MikuruVideoPlayer` and `MikuruAudioPlayer` unmount handling so media events fired while a modal or conditional branch is being removed no longer update component refs recursively.
|
|
@@ -31,13 +31,11 @@
|
|
|
31
31
|
<div class="controls">
|
|
32
32
|
<button type="button" @click="skipBackward">-10</button>
|
|
33
33
|
<button class="primary" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
34
|
-
<span
|
|
35
|
-
<span m-else>Play</span>
|
|
34
|
+
<span>{{ playText }}</span>
|
|
36
35
|
</button>
|
|
37
36
|
<button type="button" @click="skipForward">+10</button>
|
|
38
37
|
<button type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
39
|
-
<span
|
|
40
|
-
<span m-else>Sound</span>
|
|
38
|
+
<span>Sound</span>
|
|
41
39
|
</button>
|
|
42
40
|
<label class="volume">
|
|
43
41
|
<span>Volume</span>
|
|
@@ -72,11 +70,13 @@ const isPlaying = ref(false);
|
|
|
72
70
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
73
71
|
const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
|
|
74
72
|
const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
|
|
73
|
+
const playText = computed(() => isPlaying.value ? "Pause" : "Play");
|
|
75
74
|
const initials = computed(() => {
|
|
76
75
|
const words = title.value.split(" ").filter(Boolean);
|
|
77
76
|
return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
|
|
78
77
|
});
|
|
79
78
|
let isDisposed = false;
|
|
79
|
+
let ignoreMediaEventsUntil = 0;
|
|
80
80
|
|
|
81
81
|
onMounted(() => {
|
|
82
82
|
applyAudioSettings();
|
|
@@ -91,14 +91,20 @@ function getMedia() {
|
|
|
91
91
|
return mediaEl.value;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function ignoreMediaEventsFor(ms) {
|
|
95
|
+
ignoreMediaEventsUntil = performance.now() + ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldIgnoreMediaEvent() {
|
|
99
|
+
return performance.now() < ignoreMediaEventsUntil;
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
function syncMedia() {
|
|
95
|
-
if (isDisposed) return;
|
|
103
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
96
104
|
const media = getMedia();
|
|
97
105
|
if (!media) return;
|
|
98
106
|
currentTime.value = media.currentTime || 0;
|
|
99
107
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
100
|
-
volume.value = media.volume;
|
|
101
|
-
muted.value = media.muted;
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
function applyAudioSettings() {
|
|
@@ -110,12 +116,12 @@ function applyAudioSettings() {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
function markPlaying() {
|
|
113
|
-
if (isDisposed) return;
|
|
119
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
114
120
|
isPlaying.value = true;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
function markPaused() {
|
|
118
|
-
if (isDisposed) return;
|
|
124
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
119
125
|
isPlaying.value = false;
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -126,7 +132,7 @@ async function togglePlayback() {
|
|
|
126
132
|
try {
|
|
127
133
|
await media.play();
|
|
128
134
|
} catch (error) {
|
|
129
|
-
if (
|
|
135
|
+
if (error?.name !== "AbortError") {
|
|
130
136
|
throw error;
|
|
131
137
|
}
|
|
132
138
|
}
|
|
@@ -148,15 +154,26 @@ function seek(event) {
|
|
|
148
154
|
function setVolume(event) {
|
|
149
155
|
if (isDisposed) return;
|
|
150
156
|
const nextVolume = Number(event.target.value);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
window.setTimeout(() => {
|
|
158
|
+
if (isDisposed) return;
|
|
159
|
+
const media = getMedia();
|
|
160
|
+
if (!media) return;
|
|
161
|
+
media.volume = nextVolume;
|
|
162
|
+
media.muted = nextVolume === 0;
|
|
163
|
+
}, 0);
|
|
154
164
|
}
|
|
155
165
|
|
|
156
|
-
function toggleMute() {
|
|
166
|
+
function toggleMute(event) {
|
|
157
167
|
if (isDisposed) return;
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
const button = event.currentTarget;
|
|
169
|
+
window.setTimeout(() => {
|
|
170
|
+
if (isDisposed) return;
|
|
171
|
+
const media = getMedia();
|
|
172
|
+
if (!media) return;
|
|
173
|
+
media.muted = !media.muted;
|
|
174
|
+
button.textContent = media.muted ? "Muted" : "Sound";
|
|
175
|
+
button.setAttribute("aria-label", media.muted ? "Unmute audio" : "Mute audio");
|
|
176
|
+
}, 0);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
function skipBackward() {
|
|
@@ -172,8 +189,12 @@ function skipBy(offset) {
|
|
|
172
189
|
const media = getMedia();
|
|
173
190
|
if (!media) return;
|
|
174
191
|
const nextTime = Math.min(Math.max(media.currentTime + offset, 0), safeDuration.value || media.currentTime + offset);
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
ignoreMediaEventsFor(100);
|
|
193
|
+
window.setTimeout(() => {
|
|
194
|
+
if (isDisposed) return;
|
|
195
|
+
media.currentTime = nextTime;
|
|
196
|
+
currentTime.value = nextTime;
|
|
197
|
+
}, 0);
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
function formatTime(seconds) {
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<button class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
35
|
-
<span
|
|
36
|
-
<span m-else class="fa-icon icon-play large" aria-hidden="true"></span>
|
|
35
|
+
<span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
|
|
37
36
|
</button>
|
|
38
37
|
|
|
39
38
|
<div class="control-shelf">
|
|
@@ -61,15 +60,13 @@
|
|
|
61
60
|
<div class="control-row">
|
|
62
61
|
<div class="left-controls">
|
|
63
62
|
<button class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
64
|
-
<span
|
|
65
|
-
<span m-else class="fa-icon icon-play" aria-hidden="true"></span>
|
|
63
|
+
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
66
64
|
</button>
|
|
67
65
|
<button class="icon-button" type="button" @click="stopPlayback" aria-label="Stop video">
|
|
68
66
|
<span class="fa-icon icon-stop" aria-hidden="true"></span>
|
|
69
67
|
</button>
|
|
70
68
|
<button class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
71
|
-
<span
|
|
72
|
-
<span m-else class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
69
|
+
<span class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
73
70
|
</button>
|
|
74
71
|
<label class="volume">
|
|
75
72
|
<span>Volume</span>
|
|
@@ -79,10 +76,9 @@
|
|
|
79
76
|
</div>
|
|
80
77
|
|
|
81
78
|
<div class="right-controls">
|
|
82
|
-
<button class="pill-button" type="button" @click="cycleRate"
|
|
79
|
+
<button class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
|
|
83
80
|
<button class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
|
|
84
|
-
<span
|
|
85
|
-
<span m-else class="fa-icon icon-maximize" aria-hidden="true"></span>
|
|
81
|
+
<span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
|
|
86
82
|
</button>
|
|
87
83
|
</div>
|
|
88
84
|
</div>
|
|
@@ -119,7 +115,6 @@ const isFullscreen = ref(false);
|
|
|
119
115
|
const isSeeking = ref(false);
|
|
120
116
|
const pointerInside = ref(false);
|
|
121
117
|
const controlsVisible = ref(true);
|
|
122
|
-
const playbackRate = ref(1);
|
|
123
118
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
124
119
|
const seekProgress = computed(() => {
|
|
125
120
|
if (safeDuration.value <= 0) return 0;
|
|
@@ -129,8 +124,13 @@ const seekProgress = computed(() => {
|
|
|
129
124
|
const seekStyle = computed(() => "--progress: " + seekProgress.value + "%");
|
|
130
125
|
const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video");
|
|
131
126
|
const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
|
|
127
|
+
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
128
|
+
const fullscreenIconClass = computed(() => isFullscreen.value ? "icon-minimize" : "icon-maximize");
|
|
132
129
|
const rates = [1, 1.25, 1.5, 2, 0.75];
|
|
130
|
+
let playbackRate = 1;
|
|
131
|
+
let rateIndex = 0;
|
|
133
132
|
let isDisposed = false;
|
|
133
|
+
let ignoreMediaEventsUntil = 0;
|
|
134
134
|
|
|
135
135
|
onMounted(() => {
|
|
136
136
|
applyAudioSettings();
|
|
@@ -152,15 +152,20 @@ function getMedia() {
|
|
|
152
152
|
return mediaEl.value;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function ignoreMediaEventsFor(ms) {
|
|
156
|
+
ignoreMediaEventsUntil = performance.now() + ms;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shouldIgnoreMediaEvent() {
|
|
160
|
+
return performance.now() < ignoreMediaEventsUntil;
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
function syncMedia() {
|
|
156
|
-
if (isDisposed) return;
|
|
164
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
157
165
|
const media = getMedia();
|
|
158
166
|
if (!media) return;
|
|
159
167
|
currentTime.value = media.currentTime || 0;
|
|
160
168
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
161
|
-
volume.value = media.volume;
|
|
162
|
-
muted.value = media.muted;
|
|
163
|
-
playbackRate.value = media.playbackRate;
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
function applyAudioSettings() {
|
|
@@ -169,17 +174,17 @@ function applyAudioSettings() {
|
|
|
169
174
|
if (!media) return;
|
|
170
175
|
media.volume = volume.value;
|
|
171
176
|
media.muted = muted.value;
|
|
172
|
-
media.playbackRate = playbackRate
|
|
177
|
+
media.playbackRate = playbackRate;
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
function markPlaying() {
|
|
176
|
-
if (isDisposed) return;
|
|
181
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
177
182
|
isPlaying.value = true;
|
|
178
183
|
controlsVisible.value = pointerInside.value;
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
function markPaused() {
|
|
182
|
-
if (isDisposed) return;
|
|
187
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
183
188
|
isPlaying.value = false;
|
|
184
189
|
controlsVisible.value = true;
|
|
185
190
|
}
|
|
@@ -196,7 +201,7 @@ async function togglePlayback() {
|
|
|
196
201
|
try {
|
|
197
202
|
await media.play();
|
|
198
203
|
} catch (error) {
|
|
199
|
-
if (
|
|
204
|
+
if (error?.name !== "AbortError") {
|
|
200
205
|
throw error;
|
|
201
206
|
}
|
|
202
207
|
}
|
|
@@ -283,33 +288,56 @@ function seekWithKeyboard(event) {
|
|
|
283
288
|
|
|
284
289
|
function stopPlayback() {
|
|
285
290
|
if (isDisposed) return;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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);
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
function setVolume(event) {
|
|
295
302
|
if (isDisposed) return;
|
|
296
303
|
const nextVolume = Number(event.target.value);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
window.setTimeout(() => {
|
|
305
|
+
if (isDisposed) return;
|
|
306
|
+
const media = getMedia();
|
|
307
|
+
if (!media) return;
|
|
308
|
+
media.volume = nextVolume;
|
|
309
|
+
media.muted = nextVolume === 0;
|
|
310
|
+
}, 0);
|
|
300
311
|
}
|
|
301
312
|
|
|
302
|
-
function toggleMute() {
|
|
313
|
+
function toggleMute(event) {
|
|
303
314
|
if (isDisposed) return;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
const button = event.currentTarget;
|
|
316
|
+
window.setTimeout(() => {
|
|
317
|
+
if (isDisposed) return;
|
|
318
|
+
const media = getMedia();
|
|
319
|
+
if (!media) return;
|
|
320
|
+
media.muted = !media.muted;
|
|
321
|
+
const icon = button.querySelector(".fa-icon");
|
|
322
|
+
icon?.classList.toggle("icon-volume", !media.muted);
|
|
323
|
+
icon?.classList.toggle("icon-mute", media.muted);
|
|
324
|
+
button.setAttribute("aria-label", media.muted ? "Unmute video" : "Mute video");
|
|
325
|
+
}, 0);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function cycleRate(event) {
|
|
309
329
|
if (isDisposed) return;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
330
|
+
const button = event.currentTarget;
|
|
331
|
+
window.setTimeout(() => {
|
|
332
|
+
if (isDisposed) return;
|
|
333
|
+
rateIndex = (rateIndex + 1) % rates.length;
|
|
334
|
+
playbackRate = rates[rateIndex];
|
|
335
|
+
const media = getMedia();
|
|
336
|
+
if (media) {
|
|
337
|
+
media.playbackRate = playbackRate;
|
|
338
|
+
}
|
|
339
|
+
button.textContent = playbackRate + "x";
|
|
340
|
+
}, 0);
|
|
313
341
|
}
|
|
314
342
|
|
|
315
343
|
function toggleFullscreen() {
|