mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 21:12:49 +08:00
fix(web): keep the volume slider draggable under the per-frame progress loop (#111)
The 60fps requestAnimationFrame progress clock (#107) re-renders the player every ~16ms, and Vue re-applied `el.value = storeVolume` on a range input each time — snapping the thumb back to the stale store value mid-drag (un-draggable on desktop, janky on mobile). Extract the decoupling into a useDecoupledSlider composable used by both the desktop (Player.vue) and mobile (App.vue) sliders: a local display ref tracks the native drag via @input (so the bound value always matches the element), the store is committed only on @change (release), and an onRelease safety-net (pointerup/pointercancel/blur) clears the dragging guard even when the browser skips `change` (value released at its start point). External/store changes still flow into the display except while dragging. Adds a regression test for the no-snap-back invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4cb1da29d4
commit
5ae5168b6b
4 files changed
+187
-14
No files matched your search
@@ -65,8 +65,12 @@
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
:value="activeBot?.volume ?? 75"
|
||||
:value="volumeDisplay"
|
||||
@input="onVolumeInput"
|
||||
@change="onVolumeChange"
|
||||
@pointerup="onVolumeRelease"
|
||||
@pointercancel="onVolumeRelease"
|
||||
@blur="onVolumeRelease"
|
||||
class="volume-slider"
|
||||
/>
|
||||
</template>
|
||||
@@ -87,6 +91,7 @@ import { Icon } from '@iconify/vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { usePlayerStore } from '../stores/player.js';
|
||||
import { useSession } from '../composables/useSession.js';
|
||||
import { useDecoupledSlider } from '../composables/useDecoupledSlider.js';
|
||||
import CoverArt from './CoverArt.vue';
|
||||
import Queue from './Queue.vue';
|
||||
|
||||
@@ -185,10 +190,17 @@ function togglePlay() {
|
||||
}
|
||||
}
|
||||
|
||||
function onVolumeChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
store.setVolume(parseInt(target.value));
|
||||
}
|
||||
// Volume slider is decoupled from the per-frame rAF re-render so dragging the
|
||||
// thumb isn't reset every frame (#111). See useDecoupledSlider.
|
||||
const {
|
||||
display: volumeDisplay,
|
||||
onInput: onVolumeInput,
|
||||
onChange: onVolumeChange,
|
||||
onRelease: onVolumeRelease,
|
||||
} = useDecoupledSlider(
|
||||
() => activeBot.value?.volume,
|
||||
(v) => store.setVolume(v)
|
||||
);
|
||||
|
||||
const modeOrder = ['seq', 'loop', 'random', 'rloop'] as const;
|
||||
const modeIcons: Record<string, string> = {
|
||||
|
||||
Reference in new issue
Block a user