Files
qbz/crates/qbz-qt/qml/views/library/FeedListRow.qml
T
vicrodh 47a7641fc2 fix(qt): bound the queue panels, and make the gate catch what it missed
Follow-up to 66ba12bd5, which fixed the coverflow but left two of its
neighbours and a gate that could not see either of them.

QueueTabsPanel built one ImmQueueRow per queue track when opened —
including every collapsed 0-height history slot, which on the reported
session was 1162 of 1935. The slot stays (its height is what makes the
collapsed-history arithmetic work); the row inside is now built only
within a viewport of the visible band.

SuggestionsPanel and AppShell's QueuePanel move behind Loaders. The
queue panel was never the freeze — its rows are paginated at 40 in
Rust — but a closed panel should not be constructed, and the audit
cannot know about a cap that lives on the other side of the bridge.
Gating it also let SuggestionsPanel drop the onVisibleChanged half of
its load hook: mounting IS becoming visible now.

The audit is the real content of this commit. Written narrow — only a
model fed by unbounded user data counts, because the wide rule reports
145 sites and a gate that noisy gets switched off in a day — it then
reported the pre-fix tree CLEAN. A gate that cannot catch the bug it
was written for is worth nothing, so it was tested against that tree
and fixed on two counts:

  - CROSS-FILE. The `visible:` sat in ImmersiveView.qml while the two
    Repeaters over the whole queue lived in CoverflowPanel.qml. There
    is now a transitive index: a component is heavy if its own file has
    an unbounded view or if it mounts something that does.
  - MULTI-LINE. The coverflow's gate spans two lines, so a first-line
    read saw `QbzShaderScene.scene === 0`, matched no known gate, and
    skipped it.

With both, it finds all five mounts on the pre-fix tree, including the
two that froze the app. Known gates also grew past tab predicates: the
coverflow was keyed on QbzImmersive.mode.

Verified on a copy of the real session state with the 1935-row queue
restored: launches to a painted Discover, idle at 1.0% CPU.

cc
2026-08-18 07:48:56 -06:00

357 lines
17 KiB
QML

