fix: stabilize playback memory, local library sync and Qt settings

Add shared playback memory profiles, bounded disk-backed playback and storage
selection for desktop and daemon. Preserve gapless during streaming-only playback
and provide explicit cache eviction with playback confirmation.

Stop watcher read events and unchanged scans from rebuilding the local catalog.
Restore playlist/sidebar updates, folder availability feedback and player layout.
Add appearance presets and a radio-based cache settings panel with translated
trade-offs, plus consistent clipboard menus across visible Qt text editors.

Validate with Rust regressions, QML interaction tests, isolated release startup
and idle-library checks; extend the shared local and CI gates.

cx
This commit is contained in:
vicrodh committed 2026-09-13 02:57:21 -06:00
1 parent 3cf1f32177
commit e291eb3af4
118 files changed
+7128 -883

No files matched your search

+7 -98
View File
@@ -44,9 +44,6 @@ Rectangle {
// PlayerBarSmall.slint).
color: ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
readonly property bool ambientOn: theme.ambientOn
// Feature parked for a later release. Keep the compact renderer in place
// but use the classic seekbar consistently with the other NPB modes.
readonly property bool waveformVisible: false
/// AppShell's one shared hover-tooltip overlay.
property Item tooltip: null
@@ -142,27 +139,6 @@ Rectangle {
return m + ":" + (s < 10 ? "0" : "") + s
}
// --- Seek clamp (closes PARITY-DEBT #15, QML half) -------------------
// Same SeekBar the full bar mounts (PlayerBarSmall.slint:186 passes
// seekable-max: NowPlayingState.seekable-max into SeekBar.slint), so the
// behaviour is identical here: while a track is still downloading the
// seek target is LOCKED to the furthest fraction that has arrived and the
// cursor turns not-allowed past it. Source: state.slint:4402, fed by
// playback.rs:5304 `buffer_progress.clamp(0,1)`, published as
// QbzPlayer.npSeekableMax. A fully-available track reports 1.0, so the
// Math.min() is a no-op and local/cached seeking stays free.
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) — PlayerBarSmall.slint fires media-action("track", id,
@@ -214,81 +190,14 @@ Rectangle {
spacing: 0
// === A. TOP full-width seekbar (the content/bar divider) ========
Item {
QbzCompactSeekBar {
width: parent.width
height: 3
Rectangle {
id: seekTrack
width: parent.width
height: root.waveformVisible ? 13 : 3
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: root.waveformVisible ? "transparent" : theme.surfaceElevated
SeekWaveformItem {
anchors.fill: parent
visible: root.waveformVisible
values: QbzPlayer.npSeekWaveform
playedProgress: QbzPlayer.npProgress
cacheProgress: QbzPlayer.npCacheProgress
baseColor: theme.surfaceElevated
cacheColor: Qt.rgba(theme.textMuted.r, theme.textMuted.g,
theme.textMuted.b, 0.35)
playedColor: theme.accent
renderMode: 1
}
// Buffered / cache line.
Rectangle {
visible: !root.waveformVisible
width: parent.width * Math.min(Math.max(QbzPlayer.npCacheProgress, 0), 1)
height: parent.height
radius: 2
color: Qt.rgba(theme.textMuted.r, theme.textMuted.g,
theme.textMuted.b, 0.35)
// Alpha in the material, not a container `opacity`: an
// always-on opacity node pins this quad in its own batch,
// and the three seek quads use compatible materials
// (QSGSmoothColorMaterial::compare() returns 0), so folding
// it lets rail + cache + progress merge.
}
// Playback progress line.
Rectangle {
visible: !root.waveformVisible
width: parent.width * Math.min(Math.max(QbzPlayer.npProgress, 0), 1)
height: parent.height
radius: 2
color: theme.accent
}
// Hover thumb.
Rectangle {
width: 12
height: 12
radius: 6
color: theme.textPrimary
x: parent.width * Math.min(Math.max(QbzPlayer.npProgress, 0), 1) - width / 2
anchors.verticalCenter: parent.verticalCenter
opacity: seekArea.containsMouse ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 100 } }
}
}
// Tall, easy-to-grab hit area over the thin line.
MouseArea {
id: seekArea
width: parent.width
height: 18
anchors.verticalCenter: seekTrack.verticalCenter
hoverEnabled: true
// No-drop cursor over the not-yet-downloaded region
// (SeekBar.slint:93-95).
cursorShape: root.beyondSeekable(mouseX / width) ? Qt.ForbiddenCursor
: Qt.PointingHandCursor
enabled: QbzPlayer.npHasTrack
// Lock the seek target to what has downloaded while streaming
// (SeekBar.slint:96-99).
onClicked: QbzPlayer.seek(root.seekTarget(mouseX / width))
}
hasTrack: QbzPlayer.npHasTrack
progress: QbzPlayer.npProgress
cacheProgress: QbzPlayer.npCacheProgress
seekableMax: QbzPlayer.npSeekableMax
durationSecs: QbzPlayer.npDurationSecs
onSeekRequested: function (fraction) { QbzPlayer.seek(fraction) }
}
// === B. CONTROLS ROW — symmetric 3 columns =======================