fix(web): tick player time every frame and keep lyrics in sync (#107)

`store.elapsed` is a Pinia getter (a cached Vue computed) that interpolates
with `Date.now()`. Because `Date.now()` is not a reactive dependency, the
computed only re-ran on WebSocket pushes / the 3s server poll, so the bottom
progress bar jumped ~3s at a time and lyric highlighting lagged ~half a line —
even though the consumers read it from a 60fps requestAnimationFrame loop.

Add a pure `interpolateElapsed()` helper and a non-cached `liveElapsed()` store
action. The per-frame consumers now call `liveElapsed()` so the value advances
every frame instead of returning a frozen cache:
- web/src/components/Player.vue  (desktop progress bar, rAF)
- web/src/App.vue                (mobile progress bar, rAF)
- web/src/views/Lyrics.vue       (lyric highlight, 500ms interval)

pause() now freezes at the live value rather than a possibly-stale cached one.
The `elapsed` getter is refactored onto the same helper (behaviour unchanged).

Adds web/src/stores/elapsed.test.ts covering the time-advancing interpolation,
paused freeze, no-anchor, and duration-clamp cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-06-30 16:17:53 +08:00
1 parent ea6820204d
commit 45f5d236c1
5 files changed
+107 -11

No files matched your search

+3 -2
View File
@@ -135,8 +135,9 @@ function formatTime(seconds: number): string {
}
function updateProgress() {
// Use store.elapsed which interpolates from server ground truth
currentElapsed.value = store.elapsed;
// liveElapsed() (an action, not the cached `elapsed` getter) re-interpolates
// from the server anchor on every frame so the clock ticks each second (#107).
currentElapsed.value = store.liveElapsed();
const duration = currentSong.value?.duration ?? 0;
progressPercent.value = duration > 0