Files
qbz/crates/qbz-qt/qml/controls/CardMenu.qml
T
vicrodh 1817de0743 feat(qt): release-aware album links, an unavailable-release page, Copy submenus and Buy on Qobuz
Unavailable tracks (8): playlist rows now say when the WHOLE release is
gone (`releaseUnavailable`, one probe per distinct album of the pulled rows
through the library's helper); TrackRow's album link and "Go to album" go
dark on it instead of opening an empty page. Every row that opens an album
passes the names it knows (`openAlbumFrom`), so when the catalog answers
404 the page names the release: AlbumView renders an "unavailable" arm —
heading, "This release is no longer available on Qobuz." — with the
best-ranked alternatives Qobuz still sells (the replacement flow's ranking,
exposed as `ranked_alternatives`) in a rail; a network failure gets the
same page with "Couldn't load this release." and no rail.

Copy (9): the player bar's "+" and the track row menus (shared TrackRow
and the artist's Popular Tracks rows) gain a Copy entry with a submenu —
"Track name" or "Track - Album - Artist" — through the shared clipboard
carrier; CardMenu marks `submenu: true` entries with a chevron and the
host reopens the second menu on the same anchor. Row menus keep both lazy.

Buy on Qobuz (10): the album header shows a store action when the catalog
says the release is purchasable and it is not already owned; the track
menus and the player bar carry the same entry for catalog rows and resolve
it at click time — the store page opens in the browser, or a toast says
the release is not sold. The link is `https://www.qobuz.com/album/-/{id}`,
which the store answers with a 301 to the localised page (verified).

cc
2026-09-13 20:26:11 -06:00

120 lines
4.8 KiB
QML

// CardMenu — THE shared ⋯ / right-click menu surface (promoted from
// LibraryView in phase 21, brought up to primitives/ContextMenuItem.slint
// in the menu-parity round).
//
// Driven by an `entries` model; emits picked(action). Entry shape:
//
// { label, icon, action } a normal row (33px)
// { ..., enabled: false } muted row, ignores clicks
// (ContextMenuItem.slint `enabled`)
// { ..., danger: true } destructive row, red label
// (ContextMenuItem.slint `danger`)
// { sep: true } 1px group separator + 3px air
// (AlbumContextMenu.slint's
// `Rectangle { height: 1px; }`)
//
// Row metrics are ContextMenuItem.slint's: 33px, icon 15px, label 13px,
// and BOTH go text-primary on hover (the .slint tints icon + label
// together; the old POC row kept them permanently secondary).
//
// DEVIATION, deliberate: a `danger` row reddens the LABEL only. Slint
// tints the icon with Theme.danger at render time; QML tinting here is
// pre-baked per tint directory (assets/icons/<tint>/) and there is no
// `danger` bake — inventing one needs a new asset set + build.rs glue, and
// a missing pre-baked file renders NOTHING at runtime. The red label
// carries the signal on its own.
import QtQuick
import com.blitzfc.qbz
import "../theme"
QbzContextMenu {
id: cmRoot
property var entries: []
signal picked(string action)
QbzTheme { id: theme }
Repeater {
model: cmRoot.entries
delegate: Rectangle {
id: row
required property var modelData
readonly property bool isSep: modelData.sep === true
readonly property bool rowEnabled: modelData.enabled !== false
readonly property bool isDanger: modelData.danger === true
readonly property bool hot: cmiArea.containsMouse && rowEnabled
width: parent ? parent.width : 0
height: isSep ? 7 : (cmRoot.kioskHost ? 44 : 33)
radius: isSep ? 0 : 5
color: hot ? theme.surfaceHover : "transparent"
// ContextMenuItem.slint: `opacity: enabled ? 1.0 : 0.4`.
opacity: (isSep || rowEnabled) ? 1.0 : 0.4
// Separator: a 1px rule centred in the 7px band.
Rectangle {
visible: row.isSep
y: 3
width: parent.width
height: 1
color: theme.borderSubtle
}
Row {
visible: !row.isSep
anchors.fill: parent
anchors.leftMargin: 8
spacing: 8
QbzIcon {
name: row.modelData.icon || ""
width: 15
height: 15
anchors.verticalCenter: parent.verticalCenter
// Host is `surfaceHover`/transparent over a theme surface,
// so the glyph tracks the sibling label (textPrimary/
// textSecondary below) rather than a fixed white.
tintName: row.hot ? "textPrimary" : "secondary"
}
Text {
height: parent.height
width: parent.width - 23 - (row.modelData.submenu === true ? 22 : 0)
text: row.modelData.label || ""
color: row.isDanger ? theme.danger
: (row.hot ? theme.textPrimary : theme.textSecondary)
font.pixelSize: cmRoot.kioskHost ? 16 : 13
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
// Submenu marker (`submenu: true`): the host answers the pick
// by opening a second menu on the same anchor.
QbzIcon {
visible: row.modelData.submenu === true
name: "chevron-right"
width: 14
height: 14
anchors.verticalCenter: parent.verticalCenter
tintName: row.hot ? "textPrimary" : "muted"
}
}
MouseArea {
id: cmiArea
anchors.fill: parent
// A separator and a muted row take no input at all — a
// disabled MouseArea also reports no hover, so neither
// highlights.
enabled: !row.isSep && row.rowEnabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
cmRoot.close()
cmRoot.picked(row.modelData.action)
}
}
}
}
}