// FeedListRow — the Library "All" feed's LIST row (FavoritesView.slint:1212+,
// the windowed mixed row: cover · title/subtitle · TYPE · source glyph ·
// quality · favourite indicator · ⋯).
//
// EXTRACTED from views/LibraryView.qml, where it was an inline `component`
// ~300 lines long inside a 1,880-line file (track rule 2). Behaviour is
// unchanged; the only edit is that the six things it used to read off `root`
// now come through `view` — the LibraryView instance — which is the same seam
// views/local/*.qml uses for LocalLibraryView.
import QtQuick
import com.blitzfc.qbz
import "../../cards"
import "../../controls"
import "../../theme"
Rectangle {
id: feedRow
/// The LibraryView root: artMap, skelPhase, showLocal and the three
/// shared play/menu helpers live there (never duplicated per row).
property var view: null
property var item: ({})
/// Viewport-relative position, for the skeleton's animated cap.
property int rowIndex: 0
QbzTheme { id: theme }
height: 44
radius: 6
color: rowArea.containsMouse ? theme.surfaceHover : "transparent"
// Live heart, as a real property — `item.isFavorite = !…` notified
// nothing (plain JS object; see rows/TrackRow.qml for the measurement)
// so this row's glyph and its menu label never moved on click. The
// binding is re-established on every new row object, so a republished
// feed still wins.
property bool favorite: feedRow.item.isFavorite === true
onItemChanged: feedRow.favorite = Qt.binding(function () {
return feedRow.item.isFavorite === true
})
function toggleFavorite() {
feedRow.favorite = !feedRow.favorite
QbzLibrary.libraryToggleFavorite(feedRow.item.kind, feedRow.item.id)
}
// Settle + rollback + cross-surface walk. `artKey` IS
// `library_qt::feed_key(kind, id)`, the very key the signal carries —
// which is also why LibraryView's own handler can patch the backing row by
// comparing against it.
Connections {
target: QbzLibrary
function onLibraryFavoriteChanged(key, value) {
if ((feedRow.item.artKey || "") !== "" && key === feedRow.item.artKey)
feedRow.favorite = value
}
}
// Row body — click plays/opens by kind. Declared BEFORE the cells so
// the ⋯ button and art-play win their clicks.
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (feedRow.item.kind === "track") feedRow.view.playTrackInContext(feedRow.item.id)
else if (feedRow.item.kind === "album") QbzAlbum.openAlbum(feedRow.item.id)
else if (feedRow.item.kind === "artist") QbzArtist.openArtist(feedRow.item.id)
else if (feedRow.item.kind === "playlist") QbzBridge.openPlaylist(feedRow.item.id)
else if (feedRow.item.kind === "label") QbzHome.openLabel(feedRow.item.id)
}
}
Row {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
// Col — artwork (round for artists) + hover play.
Rectangle {
width: 44
height: 44
anchors.verticalCenter: parent.verticalCenter
radius: feedRow.item.kind === "artist" ? 22 : 6
color: theme.surfaceElevated
clip: true
RoundedImage {
anchors.fill: parent
visible: !lrCollage.visible
source: feedRow.view.artMap[feedRow.item.artKey] || ""
radius: feedRow.item.kind === "artist" ? 22 : 6
// A label logo is contain-fitted (cropping cuts the
// wordmark) on a FLAT surface — those are transparent
// PNGs whose edges carry no colour to derive from.
// A playlist takes "auto": its own graphic is the 800x380
// `image_rectangle` and pads with an image-derived
// gradient, while a square member cover still crops. The
// flag is not required for that any more, but it stays
// authoritative where it IS published (this feed).
fit: feedRow.item.kind === "label" ? "contain"
: feedRow.item.kind === "playlist"
? (feedRow.item.playlistOwnImage === true ? "pad" : "auto")
: "crop"
}
// User playlists have no graphic of their own, so they show the
// member-cover mosaic instead of a blank tile.
PlaylistCollage {
id: lrCollage
anchors.fill: parent
visible: feedRow.item.kind === "playlist"
&& feedRow.item.playlistOwnImage !== true
&& (feedRow.view.artMap[feedRow.item.artKey] || "") === ""
urls: feedRow.item.covers || []
radius: 6
}
// Per-row cover placeholder — same progressive rule as the
// grid: it clears when THIS row's cover lands. Skipped for
// the collage arm (a playlist mosaic is real content) and
// for artists/labels (round designed placeholders).
QbzSkeleton {
variant: "art"
anchors.fill: parent
blockRadius: 6
visible: (feedRow.item.kind === "track" || feedRow.item.kind === "album")
&& feedRow.item.imageUrl !== ""
&& (feedRow.view.artMap[feedRow.item.artKey] || "") === ""
phase: feedRow.view.skelPhase
cellIndex: feedRow.rowIndex
}
Rectangle {
visible: feedRow.item.kind === "track" || feedRow.item.kind === "album"
|| feedRow.item.kind === "playlist"
anchors.fill: parent
radius: feedRow.item.kind === "artist" ? 22 : 6
color: "#a6000000"
opacity: lrPlayArea.containsMouse ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 150 } }
// On the #a6000000 artwork scrim — dark under every theme.
QbzIcon { name: "play-fill"; width: 16; height: 16; anchors.centerIn: parent; tintName: "white" }
MouseArea {
id: lrPlayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (feedRow.item.kind === "track") feedRow.view.playTrackInContext(feedRow.item.id)
else if (feedRow.item.kind === "album") QbzPlayer.playAlbum(feedRow.item.id)
// `playPlaylistById`, NOT `playPlaylist` — the latter
// plays the OPEN playlist page and takes no id
// (player_bridge.rs:189 is the by-id entry).
else if (feedRow.item.kind === "playlist") QbzPlayer.playPlaylistById(feedRow.item.id)
}
}
}
}
// Col — title + subtitle (artist link for track/album).
Column {
width: parent.width - 44 - 116 - (srcCol.visible ? 44 : 0)
- 150 - 18 - 30 - 6 * 12
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
width: parent.width
height: 18
text: feedRow.item.title
color: lrTitleArea.containsMouse ? theme.accent : theme.textPrimary
font.pixelSize: 14
font.weight: theme.weightMedium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
MouseArea {
id: lrTitleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (feedRow.item.kind === "track") feedRow.view.playTrackInContext(feedRow.item.id)
else if (feedRow.item.kind === "album") QbzAlbum.openAlbum(feedRow.item.id)
else if (feedRow.item.kind === "artist") QbzArtist.openArtist(feedRow.item.id)
else if (feedRow.item.kind === "playlist") QbzBridge.openPlaylist(feedRow.item.id)
else if (feedRow.item.kind === "label") QbzHome.openLabel(feedRow.item.id)
}
}
}
Text {
visible: feedRow.item.subtitle !== ""
width: parent.width
height: 16
text: feedRow.item.subtitle
color: (feedRow.item.artistId !== ""
&& (feedRow.item.kind === "track" || feedRow.item.kind === "album")
&& lrSubArea.containsMouse) ? theme.accent : theme.textMuted
font.pixelSize: 12
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
MouseArea {
id: lrSubArea
anchors.fill: parent
hoverEnabled: true
cursorShape: (feedRow.item.artistId !== ""
&& (feedRow.item.kind === "track" || feedRow.item.kind === "album"))
? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: if (feedRow.item.artistId !== "") QbzArtist.openArtist(feedRow.item.artistId)
}
}
}
// Col — type (icon + caps label).
Rectangle {
width: 116
height: parent.height
color: "transparent"
Row {
spacing: 6
anchors.verticalCenter: parent.verticalCenter
QbzIcon {
name: feedRow.item.kind === "track" ? "music"
: feedRow.item.kind === "playlist" ? "list-music"
: feedRow.item.kind === "artist" ? "user"
: feedRow.item.kind === "label" ? "disc-3" : "disc"
width: 13
height: 13
anchors.verticalCenter: parent.verticalCenter
tintName: "muted"
}
Text {
text: feedRow.item.kind === "track" ? QbzSession.tr("Track", QbzSession.trRev)
: feedRow.item.kind === "album" ? QbzSession.tr("Album", QbzSession.trRev)
: feedRow.item.kind === "artist" ? QbzSession.tr("Artist", QbzSession.trRev)
: feedRow.item.kind === "playlist" ? QbzSession.tr("Playlist", QbzSession.trRev)
: QbzSession.tr("Label", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 10
font.weight: theme.weightSemibold
font.letterSpacing: 1.2
verticalAlignment: Text.AlignVCenter
}
}
}
// Col — source glyph (only when local/Plex can appear).
Rectangle {
id: srcCol
visible: feedRow.view.showLocal
width: 44
height: parent.height
color: "transparent"
// Through controls/SourceIcon.qml, never QbzIcon: the Plex and
// Qobuz marks are MULTI-COLOUR and a tint flattens them to a
// silhouette. This column used to draw an accent-tinted
// `hard-drive` for Plex — a blue hard drive.
//
// DEVIATION, stated on purpose: the .slint has NO source column in
// this list at all (FavoritesView.slint:1767/2154 pass
// `show-source: false` to AlbumCollectionView). The numbers below
// are therefore its siblings' — AlbumListRow.slint:319, the row
// glyph, not the card badge.
SourceIcon {
visible: (feedRow.item.source || "") !== ""
&& feedRow.item.source !== "qobuz"
kind: feedRow.item.source || ""
glyphSize: 15
plexSize: 16
qobuzSize: 16
// Host is a transparent column over the theme row (NOT an
// artwork scrim), so this matches its siblings
// local/LocalAlbumRow.qml and local/LocalTrackRow.qml.
localTint: "muted"
anchors.verticalCenter: parent.verticalCenter
}
}
// Col — quality (albums + tracks).
Rectangle {
width: 150
height: parent.height
color: "transparent"
QualityMini {
visible: (feedRow.item.kind === "album" || feedRow.item.kind === "track")
&& feedRow.item.qualityTier !== ""
tier: feedRow.item.qualityTier
anchors.verticalCenter: parent.verticalCenter
}
}
// Col — favorite / follow indicator.
QbzIcon {
name: feedRow.item.kind === "artist" ? "user-plus"
: (feedRow.favorite ? "heart-filled" : "heart")
width: 18
height: 18
anchors.verticalCenter: parent.verticalCenter
tintName: "accent"
}
// Col — context-menu button.
Rectangle {
width: 30
height: 30
radius: 6
anchors.verticalCenter: parent.verticalCenter
color: lrMenuArea.containsMouse ? theme.surfaceElevated : "transparent"
QbzIcon { name: "ellipsis"; width: 16; height: 16; anchors.centerIn: parent; tintName: "secondary" }
MouseArea {
id: lrMenuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function (mouse) { feedRow.openLrMenu(lrMenuArea, mouse.x, mouse.y) }
}
}
}
// LAZY. This is a QtQuick.Controls Popup with a Repeater over its
// entries, and it was built EAGERLY for every row — so a long list
// constructed one whole popup per visible row, and rebuilt them all
// on every scroll step, to show a menu only ever opened on click.
// Same lazy-Loader idiom this file already uses for the clipboard
// helper and the info modal.
Loader {
id: lrMenuLoader
active: false
sourceComponent: CardMenu {
menuWidth: 196
entries: feedRow.item.kind === "track"
? feedRow.view.trackMenuModel(feedRow.item, feedRow.favorite)
: feedRow.item.kind === "album" ? [
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
{ "label": feedRow.favorite ? QbzSession.tr("Remove from Library", QbzSession.trRev) : QbzSession.tr("Add to Library", QbzSession.trRev),
"icon": feedRow.favorite ? "heart-filled" : "heart", "action": "favorite" },
]
: feedRow.item.kind === "artist" ? [
{ "label": QbzSession.tr("Go to artist", QbzSession.trRev), "icon": "user", "action": "go-artist" },
]
: [
{ "label": QbzSession.tr("Open", QbzSession.trRev), "icon": "list-music", "action": "open" },
{ "label": feedRow.favorite ? QbzSession.tr("Remove from Library", QbzSession.trRev) : QbzSession.tr("Add to Library", QbzSession.trRev),
"icon": feedRow.favorite ? "heart-filled" : "heart", "action": "favorite" },
]
onPicked: function (a) {
if (feedRow.item.kind === "track") { feedRow.view.trackAction(feedRow, a); return }
if (a === "open") {
if (feedRow.item.kind === "album") QbzAlbum.openAlbum(feedRow.item.id)
else if (feedRow.item.kind === "playlist") QbzBridge.openPlaylist(feedRow.item.id)
else if (feedRow.item.kind === "label") QbzHome.openLabel(feedRow.item.id)
} else if (a === "play") {
if (feedRow.item.kind === "album") QbzPlayer.playAlbum(feedRow.item.id)
} else if (a === "go-artist") {
QbzArtist.openArtist(feedRow.item.id)
} else if (a === "favorite") {
feedRow.toggleFavorite()
}
}
}
}
/// Build the popup on first use, then open it.
function openLrMenu(anchor, x, y) {
lrMenuLoader.active = true
lrMenuLoader.item.openAtCursor(anchor, x, y)
}
}