feat(qt): port the My QBZ domain — mixtapes, collections and the discography builder
The Slint reference is 3,885 lines of .slint across ten files plus 5,514 lines
of Rust glue over the shared qbz-mixtape crate. None of it existed in the Qt
port; the nav flyout carried a "GAP:" comment and two disabled entries.
Rust (flat in src/, per QTBUG-93443): three bridges rather than one God-object
— QbzMyQbz (grids + detail + edit + cover + mix), QbzMyQbzAdd (the app-wide
picker, kept apart so TrackRow, PlayerBar and the Local Library bulk bars
cannot couple to detail state) and QbzDisco (artist-scoped, reached from the
artist page, never coexisting with the MyQBZ views). Twelve controllers behind
them, ported function by function from myqbz{,_detail,_play,_edit,_cover,_mix,
_add,_prefs,_view_prefs,_builder}.rs. src/toast_qt.rs finally gives the port a
toast host.
QML in qml/views/myqbz/ plus four global overlays. Reused rather than forked:
TrackRow, TrackListHeader, QualityBadgeFull, QbzContextMenu, CardMenu,
QbzCheckbox, QbzSkeleton, QbzScrollBar, RoundedImage. LocalSegToggle and
LocalMultiSelectBar are PROMOTED to controls/QbzSegToggle and
controls/QbzMultiSelectBar (they were ports of shared Slint primitives that
MyQBZ needs too) — a move plus an import fix, with the Local metrics kept as
the defaults so the four Local Library toggles are byte-unchanged and the MyQBZ
call sites pass ViewToggle's own numbers.
Every list is windowed at write time: GridView/ListView with reuseItems and a
cacheBuffer, and expanded mode is an outer recycled ListView whose delegate is
a Column + Repeater, because a nested ListView inside a recycled delegate
cannot size itself. Single-row updates patch in place; replacing the array
resets contentY and throws the user to the top.
Shared controls gained the arms the new views need, with the numbers taken from
the .slint and not guessed: SettingsButton (btnHeight / minWidth /
trailingIconName), QbzEmptyState's four metrics, QbzSelect's `sm` variant
(30px, r6, no outline, 12px label, 14px chevron, 40px narrower —
QbzSelect.slint:74-148), QbzContextMenu.openBelowLeft, and QbzLineEdit gains a
distinct `accepted` signal fired on ENTER ONLY: a modal that submits on
`committed` submits TWICE, because closing it removes focus and fires
`committed` again — a rename would have run twice and a create made two
collections. clearSearch() no longer assigns root.text either; that JS write
destroyed the call site's binding permanently, so Rust could never re-seed the
field after one click on the X.
SourceIcon gained the plain "qobuz" arm it was missing, which had every Qobuz
row in a collection drawing itself as a local file.
Two icons (turntable, image) are baked from the Slint masters into the seven
full tint dirs and registered in MASTERS; three more (circle-check-big,
circle-alert, triangle-alert) back the toast. A missing tint dir or glyph
renders nothing and logs nothing, so this class never surfaces in a build.
cc
This commit is contained in:
107 files changed
+14608
-226
No files matched your search
@@ -158,6 +158,7 @@ Rectangle {
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: viewLoader
|
||||
anchors.fill: parent
|
||||
source: QbzShell.currentView === "home" ? "../views/HomeView.qml"
|
||||
: QbzShell.currentView === "library" ? "../views/LibraryView.qml"
|
||||
@@ -181,7 +182,44 @@ Rectangle {
|
||||
// mix tile navigated to a view nobody mounted and the content
|
||||
// pane simply went blank (back recovered, which is why it read
|
||||
// as "nothing happens" rather than a crash).
|
||||
: QbzShell.currentView === "mix" ? "../views/MixView.qml" : ""
|
||||
: QbzShell.currentView === "mix" ? "../views/MixView.qml"
|
||||
// MyQBZ. Mixtapes and Collections are ONE file — the two
|
||||
// pages differ only in their filter row and their empty
|
||||
// state, so MyQbzGridView carries a `kind` discriminator
|
||||
// (see the Binding below, which is how it gets set).
|
||||
: QbzShell.currentView === "mixtapes"
|
||||
|| QbzShell.currentView === "collections" ? "../views/myqbz/MyQbzGridView.qml"
|
||||
: QbzShell.currentView === "mixtapedetail" ? "../views/myqbz/MyQbzDetailView.qml"
|
||||
: QbzShell.currentView === "discobuilder" ? "../views/myqbz/DiscoBuilderView.qml"
|
||||
// Settings > Blacklist > Manage. Not reachable from the
|
||||
// sidebar — blacklist_qt::open_manager records the route.
|
||||
: QbzShell.currentView === "blacklist" ? "../views/BlacklistManagerView.qml" : ""
|
||||
}
|
||||
|
||||
// MyQbzGridView serves BOTH the "mixtapes" and "collections" routes
|
||||
// and needs to know which. A Loader cannot pass properties through a
|
||||
// declarative `source:` ternary, and the view's own default is
|
||||
// "mixtape" — so without this the Collections route would render the
|
||||
// Mixtapes page, permanently, with nothing logged.
|
||||
//
|
||||
// A Binding rather than `onLoaded`/`setSource`: it applies the moment
|
||||
// `item` exists (the Loader creates it synchronously, so still before
|
||||
// the first render pass), it re-applies if the route flips
|
||||
// mixtapes <-> collections while the same instance is mounted, and it
|
||||
// keeps the whole router declarative. Same shape as the seeded-field
|
||||
// pattern at controls/QbzLineEdit.qml:174-179.
|
||||
//
|
||||
// RestoreNone: the target is DESTROYED on unload, and the default
|
||||
// RestoreBindingOrValue would try to write the old value back into a
|
||||
// dying object.
|
||||
Binding {
|
||||
target: viewLoader.item
|
||||
property: "kind"
|
||||
value: QbzShell.currentView === "collections" ? "collection" : "mixtape"
|
||||
when: viewLoader.item !== null
|
||||
&& (QbzShell.currentView === "mixtapes"
|
||||
|| QbzShell.currentView === "collections")
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,13 +258,26 @@ Rectangle {
|
||||
// touches the frame.
|
||||
//
|
||||
// Mechanism: four small Canvas nubs, each filling its corner square with
|
||||
// the shell colour and punching the quarter-disc back out. Canvas is this
|
||||
// port's rounding primitive for exactly this reason
|
||||
// (theme/RoundedImage.qml, cards/PlaylistCollage.qml): ShaderEffect /
|
||||
// OpacityMask render NOTHING on the software path, and `layer.enabled` +
|
||||
// a rounded mask would cost an FBO the size of the whole content pane
|
||||
// every frame. Cost here is 4 x 12x12 px rastered once, repainted only
|
||||
// when the theme colour or the radius changes.
|
||||
// the shell colour and punching the quarter-disc back out.
|
||||
//
|
||||
// DOCTRINE CORRECTION (2026-07-29). This paragraph used to justify the
|
||||
// Canvas by claiming "ShaderEffect / OpacityMask render NOTHING on the
|
||||
// software path, and `layer.enabled` would cost an FBO every frame".
|
||||
// Both halves are FALSE as stated: effects need shaders, and this port
|
||||
// runs on the GPU (OpenGL RHI, measured via QSG_INFO 2026-07-29) — the
|
||||
// "renders nothing" note came from an offscreen session, which forces the
|
||||
// software renderer by definition. A `layer` FBO is also rendered once and
|
||||
// CACHED for static content, not re-rendered per frame. Where a software
|
||||
// path is genuinely possible, detect it with `GraphicsInfo.api` rather
|
||||
// than assuming it (theme/RoundedImage.qml does exactly that and is the
|
||||
// canonical statement of this rule — read its "HOW THE ROUNDING IS DONE"
|
||||
// header before touching any masking code).
|
||||
//
|
||||
// The Canvas here is still the right call, but on its own merits, not that
|
||||
// one: the nubs are 4 x 12x12 px rastered ONCE and repainted only when the
|
||||
// theme colour or the radius changes, whereas masking the frame would mean
|
||||
// an FBO the size of the whole content pane, re-rendered on every window
|
||||
// resize and invalidated by anything animating inside the pane.
|
||||
//
|
||||
// The fill is the shell base (`root.color`) because that is literally
|
||||
// what shows through the bezel: the 8px gutter, the HeaderBar above
|
||||
@@ -468,6 +519,42 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- MyQBZ global overlays + the shared toast host ---------------------
|
||||
// Three window-level overlays, mounted ONCE here because each is reachable
|
||||
// from anywhere and must outlive the view that triggered it:
|
||||
//
|
||||
// AddToMixtapeModal — the "Add to Mixtape/Collection" picker, opened from
|
||||
// a TrackRow menu, an album/playlist page, a Local Library bulk bar or
|
||||
// the now-playing bar. The view that opened it is often unmounted by
|
||||
// the time the user picks a target.
|
||||
// MyQbzModals — Create / Edit (rename · description · delete) / DJ
|
||||
// Mix, three sibling panels in one file (spec 01 §10/§11). Delete in
|
||||
// particular navigates away from the page that raised it.
|
||||
// QbzToast — the ONE in-app toast host (toast_qt.rs publishes
|
||||
// onto QbzShell.toastJson; the auto-hide timer lives in the QML).
|
||||
//
|
||||
// NO `z` on any of them, and that is the point. This file's convention is
|
||||
// DECLARATION ORDER (see the note at ArtPreviewOverlay below: "LAST child =
|
||||
// above every surface"), and the bezel-corner note at :200-264 explains why
|
||||
// `z` is the wrong tool out here — ADR-009 pins in-pane modals at z >= 3000
|
||||
// with no ceiling, so any number picked here loses to the next one added.
|
||||
// Position in the chain is byte-equivalent to Slint, where `Toast { }` sits
|
||||
// at AppShell.slint:950: after DragGhost, before TooltipOverlay and
|
||||
// ArtPreviewOverlay. Covering the content pane's rounded corners is CORRECT
|
||||
// for a window-level overlay — a window-wide scrim covers them too.
|
||||
//
|
||||
// All three self-gate on their own document, so an unopened overlay is an
|
||||
// invisible, non-interactive Item and costs nothing.
|
||||
AddToMixtapeModal {
|
||||
anchors.fill: parent
|
||||
}
|
||||
MyQbzModals {
|
||||
anchors.fill: parent
|
||||
}
|
||||
QbzToast {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// LAST child = above every surface, exactly like ArtPreviewOverlay.slint's
|
||||
// mount in AppShell.slint. Non-interactive, so it never steals the hover
|
||||
// that is keeping it open (see the file header).
|
||||
|
||||
@@ -132,11 +132,15 @@ Rectangle {
|
||||
leftPadding: navTab.showIcon ? 9 : 11
|
||||
rightPadding: 11
|
||||
spacing: 6
|
||||
QbzIcon {
|
||||
// Baked glyph, or the section's own raw image when it carries one
|
||||
// (My QBZ branding) — see shell/NavSectionGlyph.qml.
|
||||
NavSectionGlyph {
|
||||
visible: navTab.showIcon
|
||||
name: navTab.section ? navTab.section.icon : ""
|
||||
section: navTab.section
|
||||
size: 14
|
||||
// Collapses to zero width under the 1140px breakpoint so the
|
||||
// Row reclaims the space (the height stays 14 via `size`).
|
||||
width: navTab.showIcon ? 14 : 0
|
||||
height: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Both arms are THEME tokens (the tab sits on surface-elevated
|
||||
// / surface-hover): "textPrimary", never the fixed-white
|
||||
@@ -192,10 +196,11 @@ Rectangle {
|
||||
opacity: isEnabled ? 1.0 : 0.5
|
||||
color: isActive ? theme.surfaceElevated
|
||||
: (cnbArea.containsMouse && isEnabled) ? theme.surfaceHover : "transparent"
|
||||
QbzIcon {
|
||||
name: cnb.section ? cnb.section.icon : ""
|
||||
width: 16
|
||||
height: 16
|
||||
// Baked glyph, or the section's own raw image when it carries one
|
||||
// (My QBZ branding) — see shell/NavSectionGlyph.qml.
|
||||
NavSectionGlyph {
|
||||
section: cnb.section
|
||||
size: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: cnb.isActive ? "textPrimary" : "secondary"
|
||||
}
|
||||
|
||||
@@ -27,10 +27,19 @@
|
||||
// row — hence the two-flag OR, HeaderMenuOverlay.slint:41);
|
||||
// - a press outside, or Escape, closes it.
|
||||
//
|
||||
// GAP vs Slint: no drop shadow. Qt's shadow effects (Qt5Compat /
|
||||
// QtQuick.Effects MultiEffect) render NOTHING on the software path this port
|
||||
// runs on (see RoundedImage.qml / TrackInfoModal.qml) — the 1px border-muted
|
||||
// outline carries the separation instead.
|
||||
// GAP vs Slint: no drop shadow — the 1px border-muted outline carries the
|
||||
// separation instead.
|
||||
//
|
||||
// SUPERSEDED (2026-07-29), the REASON this gap used to give: "Qt's shadow
|
||||
// effects (Qt5Compat / QtQuick.Effects MultiEffect) render NOTHING on the
|
||||
// software path this port runs on". Effects need shaders. This port runs on
|
||||
// the GPU (OpenGL RHI, measured 2026-07-29); the earlier "renders nothing"
|
||||
// note came from an offscreen session, which forces the software renderer by
|
||||
// definition. Where a software path is genuinely possible, detect it with
|
||||
// `GraphicsInfo.api` rather than assuming it (theme/RoundedImage.qml does).
|
||||
// The gap itself STAYS for now: adding the shadow is a visual change that
|
||||
// needs its own parity pass against Slint's `drop-shadow-blur`, not a perf
|
||||
// fix — do not add one in a domain commit.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -61,6 +70,40 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// MyQBZ branding — `label` (user-editable in Settings > Appearance,
|
||||
// coerced back to "My QBZ" on the Rust side when cleared), `iconPath` +
|
||||
// `hasCustomIcon` for a user-supplied section icon. Published on
|
||||
// QbzMyQbz.brandingJson, seeded at construction (not at boot()) precisely
|
||||
// because this catalog binds it on the FIRST frame.
|
||||
//
|
||||
// The parse is GUARDED (the parseDoc idiom, views/PlaylistView.qml:53-60):
|
||||
// a raw in-binding JSON.parse throws on the pre-publish frame and takes
|
||||
// the whole nav catalog down with it — both hosts included, since both
|
||||
// read `sections` from here. The try also covers QbzMyQbz not being
|
||||
// registered at all (an unknown type name is a plain JS global lookup, so
|
||||
// it raises a catchable ReferenceError rather than a load error).
|
||||
readonly property var branding: parseBranding()
|
||||
function parseBranding() {
|
||||
try {
|
||||
return JSON.parse(QbzMyQbz.brandingJson)
|
||||
} catch (e) {
|
||||
return ({})
|
||||
}
|
||||
}
|
||||
|
||||
// Rust never publishes an empty label — it coerces a cleared one back to
|
||||
// the English literal "My QBZ" — so the localised fallback below is
|
||||
// defensive only, and the SHIPPED default is that English literal. That is
|
||||
// a pre-existing i18n hole shared with the Slint app
|
||||
// (MyQbzBrandingState.label's default is the same literal); do not "fix"
|
||||
// it here by second-guessing the Rust value.
|
||||
readonly property string brandingLabel: {
|
||||
var l = nav.branding.label || ""
|
||||
return l !== "" ? l : nav.trs("My QBZ")
|
||||
}
|
||||
readonly property string brandingIconPath:
|
||||
nav.branding.hasCustomIcon === true ? (nav.branding.iconPath || "") : ""
|
||||
|
||||
// The four top-level sections, in Slint order. `rev` is threaded through
|
||||
// buildSections purely so the binding re-runs on a language switch
|
||||
// (QbzSession.trRev is the translation revision counter every other
|
||||
@@ -73,7 +116,11 @@ Item {
|
||||
// view (shell route), tab (view-internal tab id),
|
||||
// enabled }
|
||||
// Section shape: { id, icon, label, qobuz (hidden while offline),
|
||||
// enabled, entries }
|
||||
// enabled, entries,
|
||||
// iconPath — OPTIONAL: absent or "" means "use the baked
|
||||
// `icon`"; a non-empty path is a user-supplied image
|
||||
// rendered raw by shell/NavSectionGlyph.qml. Only the
|
||||
// MyQBZ section can carry one. }
|
||||
function buildSections(rev, reco) {
|
||||
// Discover — glyph-less rows (Slint has-glyph: false).
|
||||
var discover = [
|
||||
@@ -122,23 +169,32 @@ Item {
|
||||
]
|
||||
},
|
||||
{
|
||||
// GAP: this port has NO My QBZ surface — the shell router maps
|
||||
// home / library / local / localalbum / album / artist /
|
||||
// settings / search / playlist, and neither Collections nor
|
||||
// Mixtapes has a view behind it. The whole section is disabled
|
||||
// (dimmed, pointer-inert, no flyout) rather than opening a menu
|
||||
// whose every row clicks into nothing. The entries are kept
|
||||
// here so the port stays legible against Slint.
|
||||
"id": "myqbz",
|
||||
"icon": "qbz-symbolic",
|
||||
// Slint: MyQbzBrandingState.label (user-editable, defaults to
|
||||
// "My QBZ"); this port has no branding state yet.
|
||||
"label": nav.trs("My QBZ"),
|
||||
// Slint's HEADER hosts label this tab @tr("My QBZ")
|
||||
// (HeaderBar.slint:938, :1048) while only the sidebar and the
|
||||
// kiosk rail read MyQbzBrandingState.label (Sidebar.slint:787,
|
||||
// NavRail.slint:145). ONE catalog cannot express that split,
|
||||
// and a user who renames the section expects the rename
|
||||
// everywhere, so both hosts get the branding label.
|
||||
"label": nav.brandingLabel,
|
||||
// Custom section icon, rendered raw (its own colours, no theme
|
||||
// tint) — Slint's SidebarNavRow `raw-icon` arm.
|
||||
"iconPath": nav.brandingIconPath,
|
||||
"qobuz": false,
|
||||
"enabled": false,
|
||||
"enabled": true,
|
||||
"entries": [
|
||||
{ "label": nav.trs("Collections"), "icon": "library-big", "view": "", "tab": "", "enabled": false },
|
||||
{ "label": nav.trs("Mixtapes"), "icon": "cassette-tape", "view": "", "tab": "", "enabled": false }
|
||||
// ORDER is the header's: Collections then Mixtapes
|
||||
// (HeaderBar.slint:941-942). Slint's sidebar contradicts
|
||||
// itself with Mixtapes first (Sidebar.slint:790-791), and
|
||||
// one catalog serves both hosts.
|
||||
// ICONS are the header's pair too: cassette-tape +
|
||||
// library-big are baked in all eight tint dirs, while the
|
||||
// sidebar's cassette + collection are not — preferring
|
||||
// them would manufacture two asset imports for a visually
|
||||
// equivalent glyph.
|
||||
{ "label": nav.trs("Collections"), "icon": "library-big", "view": "collections", "tab": "", "enabled": true },
|
||||
{ "label": nav.trs("Mixtapes"), "icon": "cassette-tape", "view": "mixtapes", "tab": "", "enabled": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -147,10 +203,19 @@ Item {
|
||||
// Which section owns the highlight, derived from the LIVE view (both hosts
|
||||
// used to carry this identical binding). Slint highlights off
|
||||
// HeaderMenuState.open-index; the hosts OR that in via `openId`.
|
||||
//
|
||||
// Each alternative spells `QbzShell.currentView === x` out in full: the
|
||||
// shorthand `a === x || y || z` is ALWAYS truthy (`"y"` is a non-empty
|
||||
// string), which would light the section up on every route in the app.
|
||||
// "discobuilder" is deliberately NOT in the MyQBZ arm — Slint's own active
|
||||
// predicate omits the builder (NavRail.slint:146-148).
|
||||
readonly property string activeSection:
|
||||
QbzShell.currentView === "home" ? "discover"
|
||||
: QbzShell.currentView === "library" ? "library"
|
||||
: (QbzShell.currentView === "local" || QbzShell.currentView === "localalbum") ? "local"
|
||||
: (QbzShell.currentView === "mixtapes"
|
||||
|| QbzShell.currentView === "collections"
|
||||
|| QbzShell.currentView === "mixtapedetail") ? "myqbz"
|
||||
: ""
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// The leading glyph of a nav SECTION, shared by the three hosts that render
|
||||
// one: Sidebar.qml's NavRow, HeaderBar.qml's NavTab and its CompactNavBtn.
|
||||
// The catalog (NavFlyout.sections) carries both an `icon` (baked glyph name)
|
||||
// and an `iconPath` (a user-supplied image, "" when there is none), and the
|
||||
// branch between them is Slint's SidebarNavRow `raw-icon` arm:
|
||||
// a custom icon is rendered RAW so its own colours show, every other section
|
||||
// keeps the theme-tinted glyph (Sidebar.slint:559-562, :601-612).
|
||||
//
|
||||
// It exists as its own component because the alternative is the identical
|
||||
// two-way branch copy-pasted at three call sites (TRACK-RULES §5).
|
||||
|
||||
import QtQuick
|
||||
import "../theme"
|
||||
|
||||
Item {
|
||||
id: glyph
|
||||
|
||||
// A section object from NavFlyout.sections.
|
||||
property var section: null
|
||||
// Square edge, in px. Sites that need an asymmetric width (the header tab
|
||||
// collapses its icon to 0 under the 1140px breakpoint) set `width` too.
|
||||
property real size: 16
|
||||
// QbzIcon tint for the baked-glyph arm; ignored by the raw arm.
|
||||
property string tintName: "secondary"
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
readonly property string iconPath:
|
||||
(section && section.iconPath) ? section.iconPath : ""
|
||||
|
||||
// Raw user icon — no tint, `contain` fit (Slint image-fit: contain).
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: glyph.iconPath !== ""
|
||||
source: glyph.iconPath
|
||||
sourceSize: Qt.size(Math.max(1, Math.round(width * 2)),
|
||||
Math.max(1, Math.round(height * 2)))
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Default branded glyph — tinted to the host's text colour.
|
||||
QbzIcon {
|
||||
anchors.fill: parent
|
||||
visible: glyph.iconPath === ""
|
||||
// Kept empty on the raw arm so the baked glyph is never loaded twice.
|
||||
name: (glyph.section && glyph.iconPath === "") ? glyph.section.icon : ""
|
||||
tintName: glyph.tintName
|
||||
}
|
||||
}
|
||||
@@ -457,10 +457,21 @@ Rectangle {
|
||||
} else if (a === "album-favorite") {
|
||||
if (QbzPlayer.npAlbumId !== "")
|
||||
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
|
||||
} else if (a === "mixtape") {
|
||||
// MyQBZ AddItem, built here from the now-playing state:
|
||||
// `npArtworkPath` is a file:// CACHE path, so it is NOT the
|
||||
// artworkUrl — the store would keep a dead local path.
|
||||
if (id !== "")
|
||||
QbzMyQbzAdd.open(JSON.stringify([{
|
||||
"itemType": "track", "source": "qobuz",
|
||||
"sourceItemId": id, "title": QbzPlayer.npTitle,
|
||||
"subtitle": QbzPlayer.npArtist, "artworkUrl": "",
|
||||
"year": null, "trackCount": null
|
||||
}]))
|
||||
}
|
||||
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
|
||||
// "mixtape" have no invokable in the Qt port yet — the rows are
|
||||
// rendered 1:1 with the Slint flyout and do nothing for now.
|
||||
// TODO(qt-bridge): "playlist" (add-to-playlist modal) has no
|
||||
// invokable in the Qt port yet — the row is rendered 1:1 with the
|
||||
// Slint flyout and does nothing for now.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,9 @@
|
||||
// shell/CastPicker.qml (QbzCast — discovery, connect/disconnect, the
|
||||
// per-device quality cap) and lights while a renderer is connected.
|
||||
// Inert (TODO comments at the call sites): Connect (the QConnect device
|
||||
// flyout), add-to-playlist, add-to-mixtape. The volume LOCK (ALSA hw /
|
||||
// remote) is still not enforced.
|
||||
// flyout) and add-to-playlist. add-to-mixtape is LIVE since the MyQBZ domain
|
||||
// landed (QbzMyQbzAdd.open). The volume LOCK (ALSA hw / remote) is still not
|
||||
// enforced.
|
||||
//
|
||||
// SIZE (project rule): the inline SongCard / TransportControls / FavToggle
|
||||
// components moved out to shell/SongCard.qml, shell/TransportControls.qml and
|
||||
@@ -511,10 +512,21 @@ Rectangle {
|
||||
} else if (a === "album-favorite") {
|
||||
if (QbzPlayer.npAlbumId !== "")
|
||||
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
|
||||
} else if (a === "mixtape") {
|
||||
// MyQBZ AddItem, built here from the now-playing state:
|
||||
// `npArtworkPath` is a file:// CACHE path, so it is NOT the
|
||||
// artworkUrl — the store would keep a dead local path.
|
||||
if (id !== "")
|
||||
QbzMyQbzAdd.open(JSON.stringify([{
|
||||
"itemType": "track", "source": "qobuz",
|
||||
"sourceItemId": id, "title": QbzPlayer.npTitle,
|
||||
"subtitle": QbzPlayer.npArtist, "artworkUrl": "",
|
||||
"year": null, "trackCount": null
|
||||
}]))
|
||||
}
|
||||
// TODO(qt-bridge): "playlist" (add-to-playlist modal) and
|
||||
// "mixtape" have no invokable in the Qt port yet — the rows are
|
||||
// rendered 1:1 with the Slint flyout and do nothing for now.
|
||||
// TODO(qt-bridge): "playlist" (add-to-playlist modal) has no
|
||||
// invokable in the Qt port yet — the row is rendered 1:1 with the
|
||||
// Slint flyout and does nothing for now.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,13 @@
|
||||
// behave identically wherever the nav is hosted. The rows keep the
|
||||
// SidebarDirectRow active treatment (surface-hover bg + primary text/icon)
|
||||
// for the current section and while their own menu is open.
|
||||
// Discover / Library / Local Library route into real views; My QBZ does NOT
|
||||
// (no such surface in this port) and is therefore rendered disabled, flyout
|
||||
// included (a row that clicks into nothing is a defect, not a stub).
|
||||
// All four sections route into real views, My QBZ included (its Collections /
|
||||
// Mixtapes entries reach the "collections" / "mixtapes" routes). The disabled
|
||||
// treatment the NavRow still carries — dimmed to 0.45, pointer-inert, no
|
||||
// flyout — is the mechanism for a section with no surface behind it, and
|
||||
// nothing uses it today (a row that clicks into nothing is a defect, not a
|
||||
// stub). Its leading glyph goes through shell/NavSectionGlyph.qml, because My
|
||||
// QBZ can carry a user-supplied icon (SidebarNavRow `raw-icon`).
|
||||
// The playlist/folder tree below the nav IS live (sidebar_qt.rs: load,
|
||||
// sort, search, expand/collapse, drag-drop target); folder CREATION,
|
||||
// the Playlist Manager and the importer have no seam and are disabled in
|
||||
@@ -410,8 +414,9 @@ Rectangle {
|
||||
property var section: null
|
||||
readonly property bool isActive: section
|
||||
&& (root.activeNav === section.id || navFlyout.openId === section.id)
|
||||
// No surface behind this section in this port — rendered as
|
||||
// unavailable rather than as a live row that clicks into nothing.
|
||||
// A section with no surface behind it renders as unavailable rather
|
||||
// than as a live row that clicks into nothing. Every section is live
|
||||
// today; the treatment stays for the next stub.
|
||||
readonly property bool isEnabled: section && section.enabled
|
||||
readonly property bool lit: isActive || (navArea.containsMouse && isEnabled)
|
||||
|
||||
@@ -432,10 +437,11 @@ Rectangle {
|
||||
Item {
|
||||
width: root.mini ? parent.width : 16
|
||||
height: parent.height
|
||||
QbzIcon {
|
||||
name: navRow.section ? navRow.section.icon : ""
|
||||
width: 16
|
||||
height: 16
|
||||
// Baked glyph, or the section's own raw image when it carries
|
||||
// one (My QBZ branding) — Sidebar.slint:601-612.
|
||||
NavSectionGlyph {
|
||||
section: navRow.section
|
||||
size: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: navRow.lit ? "textPrimary" : "secondary"
|
||||
}
|
||||
@@ -499,9 +505,10 @@ Rectangle {
|
||||
|
||||
// The same four sections the header hosts, from the same catalog.
|
||||
// Qobuz-only ones are HIDDEN entirely while offline (ADR-010
|
||||
// mount-site gating, not rendered-disabled); My QBZ carries
|
||||
// enabled: false in the catalog (no surface behind it in this
|
||||
// port) and so renders dimmed and inert, flyout included.
|
||||
// mount-site gating, not rendered-disabled). My QBZ is `qobuz:
|
||||
// false` in the catalog, i.e. it stays mounted while offline —
|
||||
// Slint does not offline-gate its row either
|
||||
// (Sidebar.slint:779-792, unlike the Purchases row right after it).
|
||||
Repeater {
|
||||
model: navFlyout.sections
|
||||
delegate: NavRow {
|
||||
|
||||
Reference in new issue
Block a user