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
@@ -34,6 +34,7 @@
|
||||
// album/LocalAlbumView.slint.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
@@ -367,6 +368,54 @@ Rectangle {
|
||||
if (keys.length > 0) QbzLocal.artworkWindow(JSON.stringify(keys))
|
||||
}
|
||||
|
||||
// ------------------------- skeleton pulse ----------------------------
|
||||
// ONE 900ms Timer drives EVERY placeholder in this view AND in every
|
||||
// tab body under views/local/ (they read `view.skelPhase`) — N
|
||||
// placeholders, 1 timer, which is QbzSkeleton's preferred drive mode.
|
||||
// GATING RULE: freeze on NOT VISIBLE — the view hidden, 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
|
||||
readonly property bool anyTabLoading: QbzLocal.localAlbumsLoading
|
||||
|| QbzLocal.localArtistsLoading || QbzLocal.localFoldersLoading
|
||||
|| QbzLocal.localTreeLoading || QbzLocal.localTracksLoading
|
||||
|| QbzLocal.localTracksLoadingMore || QbzLocal.localDetailLoading
|
||||
|| QbzLocal.localEphemeralLoading
|
||||
|
||||
// THE CLEANUP RULE FOR LOCAL ARTWORK. src/local_artwork.rs
|
||||
// (`resolve_window_blocking`) DROPS keys with no cover — "the QML map
|
||||
// only ever grows with real hits" — so a local album with no embedded
|
||||
// art NEVER produces an artMap entry. A per-item placeholder gated only
|
||||
// on "artMap has no path yet" would therefore shimmer forever on every
|
||||
// artless album. `artSettleMs` is the bound: each placeholder fades out
|
||||
// by itself that long after it appears, revealing the card's own empty
|
||||
// tile. Per-item clearing still wins whenever the cover DOES arrive.
|
||||
readonly property int artSettleMs: 2500
|
||||
// The pulse only has to run while an artwork window can still resolve:
|
||||
// armed by every window report, disarmed artSettleMs after the last.
|
||||
property bool artPulse: false
|
||||
Timer {
|
||||
id: artPulseOff
|
||||
interval: root.artSettleMs
|
||||
onTriggered: root.artPulse = false
|
||||
}
|
||||
Timer {
|
||||
interval: 900
|
||||
repeat: true
|
||||
running: (root.anyTabLoading || root.artPulse)
|
||||
&& root.visible && root.windowShowing
|
||||
onTriggered: root.skelPhase = !root.skelPhase
|
||||
}
|
||||
/// Is THIS row's cover still outstanding? (the per-item gate every local
|
||||
/// surface uses — one place, so the rule cannot drift between tabs.)
|
||||
function artPending(key) {
|
||||
return (key || "") !== "" && (root.artMap[key] || "") === ""
|
||||
}
|
||||
|
||||
// Debounced reporting (180ms — the LibraryView throttle).
|
||||
Timer {
|
||||
id: windowDebounce
|
||||
@@ -381,6 +430,10 @@ Rectangle {
|
||||
windowDebounce.pendingFirst = first
|
||||
windowDebounce.pendingLast = last
|
||||
windowDebounce.restart()
|
||||
// Covers can now land: keep the shared pulse alive until artSettleMs
|
||||
// after the LAST report (scrolling keeps re-arming it).
|
||||
root.artPulse = true
|
||||
artPulseOff.restart()
|
||||
}
|
||||
|
||||
// ============================ actions ================================
|
||||
|
||||
Reference in new issue
Block a user