feat(orbit): add shared library foundations and gated host tools
Extract the HTTP host and catalog layer into qbz-control, preserve the existing daemon API, and keep private session credentials out of SSE. Scope library stores and scan workers to their host. Share bounded source search and show Local results alongside Qobuz in the Qt search page. Add the launch-gated Qt Orbit laboratory and persistent remote-context banner. Let qbzd run --orbit own and administer its file library through instance-scoped API and CLI operations. Full remote UI control remains pending. Validate library/control/daemon suites, Qt search and QML regressions, two isolated daemon processes, Qt release build and final cargo check. cx
This commit is contained in:
75 files changed
+6437
-1470
No files matched your search
@@ -377,6 +377,7 @@ ApplicationWindow {
|
||||
QbzMusician.boot()
|
||||
QbzLyrics.boot()
|
||||
QbzCast.boot()
|
||||
QbzOrbit.boot()
|
||||
QbzBridge.boot()
|
||||
// MyQBZ (two grids + detail + modals), the app-wide Add picker, the
|
||||
// Artist-Collection builder, and the Blacklist manager.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Experimental host inspection, constructed only when --orbit is present.
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
id: root
|
||||
property bool kioskHost: false
|
||||
property var doc: ({})
|
||||
property string listenAddress: "127.0.0.1:17290"
|
||||
property string peerAddress: "http://127.0.0.1:17290"
|
||||
property string accessKey: ""
|
||||
property string query: ""
|
||||
spacing: 4
|
||||
QbzTheme { id: theme }
|
||||
|
||||
function reload() {
|
||||
try { doc = JSON.parse(QbzOrbit.stateJson) } catch (e) { doc = ({}) }
|
||||
}
|
||||
Component.onCompleted: reload()
|
||||
Connections {
|
||||
target: QbzOrbit
|
||||
function onStateJsonChanged() { root.reload() }
|
||||
}
|
||||
function statusText() {
|
||||
switch (doc.status || "") {
|
||||
case "verified": return QbzSession.tr("Host verified", QbzSession.trRev)
|
||||
case "not-supported": return QbzSession.tr("This host does not support library inspection.", QbzSession.trRev)
|
||||
case "invalid-address": return QbzSession.tr("Enter a valid host address.", QbzSession.trRev)
|
||||
case "listen-failed": return QbzSession.tr("Could not start the Orbit listener.", QbzSession.trRev)
|
||||
case "host-changed": return QbzSession.tr("The host changed. Verify it again.", QbzSession.trRev)
|
||||
case "host-unavailable": return QbzSession.tr("Library unavailable", QbzSession.trRev)
|
||||
case "verify-failed": return QbzSession.tr("Could not verify the host.", QbzSession.trRev)
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader { kioskHost: root.kioskHost; text: "ORBIT" }
|
||||
Text {
|
||||
width: parent.width
|
||||
text: QbzSession.tr("Orbit is experimental. Verifying a host does not switch playback or settings.", QbzSession.trRev)
|
||||
wrapMode: Text.WordWrap
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontBody
|
||||
}
|
||||
SettingsSpacer {}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
label: QbzSession.tr("Listening address", QbzSession.trRev)
|
||||
QbzLineEdit {
|
||||
kioskHost: root.kioskHost
|
||||
enabled: !root.doc.listening && !root.doc.starting
|
||||
text: root.listenAddress
|
||||
onEdited: function(value) { root.listenAddress = value }
|
||||
onCommitted: function(value) { root.listenAddress = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
label: QbzSession.tr("Share this library", QbzSession.trRev)
|
||||
description: root.doc.address || ""
|
||||
QbzToggle {
|
||||
kioskHost: root.kioskHost
|
||||
checked: root.doc.listening === true
|
||||
enabled: !root.doc.starting
|
||||
onToggled: function(value) {
|
||||
if (value) QbzOrbit.startHost(root.listenAddress)
|
||||
else QbzOrbit.stopHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
visible: root.doc.listening === true
|
||||
label: QbzSession.tr("Access key", QbzSession.trRev)
|
||||
SettingsButton {
|
||||
kioskHost: root.kioskHost
|
||||
text: QbzSession.tr("Copy access key", QbzSession.trRev)
|
||||
onClicked: QbzOrbit.copyAccessKey()
|
||||
}
|
||||
}
|
||||
SettingsDivider {}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
label: QbzSession.tr("Host address", QbzSession.trRev)
|
||||
QbzLineEdit {
|
||||
kioskHost: root.kioskHost
|
||||
text: root.peerAddress
|
||||
onEdited: function(value) { root.peerAddress = value }
|
||||
onCommitted: function(value) { root.peerAddress = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
label: QbzSession.tr("Access key", QbzSession.trRev)
|
||||
QbzLineEdit {
|
||||
kioskHost: root.kioskHost
|
||||
text: root.accessKey
|
||||
isPassword: true
|
||||
onEdited: function(value) { root.accessKey = value }
|
||||
onCommitted: function(value) { root.accessKey = value }
|
||||
}
|
||||
}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
label: QbzSession.tr("Verify host", QbzSession.trRev)
|
||||
SettingsButton {
|
||||
kioskHost: root.kioskHost
|
||||
text: root.doc.probing ? QbzSession.tr("Checking server…", QbzSession.trRev)
|
||||
: QbzSession.tr("Verify host", QbzSession.trRev)
|
||||
enabled: !root.doc.probing
|
||||
onClicked: QbzOrbit.verifyHost(root.peerAddress, root.accessKey)
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
text: root.statusText()
|
||||
wrapMode: Text.WordWrap
|
||||
color: root.doc.status === "verified" ? theme.success : theme.danger
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: !!root.doc.peer
|
||||
text: root.doc.peer ? root.doc.peer.name + " · " + root.doc.peer.tracks + " " + QbzSession.tr("Tracks", QbzSession.trRev) : ""
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
}
|
||||
SettingRow {
|
||||
kioskHost: root.kioskHost
|
||||
visible: !!root.doc.peer
|
||||
label: QbzSession.tr("Local files", QbzSession.trRev)
|
||||
Row {
|
||||
spacing: 8
|
||||
QbzLineEdit {
|
||||
kioskHost: root.kioskHost
|
||||
text: root.query
|
||||
onEdited: function(value) { root.query = value }
|
||||
onCommitted: function(value) { root.query = value }
|
||||
onAccepted: QbzOrbit.searchLibrary(root.query)
|
||||
}
|
||||
SettingsButton {
|
||||
kioskHost: root.kioskHost
|
||||
text: root.doc.searching ? QbzSession.tr("Searching…", QbzSession.trRev)
|
||||
: QbzSession.tr("Search", QbzSession.trRev)
|
||||
enabled: !root.doc.searching && root.query.trim().length >= 2
|
||||
onClicked: QbzOrbit.searchLibrary(root.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: root.doc.page ? root.doc.page.tracks : []
|
||||
delegate: Text {
|
||||
required property var modelData
|
||||
width: root.width
|
||||
text: modelData.title + " · " + modelData.artist + " · " + modelData.album
|
||||
textFormat: Text.PlainText
|
||||
elide: Text.ElideRight
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
height: 32
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// The gate is on construction, not merely visibility. Nothing in the Orbit
|
||||
// panel is created in a normal launch or while another section is selected.
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
Loader {
|
||||
id: root
|
||||
property bool selected: false
|
||||
property bool kioskHost: false
|
||||
active: QbzOrbit.enabled && selected
|
||||
visible: active
|
||||
sourceComponent: Component {
|
||||
OrbitSettings { width: root.width; kioskHost: root.kioskHost }
|
||||
}
|
||||
}
|
||||
@@ -77,9 +77,9 @@ Item {
|
||||
{ label: QbzSession.tr("Integrations", QbzSession.trRev), section: 6 },
|
||||
{ label: QbzSession.tr("Import / Export", QbzSession.trRev), section: 9 },
|
||||
{ label: QbzSession.tr("Developer", QbzSession.trRev), section: 7 }
|
||||
].concat(root.sandboxed ? [{ label: (doc.dev || ({})).installMethod === "snap"
|
||||
].concat(QbzOrbit.enabled ? [{ label: "Orbit", section: 11 }] : []).concat(root.sandboxed ? [{ label: (doc.dev || ({})).installMethod === "snap"
|
||||
? QbzSession.tr("Snap", QbzSession.trRev) : QbzSession.tr("Flatpak", QbzSession.trRev), section: 8 }] : [])
|
||||
readonly property int section: QbzBridge.settingsSection
|
||||
readonly property int section: QbzBridge.settingsSection === 11 && !QbzOrbit.enabled ? 0 : QbzBridge.settingsSection
|
||||
readonly property bool migrationRunning:
|
||||
(doc.importExport || ({})).migrationRunning === true
|
||||
|
||||
@@ -287,6 +287,13 @@ Item {
|
||||
active: root.section === 7
|
||||
onClicked: QbzBridge.settingsSetSection(7)
|
||||
}
|
||||
SubNavItem {
|
||||
visible: QbzOrbit.enabled
|
||||
name: "compass"
|
||||
label: "Orbit"
|
||||
active: root.section === 11
|
||||
onClicked: QbzBridge.settingsSetSection(11)
|
||||
}
|
||||
// Sandboxed installs only (Flatpak / Snap permissions).
|
||||
SubNavItem {
|
||||
visible: root.sandboxed
|
||||
@@ -406,6 +413,11 @@ Item {
|
||||
width: parent ? parent.width : 0
|
||||
}
|
||||
|
||||
OrbitSettingsPanel {
|
||||
width: parent.width
|
||||
selected: root.section === 11
|
||||
kioskHost: root.kioskHost
|
||||
}
|
||||
Panel {
|
||||
panelIndex: 0
|
||||
sourceComponent: Component {
|
||||
|
||||
@@ -239,6 +239,16 @@ Rectangle {
|
||||
// the compositor's business).
|
||||
}
|
||||
|
||||
OrbitBanner {
|
||||
id: orbitBanner
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
remoteActive: QbzOrbit.enabled && QbzOrbit.controllingRemote
|
||||
connectionLost: QbzOrbit.connectionLost
|
||||
hostName: QbzOrbit.hostName
|
||||
}
|
||||
|
||||
NowPlayingBar {
|
||||
id: npb
|
||||
anchors.left: parent.left
|
||||
@@ -256,7 +266,7 @@ Rectangle {
|
||||
Sidebar {
|
||||
id: sidebar
|
||||
anchors.left: parent.left
|
||||
anchors.top: header.bottom
|
||||
anchors.top: orbitBanner.bottom
|
||||
anchors.bottom: npb.top
|
||||
// The shared hover-tooltip overlay (declared last, below). The sidebar
|
||||
// clips its own overflow, so the collapsed rail's name bubble HAS to be
|
||||
@@ -281,7 +291,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
id: queueColumn
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.top: orbitBanner.bottom
|
||||
anchors.bottom: npb.top
|
||||
// #771: the open width is the user's (300..600, persisted).
|
||||
width: (root.queueSidebarVisible || QbzShell.lyricsOpen)
|
||||
@@ -344,7 +354,7 @@ Rectangle {
|
||||
visible: QbzShell.sidebarState !== 2
|
||||
// Pane edge at sidebar.width + 8; strip 245..249 at 240, centre 247.
|
||||
x: sidebar.width + 5
|
||||
anchors.top: header.bottom
|
||||
anchors.top: orbitBanner.bottom
|
||||
anchors.bottom: npb.top
|
||||
z: 5
|
||||
onDragged: function (px) { QbzShell.sidebarDrag(px) }
|
||||
@@ -355,7 +365,7 @@ Rectangle {
|
||||
visible: queueColumn.width > 0
|
||||
// Pane edge at queueColumn.x - 8; strip x-9..x-4, centre x-6.5.
|
||||
x: queueColumn.x - 9
|
||||
anchors.top: header.bottom
|
||||
anchors.top: orbitBanner.bottom
|
||||
anchors.bottom: npb.top
|
||||
z: 5
|
||||
// The column hangs off the window's RIGHT edge: a pointer at x means
|
||||
@@ -414,7 +424,7 @@ Rectangle {
|
||||
id: contentFrame
|
||||
anchors.left: sidebar.right
|
||||
anchors.right: queueColumn.left
|
||||
anchors.top: header.bottom
|
||||
anchors.top: orbitBanner.bottom
|
||||
anchors.bottom: npb.top
|
||||
color: root.ambientOn ? theme.surfaceCardA50 : theme.surfaceCard
|
||||
|
||||
|
||||
@@ -458,10 +458,18 @@ Rectangle {
|
||||
// on that view, so its 42px must NOT be subtracted — otherwise the
|
||||
// content stays short and the NavRail rides up, leaving dead space
|
||||
// below the footer.
|
||||
OrbitBanner {
|
||||
id: orbitBanner
|
||||
width: shellColumn.width
|
||||
remoteActive: QbzOrbit.enabled && QbzOrbit.controllingRemote
|
||||
connectionLost: QbzOrbit.connectionLost
|
||||
hostName: QbzOrbit.hostName
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentFrame
|
||||
width: shellColumn.width
|
||||
height: root.height - backBar.height
|
||||
height: root.height - backBar.height - orbitBanner.height
|
||||
- (QbzShell.currentView === "nowplaying" ? 0 : root.transportHeight)
|
||||
- root.navRailHeight
|
||||
color: theme.surfaceCard
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Persistent context identity. No dismiss button or mutable visibility state.
|
||||
// A lost connection keeps the selected host visible; only ending remote
|
||||
// control may remove the banner. Host verification does not activate it.
|
||||
import QtQuick
|
||||
import com.blitzfc.qbz
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property bool remoteActive: false
|
||||
property bool connectionLost: false
|
||||
property string hostName: ""
|
||||
visible: remoteActive
|
||||
height: remoteActive ? Math.max(38, label.implicitHeight + 16) : 0
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.accent
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: label.text
|
||||
QbzTheme { id: theme }
|
||||
Text {
|
||||
id: label
|
||||
objectName: "orbitContextLabel"
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Orbit · " + QbzSession.tr("Controlling {}", QbzSession.trRev).replace("{}", root.hostName)
|
||||
+ (root.connectionLost ? " · " + QbzSession.tr("Connection lost. Remote controls are unavailable.", QbzSession.trRev) : "")
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Local Library previews shared by the All and category search tabs.
|
||||
// Result actions carry the page snapshot revision, never a Qobuz/native DB ID.
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
import "../controls"
|
||||
import "../theme"
|
||||
|
||||
Column {
|
||||
id: root
|
||||
property var sections: []
|
||||
property string revision: ""
|
||||
property int tab: 0
|
||||
readonly property bool hasResults: sections.some(function(section) { return root.accepts(section.kind) })
|
||||
signal activated(string revision, int index, string action)
|
||||
signal viewMore(string revision, string kind)
|
||||
spacing: 24
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
function accepts(kind) {
|
||||
return tab === 0 || (tab === 1 && kind === "local-album")
|
||||
|| (tab === 2 && kind === "local") || (tab === 3 && kind === "local-artist")
|
||||
}
|
||||
function titleFor(kind) {
|
||||
if (kind === "local-album") return QbzSession.tr("Albums on Local Library", QbzSession.trRev)
|
||||
if (kind === "local-artist") return QbzSession.tr("Artists on Local Library", QbzSession.trRev)
|
||||
return QbzSession.tr("On Local Library", QbzSession.trRev)
|
||||
}
|
||||
function menuFor(row) {
|
||||
var t = QbzSession.tr
|
||||
var r = QbzSession.trRev
|
||||
if (row.kind === "artist")
|
||||
return [{label:t("Open artist", r), icon:"user", action:"open"}]
|
||||
var entries = []
|
||||
if (row.kind === "album")
|
||||
entries.push({label:t("Open album", r), icon:"library-big", action:"open"})
|
||||
entries.push({label:t("Play", r), icon:"play-fill", action:"play"})
|
||||
entries.push({label:t("Play next", r), icon:"list-start", action:"next"})
|
||||
entries.push({label:t("Play later", r), icon:"list-plus", action:"later"})
|
||||
entries.push({label:t("Add to queue", r), icon:"list-end", action:"queue"})
|
||||
if (row.kind === "track") {
|
||||
entries.push({label:t("Add to playlist", r), icon:"list-music", action:"add-to-playlist"})
|
||||
if ((row.albumId || "") !== "")
|
||||
entries.push({label:t("Open containing album", r), icon:"disc-3", action:"open-album"})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.sections
|
||||
delegate: Column {
|
||||
id: section
|
||||
required property var modelData
|
||||
objectName: "localSearchSection-" + section.modelData.kind
|
||||
visible: root.accepts(section.modelData.kind)
|
||||
width: root.width
|
||||
spacing: 8
|
||||
QbzSectionHeader {
|
||||
title: root.titleFor(section.modelData.kind)
|
||||
showChevrons: false
|
||||
showViewAll: section.modelData.hasMore === true
|
||||
viewAllLabel: QbzSession.tr("View more", QbzSession.trRev)
|
||||
onViewAllClicked: root.viewMore(root.revision, section.modelData.kind)
|
||||
}
|
||||
Repeater {
|
||||
model: section.visible ? section.modelData.rows : []
|
||||
delegate: Rectangle {
|
||||
id: resultRow
|
||||
required property var modelData
|
||||
objectName: "localSearchRow-" + resultRow.modelData.flatIndex
|
||||
width: section.width
|
||||
height: 68
|
||||
radius: theme.radiusSm
|
||||
color: hit.containsMouse ? theme.surfaceHover : "transparent"
|
||||
RoundedImage {
|
||||
x: 8; width: 48; height: 48
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: resultRow.modelData.artPath || ""
|
||||
radius: resultRow.modelData.kind === "artist" ? 24 : 5
|
||||
}
|
||||
Column {
|
||||
anchors.left: parent.left; anchors.leftMargin: 70
|
||||
anchors.right: menuButton.left; anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
width: parent.width
|
||||
text: resultRow.modelData.title || ""
|
||||
color: theme.textPrimary; font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: resultRow.modelData.subtitle || ""
|
||||
color: theme.textMuted; font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: text !== ""
|
||||
text: resultRow.modelData.qualityDetail || ""
|
||||
color: theme.textSecondary; font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: hit
|
||||
objectName: "localSearchHit-" + resultRow.modelData.flatIndex
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 44
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
menuLoader.active = true
|
||||
var menu = menuLoader.item as CardMenu
|
||||
if (menu) menu.openAtCursor(hit, mouse.x, mouse.y)
|
||||
} else {
|
||||
root.activated(root.revision, resultRow.modelData.flatIndex,
|
||||
resultRow.modelData.kind === "track" ? "play" : "open")
|
||||
}
|
||||
}
|
||||
}
|
||||
QbzIconButton {
|
||||
id: menuButton
|
||||
objectName: "localSearchMenu-" + resultRow.modelData.flatIndex
|
||||
anchors.right: parent.right; anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "ellipsis"; btnSize: 28; iconSize: 14
|
||||
onClicked: {
|
||||
menuLoader.active = true
|
||||
var menu = menuLoader.item as CardMenu
|
||||
if (menu) menu.openBelowRight(menuButton)
|
||||
}
|
||||
HoverHandler { id: menuHover }
|
||||
ToolTip.visible: menuHover.hovered
|
||||
ToolTip.text: QbzSession.tr("More options", QbzSession.trRev)
|
||||
ToolTip.delay: 350
|
||||
}
|
||||
Loader {
|
||||
id: menuLoader
|
||||
objectName: "localSearchMenuLoader-" + resultRow.modelData.flatIndex
|
||||
active: false
|
||||
sourceComponent: CardMenu {
|
||||
objectName: "localSearchMenuPopup-" + resultRow.modelData.flatIndex
|
||||
entries: root.menuFor(resultRow.modelData)
|
||||
onPicked: function(action) {
|
||||
root.activated(root.revision, resultRow.modelData.flatIndex, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ Rectangle {
|
||||
readonly property var artists: doc.artists || []
|
||||
readonly property var artistsCarousel: doc.artistsCarousel || []
|
||||
readonly property var playlists: doc.playlists || []
|
||||
readonly property var localSections: doc.localSections || []
|
||||
readonly property var mp: doc.mostPopular || ({})
|
||||
readonly property bool loading: doc.loading === true
|
||||
readonly property int tab: doc.tab || 0
|
||||
@@ -84,7 +85,8 @@ Rectangle {
|
||||
// IDENTITY: when it changes, the page on screen is a different result set
|
||||
// and any pending "fade the tail" threshold is stale (see clearFade).
|
||||
readonly property string query: doc.query || ""
|
||||
readonly property bool hasResults: albums.length + tracks.length + artists.length + playlists.length > 0
|
||||
readonly property bool hasQobuzResults: albums.length + tracks.length + artists.length + playlists.length > 0
|
||||
readonly property bool hasResults: hasQobuzResults || localSections.length > 0
|
||||
readonly property int previewCap: 6
|
||||
|
||||
// The per-type result tabs live inside the page Flickable, so a GridView
|
||||
@@ -665,11 +667,17 @@ Rectangle {
|
||||
font.weight: theme.weightBold
|
||||
}
|
||||
Row {
|
||||
visible: root.hasResults
|
||||
visible: root.hasQobuzResults || root.filterIndex !== 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 16
|
||||
Text {
|
||||
text: QbzSession.tr("Qobuz", QbzSession.trRev)
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
FilterRadio { label: QbzSession.tr("Main Artist", QbzSession.trRev); selected: root.filterIndex === 1; onPicked: QbzSearch.searchFilterChanged(1) }
|
||||
FilterRadio { label: QbzSession.tr("Performer", QbzSession.trRev); selected: root.filterIndex === 2; onPicked: QbzSearch.searchFilterChanged(2) }
|
||||
FilterRadio { label: QbzSession.tr("Composer", QbzSession.trRev); selected: root.filterIndex === 3; onPicked: QbzSearch.searchFilterChanged(3) }
|
||||
@@ -1213,6 +1221,20 @@ Rectangle {
|
||||
cellH: 270
|
||||
onClicked: root.armLoadMore(4)
|
||||
}
|
||||
SearchLocalResults {
|
||||
id: localSearchResults
|
||||
width: parent.width
|
||||
sections: root.localSections
|
||||
revision: root.doc.localRevision || ""
|
||||
tab: root.tab
|
||||
visible: localSearchResults.hasResults
|
||||
onActivated: function(revision, index, action) {
|
||||
QbzSearch.searchLocalAction(revision, index, action)
|
||||
}
|
||||
onViewMore: function(revision, kind) {
|
||||
QbzSearch.searchLocalMore(revision, kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Loading skeleton ---------------------------------------
|
||||
|
||||
Reference in new issue
Block a user