lyrics: keep following through same-song refreshes

This commit is contained in:
bbedward
2026-09-23 20:16:42 -04:00
parent 145b9bbc18
commit 878aa69fac
7 changed files with 43 additions and 18 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790195634,
"narHash": "sha256-s+9ZHGZmKtc1oyQgvi7obdjoASEnelQ25gNO7zL3wRM=",
"lastModified": 1790208961,
"narHash": "sha256-lCXDr+VgmazTsJx7FgrBdur70tSNK2TSV/PrL+UDHsE=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "640b739e31709d9aa7d5b9d26f511babc9b95daf",
"rev": "27e67d6a0cc3d6ea211fe68197bf296ec387b0fd",
"type": "github"
},
"original": {
@@ -25,6 +25,7 @@ QtObject {
readonly property real position: activePlayer?.position ?? 0
readonly property int playbackState: activePlayer?.playbackState ?? MprisPlaybackState.Stopped
readonly property bool playing: playbackState === MprisPlaybackState.Playing
readonly property bool stopped: playbackState === MprisPlaybackState.Stopped
readonly property real rate: activePlayer?.rate ?? 1
property var lines: []
@@ -69,6 +70,7 @@ QtObject {
onEnabledChanged: {
if (!enabled) {
cancel();
stopClock();
if (state === "loading") {
requestedKey = "";
state = "idle";
@@ -93,20 +95,22 @@ QtObject {
onPositionChanged: scheduleAnchor()
onRateChanged: updatePlaybackClock()
onPlayingChanged: updatePlaybackClock()
onPlaybackStateChanged: {
if (playbackState === MprisPlaybackState.Stopped && !anchorPlaying)
updatePlaybackClock();
Component.onDestruction: {
cancel();
stopClock();
}
Component.onDestruction: cancel()
function cancel() {
serial++;
if (requestId !== null)
backend.cancelRequest(requestId);
requestId = null;
requestDelay.stop();
}
function stopClock() {
tick.stop();
anchorUpdate.stop();
requestDelay.stop();
}
function songTitle() {
@@ -124,6 +128,7 @@ QtObject {
}
function clearContent() {
stopClock();
lines = [];
plainLines = [];
synced = false;
@@ -188,8 +193,6 @@ QtObject {
const resultKey = found ? JSON.stringify([result.instrumental === true, result.synced ?? [], result.voices ?? {}, result.plain ?? "", result.attribution ?? {}]) : "";
if (refreshing && resultKey === shownResult)
return;
if (refreshing)
clearContent();
if (response.error) {
useEmbeddedText("error");
return;
@@ -201,6 +204,7 @@ QtObject {
shownSong = requestedSong;
shownResult = resultKey;
if (result.instrumental) {
clearContent();
state = "instrumental";
return;
}
@@ -323,9 +327,9 @@ QtObject {
function updatePlaybackClock() {
const predicted = currentTime();
const sampled = position + (anchorPlaying ? (Date.now() - positionWall) * anchorRate / 1000 : 0);
const pendingSeek = anchorUpdate.running && Math.abs(sampled - predicted) >= DashMetrics.mediaLyricsPositionTolerance;
const pendingSeek = !stopped && anchorUpdate.running && Math.abs(sampled - predicted) >= DashMetrics.mediaLyricsPositionTolerance;
anchorUpdate.stop();
anchorPosition = playbackState === MprisPlaybackState.Stopped ? 0 : pendingSeek ? sampled : predicted;
anchorPosition = pendingSeek ? sampled : predicted;
anchorWall = Date.now();
anchorPlaying = playing;
anchorRate = rate;
@@ -334,7 +338,7 @@ QtObject {
function scheduleAnchor() {
positionWall = Date.now();
if (enabled && synced && !anchorUpdate.running)
if (enabled && synced && !stopped && !anchorUpdate.running)
anchorUpdate.start();
}
@@ -27,6 +27,7 @@ FocusScope {
readonly property int activeIndex: controller.activeIndex
readonly property int firstFocusedIndex: controller.focusedGroups[0] ?? -1
readonly property string trackKey: controller.trackKey
readonly property string shownResult: controller.shownResult
readonly property bool userScrolling: transcript.isUserScrolling || transcript.dragging
readonly property real currentLineY: transcript.currentItem?.y ?? 0
readonly property real shapeInset: Math.ceil(Math.min(radius, width / 2, height / 2) * (1 - Math.SQRT1_2))
@@ -50,6 +51,7 @@ FocusScope {
onActiveIndexChanged: followTimer.restart()
onFirstFocusedIndexChanged: followTimer.restart()
onReadyChanged: snapToCurrent()
onShownResultChanged: snapToCurrent()
onWidthChanged: snapToCurrent()
onHeightChanged: snapToCurrent()
onTrackKeyChanged: {
@@ -100,10 +100,10 @@ Item {
if (!root.live || !root.lyricsOpener?.visible || !root.lyricsOpener.enabled)
return;
if (typeof root.lyricsOpener.requestFocus === "function") {
root.lyricsOpener.requestFocus(false);
root.lyricsOpener.requestFocus(false, Qt.OtherFocusReason);
return;
}
root.lyricsOpener.forceActiveFocus(Qt.PopupFocusReason);
root.lyricsOpener.forceActiveFocus(Qt.OtherFocusReason);
}
}
@@ -78,7 +78,12 @@ FocusScope {
function focusFace() {
pageActions.clearFocus();
(root.tab?.focusTarget ?? root).forceActiveFocus();
const target = root.tab?.focusTarget ?? root;
if (typeof target.requestFocus === "function") {
target.requestFocus(false, Qt.OtherFocusReason);
return true;
}
target.forceActiveFocus(Qt.OtherFocusReason);
return true;
}
+15 -1
View File
@@ -206,6 +206,20 @@ ShellRoot {
check(!overlay.following && input.findChild(overlay, "followPlayback")?.visible, "browsing pauses automatic following");
input.findChild(overlay, "followPlayback").click();
check(overlay.following, "follow action resumes synchronized scrolling");
const settledCalls = backend.calls;
MprisController.stableAlbum = "Late album";
waitFor(() => backend.calls === settledCalls + 1, "a late album refreshes the lookup");
check(media.lyrics.state === "ready" && media.lyrics.tick.running, "the shown lyrics keep following during the refresh");
backend.respond({
result: {
found: true,
synced: root.timed
}
});
check(media.lyrics.tick.running, "an unchanged refresh keeps the timer running");
source.playbackState = MprisPlaybackState.Stopped;
check(media.lyrics.activeIndex === 1, "stopped playback holds the lyric position");
source.playbackState = MprisPlaybackState.Playing;
source.playbackState = MprisPlaybackState.Paused;
check(!media.lyrics.tick.running, "paused playback has no lyric timer");
source.playbackState = MprisPlaybackState.Playing;
@@ -229,7 +243,7 @@ ShellRoot {
check(!media.lyrics.tick.running, "closing stops scheduling");
media.lyricsOpen = true;
waitFor(() => !!media.lyricsFocusTarget, "reopened lyrics load");
check(backend.calls === 1 && media.lyrics.state === "ready", "reopening reuses loaded lyrics");
check(backend.calls === settledCalls + 1 && media.lyrics.state === "ready", "reopening reuses loaded lyrics");
media.lyrics.request();
const stale = backend.pending[backend.nextId];
MprisController.stableTitle = "Next track";