178 lines
6.6 KiB
QML
178 lines
6.6 KiB
QML
// Track Info modal — 1:1 QML port of crates/qbz-ui/ui/album/TrackInfoModal.slint.
|
|
//
|
|
// HOW IT OPENS (taken from the .slint, not chosen): a MODAL — a full-window
|
|
// #000000bf scrim with a centered card, NOT a flyout and NOT a docked panel.
|
|
// Slint mounts it in AppShell as `Rectangle scrim + centered card` gated on
|
|
// TrackInfoState.open; Escape and a scrim click close it (ADR-009: modal
|
|
// z >= 3000). Here it is a Popup parented to `Overlay.overlay` so the bars can
|
|
// own it without AppShell.qml having to mount anything (the NowPlayingBar
|
|
// Loader guarantees only ONE bar — hence only one instance — is alive).
|
|
//
|
|
// Geometry, verbatim from the .slint:
|
|
// card width = min(window - 40, 728)
|
|
// card height = min(window * 0.8, content preferred height)
|
|
// credits col = (card width - 48 /*content padding*/ - 24 /*gutter*/) / 2
|
|
// metadata col = (card width - 48 - 2 * 24) / 3 (three equal columns)
|
|
// scrollbar = 14px, inset 4px from the card's right edge
|
|
// The Slint card carries drop-shadow-blur 32px; QML shadows need an effect
|
|
// (Qt5Compat / MultiEffect) and it is dropped here rather than faked.
|
|
// SUPERSEDED (2026-07-29): the reason used to be "effects render NOTHING on the
|
|
// software path in this port". Effects need shaders, and this port runs on the
|
|
// GPU (OpenGL RHI, measured); that note came from an offscreen session, which
|
|
// forces the software renderer by definition — theme/RoundedImage.qml now
|
|
// detects the software path with `GraphicsInfo.api` rather than assuming it.
|
|
// The shadow is still not added: it is a visual change owing its own parity
|
|
// pass against Slint's blur, not a perf fix.
|
|
//
|
|
// DATA: QbzAlbum.openTrackInfo(trackId) fetches and publishes
|
|
// QbzAlbum.trackInfoJson while QbzAlbum.trackInfoLoading is true. Document
|
|
// shape (1:1 with crates/qbz/src/info_modals.rs `TrackInfoData`):
|
|
// { "error": "", "title": "", "album": "", "artist": "", "artistId": "",
|
|
// "duration": "3:45", "quality": "24-bit / 96kHz", "isrc": "",
|
|
// "label": "", "labelId": "", "copyright": "",
|
|
// "credits": [ { "role": "PRODUCER", "roleRaw": "Producer",
|
|
// "names": ["A", "B"] } ] }
|
|
// Every read is guarded, so while the glue is missing the modal degrades to
|
|
// its (already 1:1) loading state instead of throwing.
|
|
//
|
|
// Artist, label and musician links all route through their live Qt bridges.
|
|
//
|
|
// Split (project size rule): the scrolling content is TrackInfoBody.qml and
|
|
// the two cell primitives are InfoMetaCell.qml / InfoCreditCell.qml — the
|
|
// .slint exports those two as components as well.
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Popup {
|
|
id: root
|
|
|
|
parent: Overlay.overlay
|
|
x: 0
|
|
y: 0
|
|
width: parent ? parent.width : 0
|
|
height: parent ? parent.height : 0
|
|
padding: 0
|
|
z: 3000
|
|
modal: true
|
|
// Our own scrim (the Slint's #000000bf) — the default modal dimmer would
|
|
// darken it twice.
|
|
dim: false
|
|
closePolicy: Popup.CloseOnEscape
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// --- Data ------------------------------------------------------------
|
|
readonly property var doc: parseDoc()
|
|
function parseDoc() {
|
|
try {
|
|
return JSON.parse(QbzAlbum.trackInfoJson || "{}")
|
|
} catch (e) {
|
|
return ({})
|
|
}
|
|
}
|
|
readonly property bool loading: {
|
|
try {
|
|
return QbzAlbum.trackInfoLoading === true
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
readonly property string errorText: doc.error || ""
|
|
readonly property var credits: doc.credits || []
|
|
// Even index -> left column, odd -> right (info_modals.rs step_by(2)).
|
|
readonly property var creditsLeft: credits.filter(function (c, i) { return i % 2 === 0 })
|
|
readonly property var creditsRight: credits.filter(function (c, i) { return i % 2 === 1 })
|
|
|
|
// --- Actions ---------------------------------------------------------
|
|
|
|
/// Open the modal for a track id — the Slint's media-action("track", id,
|
|
/// "track-info"). The caller does the Qobuz-only gating.
|
|
function openFor(trackId) {
|
|
if (!trackId || trackId === "")
|
|
return
|
|
QbzAlbum.openTrackInfo(trackId)
|
|
open()
|
|
}
|
|
|
|
function openArtist(artistId) {
|
|
if (!artistId || artistId === "")
|
|
return
|
|
close()
|
|
QbzArtist.openArtist(artistId)
|
|
}
|
|
function openLabel(labelId) {
|
|
if (!labelId || labelId === "")
|
|
return
|
|
close()
|
|
QbzHome.openLabel(labelId)
|
|
}
|
|
// Musician click — consumer #5 of six (contract §2.2). This was an empty
|
|
// stub: the name hovered like a link and did nothing.
|
|
//
|
|
// CLOSE ORDER IS LOAD-BEARING, and it is the reverse of what reads
|
|
// naturally: the reference dispatches FIRST and closes AFTER
|
|
// (TrackInfoModal.svelte:20-27). Closing first would destroy this modal —
|
|
// and, for a `weak`/`none` result, the global MusicianModal is summoned
|
|
// by the very click that would have killed its opener. Dispatch, then close.
|
|
function openMusician(name, roleRaw) {
|
|
if (!name || name === "")
|
|
return
|
|
QbzArtist.resolveMusician(name, roleRaw || "")
|
|
close()
|
|
}
|
|
|
|
background: Rectangle { color: "#bf000000" }
|
|
|
|
contentItem: Item {
|
|
|
|
// Scrim — click outside dismisses.
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.close()
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
width: Math.min(root.width - 40, 728)
|
|
height: Math.min(root.height * 0.8, body.implicitHeight)
|
|
x: Math.round((parent.width - width) / 2)
|
|
y: Math.round((parent.height - height) / 2)
|
|
radius: theme.radiusMd
|
|
color: theme.surfaceCard
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
clip: true
|
|
|
|
// Swallow clicks so they don't reach the scrim.
|
|
MouseArea { anchors.fill: parent }
|
|
|
|
Flickable {
|
|
id: flick
|
|
anchors.fill: parent
|
|
contentWidth: width
|
|
contentHeight: body.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
clip: true
|
|
|
|
TrackInfoBody {
|
|
id: body
|
|
host: root
|
|
width: flick.width
|
|
}
|
|
}
|
|
|
|
// The Slint mounts a 14px ListScrollbar inset 4px from the right.
|
|
QbzScrollBar {
|
|
target: flick
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
}
|
|
}
|
|
}
|
|
}
|