Two fronts of the modularization, both gated green before this commit. QML leaves the flat qml/ directory for seven modules: cards, controls, rows, settings, shell, theme, views. Only Main.qml and LoginScreen.qml stay at the root. build.rs matches the tree exactly (55 files), relative imports and asset paths were re-anchored for the new depth. The QbzBridge God-object starts breaking up: QbzSession, QbzShell, QbzPlayer, QbzQueue and QbzHome are now their own #[cxx_qt::bridge] singletons, each with its own OnceLock<CxxQtThread> and ui() hop, booted in order from Main.qml. Bridge files stay directly under src/ because cxx-qt-build only accepts rust_files from one directory (QTBUG-93443). bridge.rs keeps 5.5 domains (library, album/artist, settings, integrations, search, playlist) and dies once they are split out. cc
404 lines
16 KiB
QML
404 lines
16 KiB
QML
// THE shared album card — QML replica of discover/AlbumCard.slint, used
|
|
// by BOTH the Home rails and the Library All grid (the Slint mounts the
|
|
// same component in both places).
|
|
//
|
|
// 200x246: 200px artwork (Radius.sm) + placeholder, hover scrim with
|
|
// genre/year meta, pin badge (top-right), favorite / play / more overlay
|
|
// buttons, award ribbon, source badge (opt-in), then the title/artist
|
|
// lines with the icon-only quality badge.
|
|
//
|
|
// Live wiring: play (art click + overlay play), favorite heart (optimistic
|
|
// + signal), pin badge (pinned store), ⋯ context menu (Play / Play next /
|
|
// Add to queue wired through playback_qt; favorite wired; Open album +
|
|
// Block inert — POC-NOTEs: no album page / blacklist store).
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
// --- card data (the ONE contract both hosts fill) --------------------
|
|
property string albumId: ""
|
|
property string title: ""
|
|
property string artist: ""
|
|
property string artistId: ""
|
|
property string genre: ""
|
|
property string year: ""
|
|
property string qualityTier: ""
|
|
property string ribbon: ""
|
|
property string ribbonKind: ""
|
|
// Artwork image source (file://… or "") — the host's cache lookup.
|
|
property string artSource: ""
|
|
property bool isFavorite: false
|
|
property bool isPinned: false
|
|
// Source badge (Library show-local): "local" | "plex" | "" (hidden).
|
|
property string source: ""
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
width: 200
|
|
height: 246
|
|
color: "transparent"
|
|
|
|
readonly property bool overlayOn: artArea.containsMouse || pinArea.containsMouse
|
|
|| favArea.containsMouse || playArea.containsMouse || moreArea.containsMouse
|
|
|
|
function toggleFavorite() {
|
|
root.isFavorite = !root.isFavorite
|
|
QbzBridge.libraryToggleFavorite("album", root.albumId)
|
|
}
|
|
function togglePin() {
|
|
root.isPinned = !root.isPinned
|
|
QbzBridge.togglePin("album", root.albumId, root.title, root.artist, "")
|
|
}
|
|
|
|
Column {
|
|
spacing: 0
|
|
|
|
// --- Artwork + hover overlay -----------------------------------
|
|
Rectangle {
|
|
width: 200
|
|
height: 200
|
|
radius: theme.radiusSm
|
|
color: theme.surfaceElevated
|
|
clip: true
|
|
|
|
RoundedImage {
|
|
anchors.fill: parent
|
|
source: root.artSource
|
|
radius: theme.radiusSm
|
|
}
|
|
|
|
// Hover scrim.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: theme.radiusSm
|
|
color: "#000000"
|
|
opacity: root.overlayOn ? 0.6 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
|
}
|
|
|
|
// Hover meta — genre + year, top-left.
|
|
Column {
|
|
x: 12
|
|
y: 12
|
|
spacing: 2
|
|
opacity: root.overlayOn ? 1.0 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
|
Text {
|
|
visible: root.genre !== ""
|
|
text: root.genre
|
|
height: 20
|
|
color: "#ebffffff"
|
|
font.pixelSize: 13
|
|
font.weight: theme.weightBold
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
Text {
|
|
visible: root.year !== ""
|
|
text: root.year
|
|
height: 17
|
|
color: "#ccffffff"
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
|
|
// Card-open + hover detector (declared before the action
|
|
// buttons so those win the pointer).
|
|
MouseArea {
|
|
id: artArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
// Phase 8: the card opens the album view (the overlay play
|
|
// button carries the play affordance).
|
|
onClicked: QbzBridge.openAlbum(root.albumId)
|
|
}
|
|
|
|
// Pin badge — top-right. Hover-revealed like the overlay
|
|
// buttons (AlbumCard.slint: opacity follows overlay-on even
|
|
// when pinned — the pinned state reads in the icon swap only:
|
|
// filled accent pin vs outline). Always-mounted (opacity) so
|
|
// its hover joins overlayOn.
|
|
Rectangle {
|
|
x: parent.width - width - 8
|
|
y: 8
|
|
width: 26
|
|
height: 26
|
|
radius: 13
|
|
color: pinArea.containsMouse ? "#cc000000" : "#99000000"
|
|
opacity: root.overlayOn ? 1.0 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
|
QbzIcon {
|
|
name: root.isPinned ? "pin-filled" : "pin"
|
|
width: 14
|
|
height: 14
|
|
anchors.centerIn: parent
|
|
tintName: root.isPinned ? "accent" : "primary"
|
|
}
|
|
MouseArea {
|
|
id: pinArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.togglePin()
|
|
}
|
|
}
|
|
|
|
// Hover action buttons — favorite / play / more (y=120, h=44,
|
|
// centered, spacing 12).
|
|
Row {
|
|
y: 120
|
|
width: parent.width
|
|
height: 44
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: 12
|
|
opacity: root.overlayOn ? 1.0 : 0.0
|
|
Behavior on opacity { NumberAnimation { duration: 150 } }
|
|
|
|
Item { width: (200 - 44 - 2 * 36 - 2 * 12) / 2; height: 1 }
|
|
Rectangle {
|
|
width: 36
|
|
height: 36
|
|
radius: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: favArea.containsMouse ? "#3dffffff" : "#24ffffff"
|
|
border.width: 1.5
|
|
border.color: "#ccffffff"
|
|
QbzIcon {
|
|
name: root.isFavorite ? "heart-filled" : "heart"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: root.isFavorite ? "favorite" : "primary"
|
|
}
|
|
MouseArea {
|
|
id: favArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.toggleFavorite()
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 44
|
|
height: 44
|
|
radius: 22
|
|
color: playArea.containsMouse ? "#d6ffffff" : "#ffffff"
|
|
QbzIcon {
|
|
name: "play-fill"
|
|
width: 18
|
|
height: 18
|
|
anchors.centerIn: parent
|
|
tintName: "black"
|
|
}
|
|
MouseArea {
|
|
id: playArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzPlayer.playAlbum(root.albumId)
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: 36
|
|
height: 36
|
|
radius: 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: moreArea.containsMouse ? "#3dffffff" : "#24ffffff"
|
|
border.width: 1.5
|
|
border.color: "#ccffffff"
|
|
QbzIcon {
|
|
name: "ellipsis"
|
|
width: 16
|
|
height: 16
|
|
anchors.centerIn: parent
|
|
tintName: "primary"
|
|
}
|
|
MouseArea {
|
|
id: moreArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: function (mouse) { albumMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
|
}
|
|
}
|
|
Item { width: (200 - 44 - 2 * 36 - 2 * 12) / 2; height: 1 }
|
|
}
|
|
|
|
// Context menu (AlbumCard.slint's album-menu): 196px, items
|
|
// 33px, icon 15 + label 13.
|
|
QbzContextMenu {
|
|
id: albumMenu
|
|
menuWidth: 196
|
|
Repeater {
|
|
model: [
|
|
{ "label": QbzSession.tr("Open album", QbzSession.trRev), "icon": "library-big", "action": "open" },
|
|
{ "label": QbzSession.tr("Play", QbzSession.trRev), "icon": "play-fill", "action": "play" },
|
|
{ "label": QbzSession.tr("Play next", QbzSession.trRev), "icon": "list-plus", "action": "next" },
|
|
{ "label": QbzSession.tr("Add to queue", QbzSession.trRev), "icon": "list-end", "action": "queue" },
|
|
{ "label": root.isFavorite ? QbzSession.tr("Remove from Library", QbzSession.trRev) : QbzSession.tr("Add to Library", QbzSession.trRev), "icon": root.isFavorite ? "heart-filled" : "heart", "action": "favorite" },
|
|
{ "label": QbzSession.tr("Block this album", QbzSession.trRev), "icon": "blind-eye", "action": "block" },
|
|
]
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
width: parent ? parent.width : 0
|
|
height: 33
|
|
radius: 5
|
|
color: miArea.containsMouse ? theme.surfaceHover : "transparent"
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
spacing: 8
|
|
QbzIcon {
|
|
name: modelData.icon
|
|
width: 15
|
|
height: 15
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
tintName: "secondary"
|
|
}
|
|
Text {
|
|
height: parent.height
|
|
width: parent.width - 23
|
|
text: modelData.label
|
|
color: theme.textSecondary
|
|
font.pixelSize: 13
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
MouseArea {
|
|
id: miArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
albumMenu.close()
|
|
var a = modelData.action
|
|
if (a === "open") QbzBridge.openAlbum(root.albumId)
|
|
else if (a === "play") QbzPlayer.playAlbum(root.albumId)
|
|
else if (a === "next") QbzPlayer.enqueueAlbum(root.albumId, "next")
|
|
else if (a === "queue") QbzPlayer.enqueueAlbum(root.albumId, "later")
|
|
else if (a === "favorite") root.toggleFavorite()
|
|
// "block" (no blacklist store): inert —
|
|
// POC-NOTE.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Award ribbon — content-width, capped at the card width.
|
|
Rectangle {
|
|
visible: root.ribbon !== ""
|
|
x: 0
|
|
y: parent.height - height - 8
|
|
height: 20
|
|
width: Math.min(ribbonRow.width, 200)
|
|
color: root.ribbonKind === "press" ? "#d49511" : "#e0000000"
|
|
topRightRadius: 3
|
|
bottomRightRadius: 3
|
|
clip: true
|
|
Rectangle {
|
|
width: root.ribbonKind === "press" ? 0 : 3
|
|
height: parent.height
|
|
color: root.ribbonKind === "qobuzissime" ? "#8b5cf6" : "#eab308"
|
|
}
|
|
Row {
|
|
id: ribbonRow
|
|
height: parent.height
|
|
leftPadding: 10
|
|
rightPadding: 10
|
|
width: ribbonText.implicitWidth + 20
|
|
Text {
|
|
id: ribbonText
|
|
height: parent.height
|
|
text: root.ribbon
|
|
color: root.ribbonKind === "press" ? "#1f1407" : "#ffffff"
|
|
font.pixelSize: 9
|
|
font.weight: theme.weightSemibold
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
// Source badge (Library show-local): bottom-right of the art.
|
|
Rectangle {
|
|
visible: root.source === "local" || root.source === "plex"
|
|
x: parent.width - width - 6
|
|
y: parent.height - height - 6
|
|
width: 24
|
|
height: 24
|
|
radius: 4
|
|
color: "#b3000000"
|
|
QbzIcon {
|
|
name: "hard-drive"
|
|
width: 14
|
|
height: 14
|
|
anchors.centerIn: parent
|
|
tintName: root.source === "plex" ? "accent" : "primary"
|
|
}
|
|
}
|
|
}
|
|
Item { width: 1; height: 6 }
|
|
|
|
// --- Title / artist + quality badge ------------------------------
|
|
Row {
|
|
width: 200
|
|
height: 40
|
|
spacing: theme.spacingSm
|
|
Column {
|
|
width: parent.width - (qBadge.visible ? qBadge.width + theme.spacingSm : 0)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
Text {
|
|
width: parent.width
|
|
height: 20
|
|
text: root.title
|
|
color: titleArea.containsMouse ? theme.accent : theme.textPrimary
|
|
font.pixelSize: theme.fontBody - 2
|
|
font.weight: theme.weightMedium
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
MouseArea {
|
|
id: titleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: QbzBridge.openAlbum(root.albumId)
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
height: 18
|
|
text: root.artist
|
|
color: root.artistId !== "" && artistArea.containsMouse
|
|
? theme.textPrimary : theme.textMuted
|
|
font.pixelSize: theme.fontLink - 1
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
MouseArea {
|
|
id: artistArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: root.artistId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: if (root.artistId !== "") QbzBridge.openArtist(root.artistId)
|
|
}
|
|
}
|
|
}
|
|
// Icon-only quality badge (QualityBadge.slint).
|
|
QualityMini {
|
|
id: qBadge
|
|
tier: root.qualityTier
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|