feat(volume-control): scroll-to-adjust icon + live debounced slider drag
Wheel over the volume icon steps volume by 5. The slider now updates live on drag (@input) instead of only on release, debounced 60ms to avoid spamming SetVolume. A local pendingVolume tracks intent so rapid events accumulate and UI stays responsive ahead of the backend echo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,28 @@ import type WaSlider from '@awesome.me/webawesome/dist/components/slider/slider.
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { designTokens } from '../../../styles/tokens.css';
|
||||
|
||||
/** Volume change (0-100) applied per scroll-wheel tick. */
|
||||
const WHEEL_STEP = 5;
|
||||
|
||||
/** Delay before a live volume change is pushed to the backend. */
|
||||
const VOLUME_DEBOUNCE_MS = 60;
|
||||
|
||||
@customElement('volume-control')
|
||||
export class VolumeControl extends LitElement {
|
||||
private player = new PlayerController(this);
|
||||
private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
private volumeDebounceTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
@state()
|
||||
private showSlider = false;
|
||||
|
||||
// Locally-tracked volume while the user is actively dragging or scrolling.
|
||||
// The store's volume only updates once the backend echoes VolumeChanged
|
||||
// (which we debounce), so we track intent here for responsive UI and to let
|
||||
// rapid events accumulate. Cleared once the store catches up.
|
||||
@state()
|
||||
private pendingVolume: number | null = null;
|
||||
|
||||
static override styles = [designTokens, css`
|
||||
:host {
|
||||
position: relative;
|
||||
@@ -70,8 +84,12 @@ export class VolumeControl extends LitElement {
|
||||
// DERIVED STATE
|
||||
// ===================================================================
|
||||
|
||||
private get currentVolume(): number {
|
||||
return this.pendingVolume ?? this.player.volume;
|
||||
}
|
||||
|
||||
private get volumeIcon(): string {
|
||||
const vol = this.player.volume;
|
||||
const vol = this.currentVolume;
|
||||
|
||||
if (vol === 0) return 'volume-xmark';
|
||||
if (vol <= 50) return 'volume-low';
|
||||
@@ -86,6 +104,15 @@ export class VolumeControl extends LitElement {
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleOutsideClick);
|
||||
clearTimeout(this.volumeDebounceTimer);
|
||||
}
|
||||
|
||||
override willUpdate() {
|
||||
// Once the backend has echoed our pending change back through the store,
|
||||
// drop the local override so external volume changes are reflected again.
|
||||
if (this.pendingVolume !== null && this.player.volume === this.pendingVolume) {
|
||||
this.pendingVolume = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -113,21 +140,37 @@ export class VolumeControl extends LitElement {
|
||||
}
|
||||
|
||||
private handleInput(e: Event) {
|
||||
const value = (e.target as WaSlider).value;
|
||||
this.player.setVolume(value);
|
||||
this.changeVolume((e.target as WaSlider).value);
|
||||
}
|
||||
|
||||
private handleWheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
const direction = e.deltaY < 0 ? 1 : -1;
|
||||
this.changeVolume(this.currentVolume + direction * WHEEL_STEP);
|
||||
}
|
||||
|
||||
private handlePopupClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/** Update the UI immediately and push to the backend on a short debounce. */
|
||||
private changeVolume(value: number) {
|
||||
const clamped = Math.max(0, Math.min(100, Math.round(value)));
|
||||
this.pendingVolume = clamped;
|
||||
|
||||
clearTimeout(this.volumeDebounceTimer);
|
||||
this.volumeDebounceTimer = setTimeout(() => {
|
||||
this.player.setVolume(clamped);
|
||||
}, VOLUME_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// RENDER
|
||||
// ===================================================================
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<button @click="${this.toggleSlider}">
|
||||
<button @click="${this.toggleSlider}" @wheel="${this.handleWheel}">
|
||||
<wa-icon name=${this.volumeIcon}></wa-icon>
|
||||
</button>
|
||||
${this.showSlider
|
||||
@@ -137,8 +180,8 @@ export class VolumeControl extends LitElement {
|
||||
orientation="vertical"
|
||||
min="0"
|
||||
max="100"
|
||||
.value="${this.player.volume}"
|
||||
@change="${this.handleInput}"
|
||||
.value="${this.currentVolume}"
|
||||
@input="${this.handleInput}"
|
||||
></wa-slider>
|
||||
</div>
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user