ONE component instantiated twice, because it is one form twice. Every difference between the protocols is a property set at the call site, so they are visible in one place instead of drifting across two files: "Test" proves the address for Jellyfin and only "a Subsonic server answered" for Subsonic (which has no credential-free endpoint), the password is discarded for Jellyfin and kept for Subsonic (no session, md5(password+salt) per request), and the sync cost is stated up front — 45.8 s per 4924 tracks against 0.81 s per 6678 — so a long sweep does not read as a hang. The credential never leaves Rust; the panel gets `hasCredential`. Assets: the colour marks for cards, the monochrome ones for rows. Both mono files arrived as BLACK ink and are invisible on a dark theme untinted, so they were normalised to the #ffffff master ink and joined `icon_tint_qt::MASTERS` — the port's only shader-free way to recolour an SVG per theme. The Jellyfin colour asset had a white rounded-rect plate behind the mark; it was stripped, because every other brand mark here is transparent and a white tile beside them reads as a rendering bug. Rendered with Qt itself and looked at before any of it was wired. CAVEAT, recorded in SourceIcon: the Subsonic mark is NAVIDROME's logo, and `subsonic` covers Gonic, Airsonic, Astiga and Ampache too. `ping` reports the server's own type and it is already persisted, so branching later is cheap — a known wrongness, not an unnoticed one. THREE BUGS THAT ONLY THE RUNNING WINDOW COULD SHOW, all found by driving it under Xvfb rather than by reading the log: 1. `MediaServerSettings is not a type` — a .qml missing from build.rs is not in the QML module. It resolves fine on disk, compiles nothing, and blanks the WHOLE settings page at runtime. All three existing audits passed. `qml_module_registration_audit.py` is the missing check and is now in the gate. 2. The toggle would not move. The panel reads its state from the settings DOCUMENT, unlike Plex's toggle which reads a bridge property, so every mutation has to republish it — connect, enable, disconnect and the end of a sweep now do. 3. Every Jellyfin album in the grid drew the LOCAL HARD-DRIVE badge: `local_rows::badge_source` had never been taught the two words and folded them into `_ => local`. Not cosmetic — that string is what the source chips filter on. `play_single_from_source` also grew the arm it needed: a media-server row from the recently-played rail has no library.db row to mint a LocalTrack from, so it resolves through `registry.tracks_of` instead. END TO END, from the UI, against the real server: toggle on -> type the address -> Connect -> "jellyfin sync: 500/4924 ... 4924 saved, 0 pruned, 4924 cached" -> Local Library shows 461 albums with their covers and the Jellyfin badge. The cached quality distribution is identical to what the server reported in the original protocol sweep. 428/428. cc
501 lines
22 KiB
QML
501 lines
22 KiB
QML
// MyQbzDetailRow — ONE item row of the My QBZ detail list, the port of the
|
||
// local `DetailRow` component in myqbz/MixtapeDetailView.slint:190-492.
|
||
//
|
||
// 8-column template, 1:1 with the .slint (:219-491): padding 12/12,
|
||
// spacing 12, cells 40 / 1fr / 140 / 80 / 160 / 72 / 60 / 40. Row height 56,
|
||
// bottom hairline `Theme.alpha-4`, hover/selected fill `surface-hover`.
|
||
//
|
||
// Plus a NINTH, leading 24px cell that no reference has: the accordion
|
||
// chevron (owner-authorised divergence, see its own comment below). It is
|
||
// mounted ONLY in the details arm (`showExpander`, owner 2026-07-31); when it
|
||
// is, it shifts the eight reference columns 36px right and `titleColWidth`
|
||
// plus the host's `ColHead` row are retuned together.
|
||
//
|
||
// --- TWO THINGS THAT ARE LOAD-BEARING -----------------------------------
|
||
//
|
||
// 1. Z-ORDER. `MixtapeDetailView.slint:203-217` documents the bug: the
|
||
// row-wide click target MUST sit BEHIND the per-cell hit areas (checkbox,
|
||
// artwork play, title, subtitle, ⋯) or it eats every one of their clicks
|
||
// and the whole row just opens the album. In QML declaration order IS
|
||
// z-order, so `rowArea` is declared FIRST and additionally pinned `z: -1`
|
||
// — the same belt-and-braces `rows/TrackRow.qml:644-652` uses.
|
||
//
|
||
// 2. `rev` IS A REAL DEPENDENCY OF EVERY `item` READ. The host holds the
|
||
// model array with a stable identity and PATCHES the row objects in place
|
||
// when Rust republishes a single-row change (`resolve_items`,
|
||
// `ensure_expanded`, a select toggle) — replacing the array would push a
|
||
// new one into `model:` and `QQuickItemView::setModel()` resets contentY
|
||
// to 0 (the analysis at `views/LibraryView.qml:174-221`). A plain JS
|
||
// object mutation fires NO notifier (`rows/TrackRow.qml:128-151` measured
|
||
// both halves), so each read below is routed through a declared property
|
||
// whose binding also reads `root.rev`. The `root.rev >= 0` guard is
|
||
// ALWAYS true — it exists only to make `rev` a captured dependency. Do
|
||
// not "simplify" it away.
|
||
//
|
||
// The row calls `QbzMyQbz` directly, exactly as the .slint calls
|
||
// `MyQbzDetailActions` directly — no signal relay.
|
||
//
|
||
// Deliberate deviations, all four documented in spec 01 §7.8: the shared
|
||
// `controls/CardMenu.qml` replaces the .slint's local `RowMenuItem`
|
||
// (33/r5/15px-glyph/8px-pad vs 32/r6/13/12; label rests `textSecondary` not
|
||
// `text-primary`; the icon tint tracks hover; a separator is a 7px band with
|
||
// a 1px `borderSubtle` rule instead of a bare 1px `surface-elevated` line).
|
||
// TRACK-RULES §5 (reuse before duplicate) outranks those deltas.
|
||
|
||
import QtQuick
|
||
import com.blitzfc.qbz
|
||
import "../../controls"
|
||
import "../../theme"
|
||
import "../local"
|
||
|
||
Rectangle {
|
||
id: root
|
||
|
||
/// One `DetailRow` object out of `detailJson.items` (spec 02 §5.2).
|
||
property var item: ({})
|
||
/// 1-based display ordinal among the VISIBLE rows (.slint `index`, :192).
|
||
/// Named `ordinal`, not `index`, so a delegate can pass `index + 1`
|
||
/// without shadowing the delegate's own context `index`.
|
||
property int ordinal: 0
|
||
property bool selectMode: false
|
||
/// Does THIS row carry the accordion chevron cell?
|
||
///
|
||
/// The accordion is the owner's feature, but it is scoped to the DETAILS
|
||
/// arm (the `expanded` segment) — the plain LIST arm never had a use for it
|
||
/// and the owner asked for it back out of there (2026-07-31). Off, the
|
||
/// leading 24px cell is `visible: false`, which in a `Row` drops the cell
|
||
/// AND its 12px gap, so the eight reference columns land exactly where
|
||
/// `MixtapeDetailView.slint:219-491` puts them again (`titleColWidth`
|
||
/// below switches back to the reference's 700 with it).
|
||
///
|
||
/// The STATE is untouched: `rowOpen` stays the one notion of open and Rust
|
||
/// stays its only writer. Rust also AND-s `rowOpen` with the details arm
|
||
/// (`myqbz_detail_qt.rs` `to_item` / `resync_row_open`), so with the chevron
|
||
/// gone from list mode no row renders open there and nothing is orphaned —
|
||
/// while the user's open set itself SURVIVES the round trip (it is persisted
|
||
/// per collection now, `collection_open_rows.json`).
|
||
property bool showExpander: false
|
||
/// The host's shared 3-phase clock for `QbzLoadingDots` (ONE timer per
|
||
/// view — spec 01 §7.5 / §15.5).
|
||
property int dotPhase: 0
|
||
/// In-place-patch revision. See note 2 in the header.
|
||
property int rev: 0
|
||
|
||
QbzTheme { id: theme }
|
||
|
||
// --- Every `item` read, made notifiable (header note 2) --------------
|
||
readonly property int itemPosition: root.rev >= 0 ? (root.item.position || 0) : 0
|
||
readonly property string itemType: root.rev >= 0 ? (root.item.itemType || "") : ""
|
||
readonly property string itemSource: root.rev >= 0 ? (root.item.source || "") : ""
|
||
readonly property string sourceItemId: root.rev >= 0 ? (root.item.sourceItemId || "") : ""
|
||
readonly property string rowTitle: root.rev >= 0 ? (root.item.title || "") : ""
|
||
readonly property string rowSubtitle: root.rev >= 0 ? (root.item.subtitle || "") : ""
|
||
readonly property bool subtitleIsLink: root.rev >= 0 && root.item.subtitleIsLink === true
|
||
readonly property string artistId: root.rev >= 0 ? (root.item.artistId || "") : ""
|
||
readonly property string sourceKind: root.rev >= 0 ? (root.item.sourceKind || "") : ""
|
||
readonly property string typeLabel: root.rev >= 0 ? (root.item.typeLabel || "") : ""
|
||
readonly property string qualityTier: root.rev >= 0 ? (root.item.qualityTier || "") : ""
|
||
readonly property string qualityDetail: root.rev >= 0 ? (root.item.qualityDetail || "") : ""
|
||
readonly property bool qualityResolving: root.rev >= 0 && root.item.qualityResolving === true
|
||
readonly property string tracksText: root.rev >= 0 ? (root.item.tracksText || "") : ""
|
||
readonly property string yearText: root.rev >= 0 ? (root.item.yearText || "") : ""
|
||
readonly property string artPath: root.rev >= 0 ? (root.item.artPath || "") : ""
|
||
readonly property bool selected: root.rev >= 0 && root.item.selected === true
|
||
/// Albums and playlists have an inline track list; a single track has
|
||
/// nothing to show, so it gets no chevron (Rust decides — `canExpand`).
|
||
readonly property bool canExpand: root.rev >= 0 && root.item.canExpand === true
|
||
/// THE accordion state, and the ONLY one: Rust derives it from the shared
|
||
/// `OPEN_ROWS` set — the chevron's own writes, persisted per collection —
|
||
/// so this row never needs to know what the view mode is.
|
||
readonly property bool rowOpen: root.rev >= 0 && root.item.rowOpen === true
|
||
|
||
/// track → music, playlist → list-music, else disc (.slint :263-267).
|
||
readonly property string typeGlyph: root.itemType === "track" ? "music"
|
||
: (root.itemType === "playlist" ? "list-music" : "disc")
|
||
|
||
// Hover: OR'd across the cell areas because a child MouseArea with
|
||
// `hoverEnabled` steals `containsMouse` from the row's own area — the
|
||
// same reason `rows/TrackRow.qml:213-214` OR's its four.
|
||
readonly property bool hovered: rowArea.containsMouse || artArea.containsMouse
|
||
|| titleArea.containsMouse || subArea.containsMouse || menuArea.containsMouse
|
||
|| chevArea.containsMouse
|
||
|
||
width: parent ? parent.width : 0
|
||
height: 56
|
||
color: (root.hovered || root.selected) ? theme.surfaceHover : "transparent"
|
||
|
||
/// Title column = rowWidth − padding(24) − fixed cells(592) − gaps(84)
|
||
/// = width − 700, the reference's own arithmetic (:219-491).
|
||
///
|
||
/// 736 while `showExpander`: the accordion chevron adds a NINTH cell (24px)
|
||
/// and a ninth 12px gap in front of the ordinal, shifting the 8-column
|
||
/// template 36px right. `MyQbzDetailView.qml`'s `ColHead` row carries the
|
||
/// identical leader and the identical switch, and the two must be changed
|
||
/// together or the header stops lining up with the rows.
|
||
readonly property int titleColWidth: Math.max(0,
|
||
root.width - (root.showExpander ? 736 : 700))
|
||
|
||
// Bottom hairline — `#ffffff0a` in the .slint (:200) == `alpha-4`, taken
|
||
// from the polarity-baked ramp so it is visible on light themes too. The
|
||
// `.length` guard covers the pre-publish frame QbzTheme documents.
|
||
Rectangle {
|
||
y: parent.height - 1
|
||
width: parent.width
|
||
height: 1
|
||
color: theme.alphaTiers.length > 0 ? theme.alphaTier(4)
|
||
: (theme.isDark ? "#0affffff" : "#0a000000")
|
||
}
|
||
|
||
// Row-wide target — FIRST + `z: -1` (header note 1). Right press opens
|
||
// the SAME menu at the pointer, the port's standing rule
|
||
// (`controls/QbzContextMenu.qml:20-22`); the .slint has no right-click
|
||
// here, so this is an additive port convention kept consistent with
|
||
// `rows/TrackRow.qml`.
|
||
MouseArea {
|
||
id: rowArea
|
||
anchors.fill: parent
|
||
z: -1
|
||
hoverEnabled: true
|
||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||
cursorShape: Qt.PointingHandCursor
|
||
onPressed: function (mouse) {
|
||
if (mouse.button === Qt.RightButton) { rowMenu.openAtCursor(rowArea, mouse.x, mouse.y); mouse.accepted = true }
|
||
}
|
||
onClicked: function (mouse) {
|
||
if (mouse.button !== Qt.LeftButton) return
|
||
if (root.selectMode) QbzMyQbz.detailToggleItemSelect(root.itemPosition)
|
||
else QbzMyQbz.openItem(root.itemSource, root.itemType, root.sourceItemId)
|
||
}
|
||
}
|
||
|
||
Row {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 12
|
||
anchors.rightMargin: 12
|
||
spacing: 12
|
||
|
||
// --- Col 0 (24) — THE ACCORDION CHEVRON, details arm only ---------
|
||
//
|
||
// ADDITIVE, and a deliberate divergence from BOTH references
|
||
// (owner-authorised 2026-07-30): neither `MixtapeDetailView.slint` nor
|
||
// the Tauri build ever had a per-row accordion — both render inline
|
||
// tracks only as a whole-list VIEW MODE, explicitly "no chevron". The
|
||
// owner asked for the accordion anyway. The view mode survives
|
||
// untouched but no longer means "open all" (owner 2026-08-01): the arm
|
||
// opens fully CLOSED and what the user opens here is remembered, per
|
||
// collection, across view switches and restarts.
|
||
//
|
||
// SCOPED to the details arm 2026-07-31 (`showExpander`): the plain LIST
|
||
// arm shows no chevron and is the reference's 8-column row again. Do
|
||
// not remove the accordion itself — only this gate is the owner's.
|
||
//
|
||
// The MouseArea is INVISIBLE on a non-expandable row, and an invisible
|
||
// item is not hit-tested — so a track row's chevron cell falls through
|
||
// to `rowArea` behind it and clicking there still opens the item, which
|
||
// is the gesture the owner asked to keep. On an expandable row this
|
||
// area sits at the default z, above `rowArea`'s `z: -1`, and swallows
|
||
// the click: the chevron toggles ONLY this row, it never navigates.
|
||
Item {
|
||
visible: root.showExpander
|
||
width: 24
|
||
height: parent.height
|
||
QbzIcon {
|
||
visible: root.canExpand
|
||
anchors.centerIn: parent
|
||
name: "chevron-right"
|
||
width: 14
|
||
height: 14
|
||
tintName: (root.rowOpen || chevArea.containsMouse) ? "textPrimary" : "muted"
|
||
// Points right when closed, down when open. `rotation` is a
|
||
// plain QQuickItem property and QbzIcon is an Image, so this
|
||
// costs no effect pass and no extra glyph.
|
||
rotation: root.rowOpen ? 90 : 0
|
||
Behavior on rotation {
|
||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||
}
|
||
}
|
||
MouseArea {
|
||
id: chevArea
|
||
visible: root.canExpand
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: QbzMyQbz.detailToggleRowExpand(root.sourceItemId)
|
||
}
|
||
}
|
||
|
||
// --- Col 1 (40) — ordinal / selection checkbox ------------------
|
||
Item {
|
||
width: 40
|
||
height: parent.height
|
||
Text {
|
||
visible: !root.selectMode
|
||
anchors.centerIn: parent
|
||
text: root.ordinal
|
||
color: theme.textMuted
|
||
font.pixelSize: 13
|
||
}
|
||
SelectCheck {
|
||
visible: root.selectMode
|
||
anchors.centerIn: parent
|
||
on: root.selected
|
||
onToggled: QbzMyQbz.detailToggleItemSelect(root.itemPosition)
|
||
}
|
||
}
|
||
|
||
// --- Col 2 (1fr) — artwork + title / subtitle -------------------
|
||
Row {
|
||
width: root.titleColWidth
|
||
height: parent.height
|
||
spacing: 12
|
||
|
||
Rectangle {
|
||
width: 36
|
||
height: 36
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
radius: 4
|
||
color: theme.surfaceElevated
|
||
clip: true
|
||
RoundedImage {
|
||
visible: root.artPath !== ""
|
||
anchors.fill: parent
|
||
source: root.artPath
|
||
radius: 4
|
||
}
|
||
QbzIcon {
|
||
visible: root.artPath === ""
|
||
anchors.centerIn: parent
|
||
name: root.typeGlyph
|
||
width: 18
|
||
height: 18
|
||
tintName: "muted"
|
||
}
|
||
// Hover play overlay (.slint :275-291).
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: "#99000000"
|
||
opacity: artArea.containsMouse ? 1.0 : 0.0
|
||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||
QbzIcon {
|
||
anchors.centerIn: parent
|
||
name: "play-fill"
|
||
width: 16
|
||
height: 16
|
||
tintName: "white"
|
||
}
|
||
}
|
||
MouseArea {
|
||
id: artArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: QbzMyQbz.playItem(root.sourceItemId)
|
||
}
|
||
}
|
||
|
||
Column {
|
||
width: Math.max(0, parent.width - 48)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 2
|
||
// Title — its OWN 18px hit area (.slint :298-313).
|
||
Item {
|
||
width: parent.width
|
||
height: 18
|
||
Text {
|
||
anchors.fill: parent
|
||
text: root.rowTitle
|
||
color: titleArea.containsMouse ? theme.accent : theme.textPrimary
|
||
font.pixelSize: 14
|
||
font.weight: theme.weightMedium
|
||
elide: Text.ElideRight
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
MouseArea {
|
||
id: titleArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: QbzMyQbz.openItem(root.itemSource, root.itemType, root.sourceItemId)
|
||
}
|
||
}
|
||
// Subtitle — 16px hit area, pointer only when it is a link.
|
||
Item {
|
||
visible: root.rowSubtitle !== ""
|
||
width: parent.width
|
||
height: 16
|
||
Text {
|
||
anchors.fill: parent
|
||
text: root.rowSubtitle
|
||
color: (root.subtitleIsLink && subArea.containsMouse)
|
||
? theme.accent : theme.textMuted
|
||
font.pixelSize: 12
|
||
elide: Text.ElideRight
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
MouseArea {
|
||
id: subArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: root.subtitleIsLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||
onClicked: {
|
||
if (root.subtitleIsLink)
|
||
QbzMyQbz.openArtist(root.itemSource, root.rowSubtitle, root.artistId)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Col 3 (140) — type ----------------------------------------
|
||
Item {
|
||
width: 140
|
||
height: parent.height
|
||
Row {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 6
|
||
QbzIcon {
|
||
name: root.typeGlyph
|
||
width: 13
|
||
height: 13
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
tintName: "muted"
|
||
}
|
||
Text {
|
||
text: root.typeLabel
|
||
color: theme.textMuted
|
||
font.pixelSize: 10
|
||
font.weight: theme.weightSemibold
|
||
font.letterSpacing: 1.2
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Col 4 (80) — source glyph (the `!== ""` gate is the .slint's,
|
||
// :370) ----------------------------------------------------------
|
||
Item {
|
||
width: 80
|
||
height: parent.height
|
||
SourceIcon {
|
||
visible: root.sourceKind !== ""
|
||
x: 0
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
kind: root.sourceKind
|
||
// A dense ROW: the media marks draw monochrome and tinted, like the
|
||
// hard-drive beside them. Colour logos are for cards — a list of
|
||
// them fights the text it labels.
|
||
mono: true
|
||
// Row glyph, so SourceGlyph.slint's numbers (:31 — 15px, the
|
||
// three Qobuz kinds 16px, muted local tint), not the version
|
||
// picker's flat-14 defaults.
|
||
glyphSize: 15
|
||
qobuzSize: 16
|
||
localTint: "muted"
|
||
}
|
||
}
|
||
|
||
// --- Col 5 (160) — quality ---------------------------------------
|
||
Item {
|
||
width: 160
|
||
height: parent.height
|
||
QualityBadgeFull {
|
||
visible: root.qualityTier !== ""
|
||
x: 0
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
tier: root.qualityTier
|
||
detail: root.qualityDetail
|
||
}
|
||
// The .slint's `LoadingDots` inherits Rectangle, so it FILLS this
|
||
// 160px cell and centres its dot row in it (LoadingDots.slint
|
||
// :26-35) — hence `anchors.fill`, not `x: 0`.
|
||
QbzLoadingDots {
|
||
visible: root.qualityTier === "" && root.qualityResolving
|
||
anchors.fill: parent
|
||
phase: root.dotPhase
|
||
}
|
||
}
|
||
|
||
// --- Col 6 (72) — tracks, right-aligned --------------------------
|
||
Text {
|
||
width: 72
|
||
height: parent.height
|
||
text: root.tracksText
|
||
color: theme.textSecondary
|
||
font.pixelSize: theme.fontLegal
|
||
horizontalAlignment: Text.AlignRight
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
// --- Col 7 (60) — year, right-aligned ----------------------------
|
||
Text {
|
||
width: 60
|
||
height: parent.height
|
||
text: root.yearText
|
||
color: theme.textSecondary
|
||
font.pixelSize: theme.fontLegal
|
||
horizontalAlignment: Text.AlignRight
|
||
verticalAlignment: Text.AlignVCenter
|
||
}
|
||
|
||
// --- Col 8 (40) — ⋯ menu -----------------------------------------
|
||
Item {
|
||
width: 40
|
||
height: parent.height
|
||
Rectangle {
|
||
id: menuBtn
|
||
x: parent.width - width
|
||
width: 24
|
||
height: 24
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
radius: 4
|
||
color: menuArea.containsMouse ? theme.surfaceHover : "transparent"
|
||
QbzIcon {
|
||
anchors.centerIn: parent
|
||
name: "ellipsis"
|
||
width: 14
|
||
height: 14
|
||
tintName: menuArea.containsMouse ? "textPrimary" : "muted"
|
||
}
|
||
MouseArea {
|
||
id: menuArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
// Slint anchors the 184px panel `x: btn.width - 184;
|
||
// y: 28` on this 24px button (:438-439) — which is exactly
|
||
// what `openBelowRight` computes (24 + 4 = 28).
|
||
onClicked: rowMenu.openBelowRight(menuBtn)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Row ⋯ menu (.slint :437-488), 184px, in the .slint's order.
|
||
CardMenu {
|
||
id: rowMenu
|
||
menuWidth: 184
|
||
entries: root.rowMenuModel()
|
||
onPicked: function (a) { root.rowMenuAction(a) }
|
||
}
|
||
|
||
function rowMenuModel() {
|
||
var t = QbzSession.tr
|
||
var r = QbzSession.trRev
|
||
return [
|
||
{ "label": t("Play", r), "icon": "play-fill", "action": "play" },
|
||
{ "label": t("Play next", r), "icon": "list-start", "action": "play-next" },
|
||
{ "label": t("Play later", r), "icon": "list-plus", "action": "play-later" },
|
||
{ "label": t("Add to queue", r), "icon": "list-end", "action": "add-to-queue" },
|
||
{ "sep": true },
|
||
{ "label": t("Remove from collection", r), "icon": "trash-2",
|
||
"action": "remove", "danger": true },
|
||
]
|
||
}
|
||
|
||
function rowMenuAction(a) {
|
||
if (a === "play") QbzMyQbz.playItem(root.sourceItemId)
|
||
else if (a === "remove") QbzMyQbz.removeItem(root.itemPosition)
|
||
else QbzMyQbz.itemAction(root.sourceItemId, a)
|
||
}
|
||
}
|