feat(qbz-qt): staged loading, skeletons everywhere, lyrics, gestures, richer menus
**Deferred external rows.** The album page awaited the Last.fm row before publishing anything, so the tracklist waited on an external service plus MusicBrainz. It publishes in stages now — header and tracks first, then each Qobuz rail as it lands, then Last.fm — with every patch merged under a generation counter so a late reply for album A cannot paint onto album B. The artist page was already staged this way; its views were the part still showing bare "Loading…" text. **Skeletons as the standard.** One control, new variants rather than new components, mounted across the views. Its second job is the one the owner named: cleanup. A cover that never resolves settles the placeholder once and reveals the card's own empty state instead of shimmering forever — local albums with no embedded art produce no artwork entry at all, so a placeholder gated only on "not arrived yet" would never stop. **Local artwork streams.** Fifteen seconds to first cover on a 1274-album grid was one batch emitted after the whole window resolved. Each cover emits as it resolves now, visible band before prefetch margin. **Lyrics migrated.** Smooth sync instead of a 1 Hz tick, word-level karaoke where the document carries it, the surrounding-line dimming, the controls flyout and the translation layer the v10 backend already supported. Exactly one row subscribes to the 30 Hz output; the timer drops to 250ms when paused or hidden. **Navigation by mouse and touchpad.** Side buttons and a two-finger horizontal swipe, matching #653 — including the part that matters, which is that a swipe over a carousel scrolls the carousel. The layer is declared first in the shell so everything else gets first refusal. **Context menus.** Brought to the Slint inventory, Play later included, with right-click opening the same menu as the button everywhere. Entries whose seam does not exist are hidden rather than shipped inert. Two controls that rendered and no-opped are now honestly disabled: the My QBZ nav row, which routes to a view the shell has no case for, and three sidebar menu entries with no backing call. The sidebar caret was never a condition bug. The row is a positioner and the name cell's width ignored the count label, so a folder WITH content pushed the chevron past the clip — which is why it vanished exactly when it mattered. cc
This commit is contained in:
52 files changed
+4962
-719
No files matched your search
@@ -16,6 +16,7 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Window
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
@@ -57,7 +58,43 @@ Rectangle {
|
||||
var i
|
||||
for (i = 0; i < upcoming.length; i++) if (upcoming[i].artUrl) urls.push(upcoming[i].artUrl)
|
||||
for (i = 0; i < historyRows.length; i++) if (historyRows[i].artUrl) urls.push(historyRows[i].artUrl)
|
||||
if (urls.length > 0) QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
|
||||
if (urls.length > 0) {
|
||||
QbzShell.sidebarArtworkWindow(JSON.stringify(urls))
|
||||
root.artPulse = true
|
||||
artPulseOff.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// --- skeleton pulse (PER-ITEM COVERS ONLY) ---------------------------
|
||||
// The queue is LOCAL state — it never "loads", so this panel has no
|
||||
// list-level skeleton ON PURPOSE: there is no asynchronous fetch to
|
||||
// stand in for. What IS asynchronous is each row's cover, which is why
|
||||
// the placeholders here are per-item and nothing else.
|
||||
// GATING RULE: freeze on NOT VISIBLE — the panel collapsed, or the
|
||||
// window minimized/hidden. NEVER on lost focus (a tiling desktop keeps
|
||||
// windows visible and unfocused).
|
||||
property bool skelPhase: false
|
||||
readonly property bool windowShowing: root.Window.window
|
||||
? (root.Window.window.visibility !== Window.Minimized
|
||||
&& root.Window.window.visibility !== Window.Hidden)
|
||||
: true
|
||||
// A cover that never resolves (no art, or a failed fetch) leaves the map
|
||||
// untouched, so every placeholder is bounded — see QbzSkeleton settleMs.
|
||||
readonly property int artSettleMs: 2500
|
||||
property bool artPulse: false
|
||||
Timer {
|
||||
id: artPulseOff
|
||||
interval: root.artSettleMs
|
||||
onTriggered: root.artPulse = false
|
||||
}
|
||||
Timer {
|
||||
interval: 900
|
||||
repeat: true
|
||||
running: root.artPulse && root.visible && root.windowShowing
|
||||
onTriggered: root.skelPhase = !root.skelPhase
|
||||
}
|
||||
function coverPending(url) {
|
||||
return (url || "") !== "" && (root.coverMap[url] || "") === ""
|
||||
}
|
||||
|
||||
component QueueTab: Text {
|
||||
@@ -132,6 +169,16 @@ Rectangle {
|
||||
source: root.coverMap[row.artUrl] || ""
|
||||
radius: 4
|
||||
}
|
||||
// Per-item: this thumbnail clears when ITS cover lands, so
|
||||
// History fills in progressively rather than all at once.
|
||||
QbzSkeleton {
|
||||
variant: "art"
|
||||
anchors.fill: parent
|
||||
blockRadius: 4
|
||||
visible: root.coverPending(row.artUrl)
|
||||
phase: root.skelPhase
|
||||
settleMs: root.artSettleMs
|
||||
}
|
||||
}
|
||||
|
||||
// Title + artist.
|
||||
@@ -389,6 +436,15 @@ Rectangle {
|
||||
source: root.currentRow ? (root.coverMap[root.currentRow.artUrl] || "") : ""
|
||||
radius: 4
|
||||
}
|
||||
QbzSkeleton {
|
||||
variant: "art"
|
||||
anchors.fill: parent
|
||||
blockRadius: 4
|
||||
visible: root.currentRow
|
||||
? root.coverPending(root.currentRow.artUrl) : false
|
||||
phase: root.skelPhase
|
||||
settleMs: root.artSettleMs
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent.width - 34 - 28 - 2 * 9
|
||||
|
||||
Reference in new issue
Block a user