fix(qt): four AUDIBLE parity rows — stream errors, seek clamp, label shuffle, Plex keys, Library queue

PARITY-DEBT #7, #15, #17, #4 and #5.

- **#7** the stream error was drained and only logged, so an ALSA / exclusive
  failure — guaranteed inside Flatpak and Snap — froze the bar at 0:00 with no
  explanation. It now toasts, with the sandbox-aware rewrite the reference
  carries (playback.rs:5093-5115). This is the support issue that fix was
  written for.
- **#15** seek was clamped to the full duration instead of the downloaded
  fraction, so the head could be dragged into bytes that do not exist. The
  poll loop publishes `npSeekableMax` and both bars clamp to it, matching
  SeekBar.slint:93-98. The cache overlay was already drawn and purely
  decorative.
- **#17** a label page's Shuffle called `set_shuffle(true)`, turning the GLOBAL
  shuffle mode on and leaving it on for everything played afterwards. The
  reference shuffles the vector in place and never touches the mode.
- **#4** a `plex:<hash>` album-boundary key was passed where a Plex rating key
  belongs, so the fetch 404'd and the PREVIOUS track kept playing under the new
  card. Guarded at both sites, 1:1 with playback.rs:668-680 (`b5c1a76e`).
- **#5** clicking a track in Library played exactly one track and stopped. It
  now queues the whole VISIBLE list from the clicked row (`order_by_visible`).

The Library row play needed one edit outside its author's lane, which the
singleton xref caught before the build did: `QbzLibrary.libraryPlayVisible` was
called from QML and implemented in `library_qt`, but never declared on the
bridge. That is precisely the failure the xref exists for — QML resolves
singleton members lazily, so a missing invokable compiles clean and throws when
the user clicks. The QML had shipped a `typeof === "function"` feature-detect
with a fallback, so it degraded instead of throwing; the invokable now exists
and the fallback is dead code by design.

cc
This commit is contained in:
vicrodh committed 2026-07-30 15:36:40 -06:00
1 parent 1c2bdb749a
commit ea373a601c
11 files changed
+759 -60

No files matched your search

+38 -3
View File
@@ -155,6 +155,29 @@ Rectangle {
return m + ":" + (s < 10 ? "0" : "") + s
}
// --- Seek clamp (closes PARITY-DEBT #15, QML half) ---------------------
// SeekBar.slint:24-26 / 93-98: while a track is still downloading the seek
// target is LOCKED to the furthest fraction that has arrived, and the
// cursor turns not-allowed over the region that has not. The source is
// NowPlayingState.seekable-max (state.slint:4402), fed by
// playback.rs:5304 `buffer_progress.clamp(0,1)` and published here as
// QbzPlayer.npSeekableMax.
//
// A fully-available track (local, cached, or a finished download) reports
// 1.0, so Math.min() is a no-op and seeking stays completely free — the
// clamp can never fight a local track.
function clamp01(v) {
return Math.min(Math.max(v, 0), 1)
}
// SeekBar.slint:98 — Math.min(clamp01(mouse-x / width), seekable-max).
function seekTarget(fraction) {
return Math.min(root.clamp01(fraction), QbzPlayer.npSeekableMax)
}
// SeekBar.slint:93 — clamp01(mouse-x / width) > seekable-max.
function beyondSeekable(fraction) {
return root.clamp01(fraction) > QbzPlayer.npSeekableMax
}
// --- Track Info (album/TrackInfoModal.slint) ---------------------------
// The (i) button and the song-card title open the MODAL (scrim + centered
// card), which is what the .slint does: the bars fire
@@ -263,9 +286,21 @@ Rectangle {
anchors.right: seekTrack.right
anchors.top: parent.top
anchors.bottom: parent.bottom
cursorShape: QbzPlayer.npHasTrack ? Qt.PointingHandCursor : Qt.ArrowCursor
onPressed: if (QbzPlayer.npHasTrack) QbzPlayer.seek(Math.min(Math.max(mouseX / width, 0), 1))
onPositionChanged: if (pressed && QbzPlayer.npHasTrack) QbzPlayer.seek(Math.min(Math.max(mouseX / width, 0), 1))
// Hover is what keeps mouseX live for the cursor binding
// below (PlayerBar.slint:180 -> SeekBar.slint's TouchArea
// always tracks mouse-x).
hoverEnabled: true
// No-drop cursor over the not-yet-downloaded region
// (SeekBar.slint:93-95). The npHasTrack arm is the Qt bar's
// own idle guard and stays first.
cursorShape: !QbzPlayer.npHasTrack
? Qt.ArrowCursor
: (root.beyondSeekable(mouseX / width) ? Qt.ForbiddenCursor
: Qt.PointingHandCursor)
// Lock the seek target to what has downloaded while streaming
// (SeekBar.slint:96-99).
onPressed: if (QbzPlayer.npHasTrack) QbzPlayer.seek(root.seekTarget(mouseX / width))
onPositionChanged: if (pressed && QbzPlayer.npHasTrack) QbzPlayer.seek(root.seekTarget(mouseX / width))
}
}