fix(qbz-qt): phase 12 — card rounding, menu anchoring, opaque window, titlebar toggle
Rounding: QML clip is rectangular (radius+clip does NOT round-clip) — new shared RoundedImage (software Canvas raster clip, GL-independent) applied at 16 artwork sites incl. true circular artist crops. Menus: new QbzContextMenu (cursor-anchored / below-trigger, window-clamped) converted at 7 sites, replicating TrackRow.slint positioning. Window: main window is OPAQUE in Slint (transparency is miniplayer-only; rounded corners are KWin's) — dropped the translucent frameless misread, squared app corners, opaque root. Titlebar: wired the EXISTING shared ui_prefs use_system_title_bar key (app-menu checkable item, restart semantics like Slint). km
No files matched your search
@@ -56,6 +56,8 @@ fn main() {
|
||||
"qml/QbzIcon.qml",
|
||||
"qml/QbzScrollBar.qml",
|
||||
"qml/QbzSpinner.qml",
|
||||
"qml/RoundedImage.qml",
|
||||
"qml/QbzContextMenu.qml",
|
||||
],
|
||||
qrc_files: &qrc_refs,
|
||||
..Default::default()
|
||||
|
||||
@@ -65,16 +65,16 @@ Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.artSource
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
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 } }
|
||||
@@ -223,7 +223,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: albumMenu.open()
|
||||
onClicked: function (mouse) { albumMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
||||
}
|
||||
}
|
||||
Item { width: (200 - 44 - 2 * 36 - 2 * 12) / 2; height: 1 }
|
||||
@@ -231,20 +231,9 @@ Rectangle {
|
||||
|
||||
// Context menu (AlbumCard.slint's album-menu): 196px, items
|
||||
// 33px, icon 15 + label 13.
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: albumMenu
|
||||
x: 150
|
||||
y: 140
|
||||
width: 196
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 196
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Open album"), "icon": "library-big", "action": "open" },
|
||||
@@ -301,7 +290,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Award ribbon — content-width, capped at the card width.
|
||||
Rectangle {
|
||||
|
||||
@@ -19,6 +19,10 @@ import com.blitzfc.qbz
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
// Round to the AppShell content-frame bezel (Radius.md): QML clips
|
||||
// are rectangular, so the frame's own rounding never reaches the
|
||||
// view — the view's own fill must round instead.
|
||||
radius: 12
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
@@ -71,7 +75,7 @@ Rectangle {
|
||||
onHeaderChanged: dispatchCovers()
|
||||
function dispatchCovers() {
|
||||
var urls = []
|
||||
if (header.artUrl) urls.push(header.artUrl)
|
||||
if (header && header.artUrl) urls.push(header.artUrl)
|
||||
var more = album.moreFromArtist || []
|
||||
for (var i = 0; i < more.length; i++) if (more[i].artUrl) urls.push(more[i].artUrl)
|
||||
var sug = album.suggestions || []
|
||||
@@ -84,7 +88,7 @@ Rectangle {
|
||||
component CircleBtn: Rectangle {
|
||||
property string name: ""
|
||||
property bool active: false
|
||||
signal clicked()
|
||||
signal clicked(var mouse)
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
@@ -103,7 +107,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.clicked()
|
||||
onClicked: function (mouse) { parent.clicked(mouse) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,22 +296,11 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: rowMenu.open()
|
||||
onClicked: function (mouse) { rowMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
||||
}
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: rowMenu
|
||||
x: 32 - 196
|
||||
y: 38
|
||||
width: 196
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 196
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Play"), "icon": "play-fill", "action": "play" },
|
||||
@@ -356,7 +349,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +422,7 @@ Rectangle {
|
||||
|
||||
// ============================ the page ================================
|
||||
Flickable {
|
||||
id: pageFlick
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentWidth: width
|
||||
@@ -459,11 +452,10 @@ Rectangle {
|
||||
radius: 12
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[header.artUrl] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 12
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,9 +623,10 @@ Rectangle {
|
||||
CircleBtn { name: "cassette-tape"; anchors.verticalCenter: parent.verticalCenter }
|
||||
CircleBtn { name: "info"; anchors.verticalCenter: parent.verticalCenter }
|
||||
CircleBtn {
|
||||
id: albumMenuBtn
|
||||
name: "ellipsis"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: albumMenu.open()
|
||||
onClicked: function (mouse) { albumMenu.openAtCursor(albumMenuBtn, mouse.x, mouse.y) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -917,7 +910,7 @@ Rectangle {
|
||||
anchors.rightMargin: 4
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
target: page.parent
|
||||
target: pageFlick
|
||||
}
|
||||
|
||||
// --- Full-description modal ------------------------------------------
|
||||
@@ -993,20 +986,9 @@ Rectangle {
|
||||
}
|
||||
|
||||
// Album ⋯ menu (AlbumContextMenu subset — card menu + pin).
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: albumMenu
|
||||
x: 260
|
||||
y: 260
|
||||
width: 196
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 196
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Play"), "icon": "play-fill", "action": "play" },
|
||||
@@ -1059,5 +1041,4 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,10 @@ import com.blitzfc.qbz
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
// TRANSPARENT — the chrome (header / sidebar / queue column / NPB)
|
||||
// paints every pixel between the header's rounded top corners and the
|
||||
// NPB's rounded bottom corners, so the window's rounded corners show
|
||||
// through (frameless translucent window).
|
||||
color: "transparent"
|
||||
// OPAQUE surface-card chrome (phase 12: the translucent-window read
|
||||
// was wrong — the Slint main window is opaque with square corners;
|
||||
// the compositor owns any rounding).
|
||||
color: theme.surfaceCard
|
||||
|
||||
// The host ApplicationWindow (custom chrome: drag / maximize / resize).
|
||||
property var hostWindow: null
|
||||
@@ -35,9 +34,8 @@ Rectangle {
|
||||
anchors.top: parent.top
|
||||
height: theme.headerHeight
|
||||
hostWindow: root.hostWindow
|
||||
// Rounded top window corners (custom chrome); the bottom corners
|
||||
// are covered by the same-color surfaces below.
|
||||
radius: 12
|
||||
// Square corners (phase 12: the window is opaque; any rounding is
|
||||
// the compositor's business).
|
||||
}
|
||||
|
||||
NowPlayingBarSmall {
|
||||
@@ -46,9 +44,7 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: theme.npbSmallHeight
|
||||
// Rounded bottom window corners (custom chrome); the top corners
|
||||
// are covered by the same-color surfaces above.
|
||||
radius: 12
|
||||
// Square corners (see the header note above).
|
||||
}
|
||||
|
||||
Sidebar {
|
||||
|
||||
@@ -21,6 +21,10 @@ import com.blitzfc.qbz
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
// Round to the AppShell content-frame bezel (Radius.md): QML clips
|
||||
// are rectangular, so the frame's own rounding never reaches the
|
||||
// view — the view's own fill must round instead.
|
||||
radius: 12
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
@@ -118,7 +122,7 @@ Rectangle {
|
||||
component CircleBtn: Rectangle {
|
||||
property string name: ""
|
||||
property bool active: false
|
||||
signal clicked()
|
||||
signal clicked(var mouse)
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
@@ -137,7 +141,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.clicked()
|
||||
onClicked: function (mouse) { parent.clicked(mouse) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,14 +201,14 @@ Rectangle {
|
||||
height: showAlbum ? 16 : 14
|
||||
tintName: "muted"
|
||||
}
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[row.artUrl] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: theme.radiusSm
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: theme.radiusSm
|
||||
color: "#000000"
|
||||
opacity: trArea.containsMouse || isActive ? 0.6 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
@@ -549,11 +553,10 @@ Rectangle {
|
||||
radius: 100
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[artist.artUrl] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,9 +614,10 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
CircleBtn {
|
||||
id: radioBtn
|
||||
name: "radio"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: radioPopup.open()
|
||||
onClicked: radioPopup.openBelowRight(radioBtn)
|
||||
}
|
||||
CircleBtn {
|
||||
name: "element-connect"
|
||||
@@ -622,9 +626,10 @@ Rectangle {
|
||||
onClicked: root.networkOpen = !root.networkOpen
|
||||
}
|
||||
CircleBtn {
|
||||
id: overflowBtn
|
||||
name: "ellipsis"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: overflowMenu.open()
|
||||
onClicked: function (mouse) { overflowMenu.openAtCursor(overflowBtn, mouse.x, mouse.y) }
|
||||
}
|
||||
Item { width: parent.width - 4 * 32 - 4 * 12 - segTabs.width; height: 1 }
|
||||
// From catalog / In library (webplayer parity).
|
||||
@@ -799,9 +804,10 @@ Rectangle {
|
||||
// POC-NOTE: multi-select out of scope.
|
||||
}
|
||||
CircleBtn {
|
||||
id: topMenuBtn
|
||||
name: "ellipsis"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: topMenu.open()
|
||||
onClicked: function (mouse) { topMenu.openAtCursor(topMenuBtn, mouse.x, mouse.y) }
|
||||
}
|
||||
}
|
||||
Item { visible: topTracks.length > 0; width: 1; height: 10 }
|
||||
@@ -954,11 +960,10 @@ Rectangle {
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[modelData.artUrl] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: theme.radiusSm
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -1318,20 +1323,9 @@ Rectangle {
|
||||
}
|
||||
|
||||
// --- Radio dropdown (INERT items — POC-NOTE: radio engines) ----------
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: radioPopup
|
||||
x: 260
|
||||
y: 300
|
||||
width: 180
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 180
|
||||
Repeater {
|
||||
model: [QbzBridge.tr("QBZ Radio"), QbzBridge.tr("Qobuz Radio")]
|
||||
delegate: Rectangle {
|
||||
@@ -1363,23 +1357,11 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ⋯ overflow menu ---------------------------------------------------
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: overflowMenu
|
||||
x: 260
|
||||
y: 300
|
||||
width: 224
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 224
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Create Artist Collection"), "icon": "library-big", "action": "stub" },
|
||||
@@ -1451,23 +1433,11 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Popular Tracks ⋯ menu ---------------------------------------------
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: topMenu
|
||||
x: 260
|
||||
y: 340
|
||||
width: 224
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 224
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Play all next"), "icon": "list-start", "action": "next-all" },
|
||||
@@ -1513,7 +1483,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Full-bio modal ----------------------------------------------------
|
||||
Rectangle {
|
||||
|
||||
@@ -32,6 +32,9 @@ Rectangle {
|
||||
// after a real movement so plain clicks/double-clicks still work.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
// Inert under the system titlebar (the native chrome owns
|
||||
// drag/double-click) — phase 12 titlebar toggle.
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
property bool dragStarted: false
|
||||
onPressed: dragStarted = false
|
||||
onPositionChanged: {
|
||||
@@ -221,7 +224,7 @@ Rectangle {
|
||||
Row {
|
||||
id: rightControls
|
||||
// Shifts left of the drawn window controls (3x34 + 2x2 = 106px).
|
||||
x: root.width - width - theme.spacingMd + 2 - 110
|
||||
x: root.width - width - theme.spacingMd + 2 - (QbzBridge.systemTitleBar ? 0 : 110)
|
||||
y: (root.height - height) / 2
|
||||
height: 36
|
||||
spacing: 4
|
||||
@@ -364,6 +367,7 @@ Rectangle {
|
||||
// --- Drawn window controls (WindowControls.slint, right placement:
|
||||
// minimize · maximize · close; close gets the danger-red hover) ------
|
||||
Row {
|
||||
visible: !QbzBridge.systemTitleBar
|
||||
x: root.width - width - 8
|
||||
y: (root.height - height) / 2
|
||||
height: 26
|
||||
@@ -487,6 +491,7 @@ Rectangle {
|
||||
component AppMenuItem: Rectangle {
|
||||
property string name: ""
|
||||
property string label: ""
|
||||
property bool checkedItem: false
|
||||
signal clicked()
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
@@ -505,12 +510,26 @@ Rectangle {
|
||||
tintName: "secondary"
|
||||
}
|
||||
Text {
|
||||
id: miLabel
|
||||
height: parent.height
|
||||
text: parent.parent.label
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Item {
|
||||
visible: parent.parent.checkedItem
|
||||
width: visible ? parent.width - 15 - miLabel.implicitWidth - 14 - 2 * parent.spacing : 0
|
||||
height: 1
|
||||
}
|
||||
QbzIcon {
|
||||
visible: parent.parent.checkedItem
|
||||
name: "check"
|
||||
width: 14
|
||||
height: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "accent"
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: miArea
|
||||
@@ -521,6 +540,23 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
AppMenuItem {
|
||||
name: "layout-grid"
|
||||
label: QbzBridge.tr("Use system title bar")
|
||||
checkedItem: QbzBridge.systemTitleBarPref
|
||||
onClicked: {
|
||||
QbzBridge.toggleSystemTitleBar()
|
||||
// Keep the menu open so the check state reads; the
|
||||
// applied mode changes on the next launch (1:1 Slint).
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
leftPadding: 37
|
||||
text: QbzBridge.tr("Takes effect after restarting QBZ.")
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 11
|
||||
}
|
||||
AppMenuItem {
|
||||
name: "settings-2"
|
||||
label: QbzBridge.tr("Settings")
|
||||
|
||||
@@ -26,6 +26,10 @@ import com.blitzfc.qbz
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
// Round to the AppShell content-frame bezel (Radius.md): QML clips
|
||||
// are rectangular, so the frame's own rounding never reaches the
|
||||
// view — the view's own fill must round instead.
|
||||
radius: 12
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
@@ -235,11 +239,10 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
Column {
|
||||
@@ -348,11 +351,10 @@ Rectangle {
|
||||
anchors.centerIn: parent
|
||||
color: theme.surfaceMain
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: card.artPath || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 95
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -396,11 +398,10 @@ Rectangle {
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: card.artPath || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: theme.radiusSm
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -505,11 +506,10 @@ Rectangle {
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: theme.radiusSm
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -559,11 +559,10 @@ Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 60
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -26,6 +26,10 @@ import com.blitzfc.qbz
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
// Round to the AppShell content-frame bezel (Radius.md): QML clips
|
||||
// are rectangular, so the frame's own rounding never reaches the
|
||||
// view — the view's own fill must round instead.
|
||||
radius: 12
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
@@ -406,23 +410,16 @@ Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.artMap[item.artKey] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
// Artist cards are circular in Slint (ArtistGridCard).
|
||||
Rectangle {
|
||||
visible: item.kind === "artist"
|
||||
anchors.fill: parent
|
||||
radius: 100
|
||||
color: "transparent"
|
||||
border.width: 0
|
||||
// Artist cards are circular in Slint (ArtistGridCard).
|
||||
radius: item.kind === "artist" ? 100 : theme.radiusSm
|
||||
}
|
||||
// Hover scrim + play overlay (albums + tracks).
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: item.kind === "artist" ? 100 : theme.radiusSm
|
||||
color: "#000000"
|
||||
opacity: overlayOn && (item.kind === "album" || item.kind === "track") ? 0.5 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
@@ -550,11 +547,10 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.artMap[item.artKey] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
Column {
|
||||
|
||||
@@ -17,8 +17,8 @@ import com.blitzfc.qbz
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
// Rounded window corners (custom chrome).
|
||||
radius: 12
|
||||
// Square window corners (phase 12: opaque window; the compositor owns
|
||||
// any rounding).
|
||||
// Set by the Loader (custom chrome drag/maximize); unused here.
|
||||
property var hostWindow: null
|
||||
|
||||
|
||||
@@ -19,10 +19,17 @@ ApplicationWindow {
|
||||
minimumHeight: 600
|
||||
visible: true
|
||||
title: "QBZ"
|
||||
// Custom chrome (phase 7): frameless + translucent so the window draws
|
||||
// its own rounded corners, 1:1 with the Slint custom-chrome look.
|
||||
flags: Qt.Window | Qt.FramelessWindowHint
|
||||
color: "transparent"
|
||||
// Custom chrome (phase 7/12): frameless but OPAQUE — the phase-7
|
||||
// translucent window was a misread: the Slint MAIN window keeps an
|
||||
// OPAQUE swapchain (only the miniplayer blends; crates/qbz/src/main.rs
|
||||
// set_surface_prefers_transparent + the Cargo.toml patch comment), and
|
||||
// the rounded corners in the Slint screenshots come from the
|
||||
// COMPOSITOR, not the app — app.slint's root is opaque surface-main
|
||||
// with square corners and a square 1px hairline frame. The system
|
||||
// titlebar is the `use_system_title_bar` pref (ui_prefs.json; applied
|
||||
// at startup, restart semantics like Slint).
|
||||
flags: QbzBridge.systemTitleBar ? Qt.Window : (Qt.Window | Qt.FramelessWindowHint)
|
||||
color: "#1a1a1a"
|
||||
|
||||
FontLoader { id: interRegular; source: "assets/fonts/Inter_18pt-Regular.ttf" }
|
||||
FontLoader { id: interMedium; source: "assets/fonts/Inter_18pt-Medium.ttf" }
|
||||
@@ -32,6 +39,7 @@ ApplicationWindow {
|
||||
|
||||
Component.onCompleted: QbzBridge.boot()
|
||||
|
||||
|
||||
Loader {
|
||||
id: screenLoader
|
||||
anchors.fill: parent
|
||||
@@ -43,15 +51,15 @@ ApplicationWindow {
|
||||
onLoaded: if (screenLoader.item) screenLoader.item.hostWindow = window
|
||||
}
|
||||
|
||||
// Rounded-window hairline frame (app.slint's no-frame 1px edge) —
|
||||
// paints at the very rim, over everything, clipped by the corners.
|
||||
// Frameless hairline (app.slint's no-frame 1px edge) — paints at the
|
||||
// very rim, over everything. SQUARE (the app draws no corner rounding).
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: 12
|
||||
border.width: 1
|
||||
border.color: "#14ffffff"
|
||||
visible: window.visibility !== Window.Maximized && window.visibility !== Window.FullScreen
|
||||
visible: !QbzBridge.systemTitleBar
|
||||
&& window.visibility !== Window.Maximized && window.visibility !== Window.FullScreen
|
||||
}
|
||||
|
||||
// Edge/corner resize grips (custom chrome — the compositor draws no
|
||||
@@ -59,41 +67,49 @@ ApplicationWindow {
|
||||
MouseArea {
|
||||
anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; width: 6
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.LeftEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.right: parent.right; anchors.top: parent.top; anchors.bottom: parent.bottom; width: 6
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.RightEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; height: 6
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.TopEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; height: 6
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.BottomEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.left: parent.left; anchors.top: parent.top; width: 12; height: 12
|
||||
cursorShape: Qt.SizeFDiagCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.TopEdge | Qt.LeftEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.right: parent.right; anchors.top: parent.top; width: 12; height: 12
|
||||
cursorShape: Qt.SizeBDiagCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.TopEdge | Qt.RightEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.left: parent.left; anchors.bottom: parent.bottom; width: 12; height: 12
|
||||
cursorShape: Qt.SizeBDiagCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.BottomEdge | Qt.LeftEdge)
|
||||
}
|
||||
MouseArea {
|
||||
anchors.right: parent.right; anchors.bottom: parent.bottom; width: 12; height: 12
|
||||
cursorShape: Qt.SizeFDiagCursor
|
||||
enabled: !QbzBridge.systemTitleBar
|
||||
onPressed: window.startSystemResize(Qt.BottomEdge | Qt.RightEdge)
|
||||
}
|
||||
|
||||
@@ -102,7 +118,6 @@ ApplicationWindow {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#0f0f0f"
|
||||
radius: 12
|
||||
visible: QbzBridge.screen === "splash"
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -187,11 +187,10 @@ Rectangle {
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
visible: QbzBridge.npHasTrack && QbzBridge.npArtworkPath !== ""
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: QbzBridge.npArtworkPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
radius: 6
|
||||
}
|
||||
// Idle (no track): a STATIC album glyph
|
||||
// placeholder (50% of the art size, muted).
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// QbzContextMenu — the shared popup surface for the app's ⋯ / context
|
||||
// menus, with CORRECT positioning (phase 12):
|
||||
//
|
||||
// - openAtCursor(item, localX, localY): context menus open AT THE
|
||||
// POINTER, like the Slint unified TrackContextMenu (TrackRow.slint:
|
||||
// `absolute-position - layout-offset + local-pointer`, clamped into
|
||||
// the row/window). Coordinates are mapped to the window and the menu
|
||||
// is parented to the Overlay, so a menu never renders inside the
|
||||
// triggering card/row.
|
||||
// - openBelowRight(item): button menus anchor under their trigger,
|
||||
// right edge aligned (the QbzSelect / sort-menu convention).
|
||||
// - Both clamp into the window with an 8px margin (Slint clamps the x
|
||||
// to `width - menu-width - 12`) so a menu never opens off-screen —
|
||||
// Popup does NOT do this on its own.
|
||||
//
|
||||
// The background matches ContextMenu.slint (surface-main, Radius.sm,
|
||||
// 1px border-muted). Rows go in the default content (33px, icon 15 +
|
||||
// label 13 per the existing per-site convention).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
parent: Overlay.overlay
|
||||
width: menuWidth
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
|
||||
|
||||
property int menuWidth: 196
|
||||
default property alias menuContent: col.data
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column { id: col }
|
||||
|
||||
// Final placement is computed BEFORE open() — the popup's implicit
|
||||
// height is already known (static content), and `Window.window` is
|
||||
// only reliably attached on the SOURCE item, not on the popup.
|
||||
function _place(gx, gy, win) {
|
||||
if (win) {
|
||||
x = Math.max(8, Math.min(gx, win.width - width - 8))
|
||||
y = Math.max(8, Math.min(gy, win.height - implicitHeight - 8))
|
||||
} else {
|
||||
x = gx
|
||||
y = gy
|
||||
}
|
||||
open()
|
||||
}
|
||||
|
||||
/// Context menu at the pointer. `localX/localY` are the click coords
|
||||
/// inside `sourceItem` (the MouseArea that caught the click).
|
||||
function openAtCursor(sourceItem, localX, localY) {
|
||||
var g = sourceItem.mapToItem(null, localX, localY)
|
||||
_place(g.x, g.y, sourceItem.Window.window)
|
||||
}
|
||||
|
||||
/// Button menu: below the trigger, right edges aligned.
|
||||
function openBelowRight(sourceItem) {
|
||||
var g = sourceItem.mapToItem(null, sourceItem.width - width, sourceItem.height + 4)
|
||||
_place(g.x, g.y, sourceItem.Window.window)
|
||||
}
|
||||
}
|
||||
@@ -153,11 +153,10 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.coverMap[row.artUrl] || ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +226,11 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: rowMenu.open()
|
||||
onClicked: function (mouse) { rowMenu.openAtCursor(menuArea, mouse.x, mouse.y) }
|
||||
}
|
||||
Popup {
|
||||
QbzContextMenu {
|
||||
id: rowMenu
|
||||
x: width - 196
|
||||
y: 44
|
||||
width: 196
|
||||
padding: 5
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
background: Rectangle {
|
||||
color: theme.surfaceMain
|
||||
radius: theme.radiusSm
|
||||
border.width: 1
|
||||
border.color: theme.borderMuted
|
||||
}
|
||||
contentItem: Column {
|
||||
menuWidth: 196
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": QbzBridge.tr("Remove from queue"), "icon": "trash-2", "action": "remove" },
|
||||
@@ -289,7 +277,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,11 +410,10 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: theme.surfaceCard
|
||||
clip: true
|
||||
Image {
|
||||
RoundedImage {
|
||||
anchors.fill: parent
|
||||
source: root.currentRow ? (root.coverMap[root.currentRow.artUrl] || "") : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
Column {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// RoundedImage — an Image with TRUE rounded corners (PreserveAspectCrop).
|
||||
//
|
||||
// Why this exists: QML's `clip` is rectangular — a Rectangle with
|
||||
// `radius` + `clip: true` does NOT clip children to the rounded shape
|
||||
// (proven with an isolated scene on this Qt build: the child paints
|
||||
// square over the rounded fill). Shader masks (Qt5Compat OpacityMask,
|
||||
// QtQuick.Effects MultiEffect) render NOTHING on the software/offscreen
|
||||
// path (the phase-2 icon-tinting probe), and the Pi kiosk can run the
|
||||
// software path — so rounding is done with a Canvas raster clip
|
||||
// (QPainter, antialiased, renderTarget Image = CPU).
|
||||
//
|
||||
// Usage mirrors the old pattern: `RoundedImage { anchors.fill: parent;
|
||||
// source: artPath; radius: theme.radiusSm }` inside the same rounded
|
||||
// placeholder Rectangle (the placeholder's OWN fill rounds fine — only
|
||||
// children needed the workaround).
|
||||
|
||||
import QtQuick
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
property string source: ""
|
||||
property real radius: 8
|
||||
|
||||
renderTarget: Canvas.Image
|
||||
renderStrategy: Canvas.Cooperative
|
||||
|
||||
// Dimension probe: Canvas can't query the loaded image's intrinsic
|
||||
// size, and PreserveAspectCrop needs it. A hidden Image doubles as
|
||||
// the async loader notification.
|
||||
Image {
|
||||
id: probe
|
||||
source: root.source
|
||||
visible: false
|
||||
asynchronous: true
|
||||
cache: true
|
||||
onStatusChanged: if (status === Image.Ready || status === Image.Error) root.requestPaint()
|
||||
}
|
||||
|
||||
onSourceChanged: loadImage(source)
|
||||
Component.onCompleted: loadImage(source)
|
||||
onImageLoaded: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
var w = width
|
||||
var h = height
|
||||
if (w <= 0 || h <= 0) return
|
||||
var r = Math.max(0, Math.min(radius, w / 2, h / 2))
|
||||
if (r > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, 0)
|
||||
ctx.lineTo(w - r, 0)
|
||||
ctx.arcTo(w, 0, w, r, r)
|
||||
ctx.lineTo(w, h - r)
|
||||
ctx.arcTo(w, h, w - r, h, r)
|
||||
ctx.lineTo(r, h)
|
||||
ctx.arcTo(0, h, 0, h - r, r)
|
||||
ctx.lineTo(0, r)
|
||||
ctx.arcTo(0, 0, r, 0, r)
|
||||
ctx.closePath()
|
||||
ctx.clip()
|
||||
}
|
||||
if (probe.status === Image.Ready && probe.sourceSize.width > 0 && isImageLoaded(source)) {
|
||||
// PreserveAspectCrop: uniform scale covering the rect, centered.
|
||||
var iw = probe.sourceSize.width
|
||||
var ih = probe.sourceSize.height
|
||||
var scale = Math.max(w / iw, h / ih)
|
||||
var dw = iw * scale
|
||||
var dh = ih * scale
|
||||
ctx.drawImage(source, (w - dw) / 2, (h - dh) / 2, dw, dh)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -146,6 +146,15 @@ pub mod qbz_bridge {
|
||||
// row states, select option lists, output-device groups).
|
||||
#[qproperty(QString, settings_json)]
|
||||
|
||||
// --- Window chrome (phase 12) --------------------------------------
|
||||
// The APPLIED titlebar mode (the ui_prefs `use_system_title_bar`
|
||||
// value read at startup — drives the window flags; never mutated at
|
||||
// runtime, matching the Slint restart semantics).
|
||||
#[qproperty(bool, system_title_bar)]
|
||||
// The PERSISTED pref as it stands NOW (the app-menu check state;
|
||||
// flips live when the user toggles, applies on the next launch).
|
||||
#[qproperty(bool, system_title_bar_pref)]
|
||||
|
||||
type QbzBridge = super::QbzBridgeRust;
|
||||
|
||||
/// Called once from Main.qml's Component.onCompleted: registers the
|
||||
@@ -370,6 +379,12 @@ pub mod qbz_bridge {
|
||||
/// Output-device refresh button: release a held device, re-enumerate.
|
||||
#[qinvokable]
|
||||
fn refresh_devices(self: Pin<&mut QbzBridge>);
|
||||
|
||||
/// App-menu chrome toggle: flip the persisted `use_system_title_bar`
|
||||
/// pref (applies on the next launch — the window flags are fixed at
|
||||
/// creation, 1:1 Slint). Updates `systemTitleBarPref` only.
|
||||
#[qinvokable]
|
||||
fn toggle_system_title_bar(self: Pin<&mut QbzBridge>);
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for QbzBridge {}
|
||||
@@ -441,6 +456,8 @@ pub struct QbzBridgeRust {
|
||||
lyrics_open: bool,
|
||||
lyrics_json: QString,
|
||||
settings_json: QString,
|
||||
system_title_bar: bool,
|
||||
system_title_bar_pref: bool,
|
||||
}
|
||||
|
||||
impl Default for QbzBridgeRust {
|
||||
@@ -502,6 +519,8 @@ impl Default for QbzBridgeRust {
|
||||
lyrics_open: false,
|
||||
lyrics_json: QString::from("{}"),
|
||||
settings_json: QString::from("{}"),
|
||||
system_title_bar: crate::settings_qt::use_system_title_bar(),
|
||||
system_title_bar_pref: crate::settings_qt::use_system_title_bar(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,4 +789,8 @@ impl qbz_bridge::QbzBridge {
|
||||
pub fn refresh_devices(self: Pin<&mut Self>) {
|
||||
crate::refresh_devices();
|
||||
}
|
||||
|
||||
pub fn toggle_system_title_bar(self: Pin<&mut Self>) {
|
||||
crate::toggle_system_title_bar();
|
||||
}
|
||||
}
|
||||
@@ -725,6 +725,16 @@ pub(crate) fn refresh_devices() {
|
||||
spawn(async move { settings_qt::refresh_devices(&runtime).await });
|
||||
}
|
||||
|
||||
/// App-menu chrome toggle: persist the flipped pref and update the menu
|
||||
/// state. The APPLIED mode (`systemTitleBar`) is deliberately untouched —
|
||||
/// it changes on the next launch (the window flags are read once at
|
||||
/// startup, 1:1 the Slint restart semantics).
|
||||
pub(crate) fn toggle_system_title_bar() {
|
||||
let next = settings_qt::toggle_system_title_bar();
|
||||
log::info!("[qbz-qt] use_system_title_bar -> {next} (applies on next launch)");
|
||||
ui(move |mut b| b.as_mut().set_system_title_bar_pref(next));
|
||||
}
|
||||
|
||||
fn load_library_once() {
|
||||
if *LIBRARY_LOADED.lock().unwrap() {
|
||||
return;
|
||||
|
||||
@@ -115,6 +115,47 @@ fn save_streaming_quality(key: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ui_prefs.json (window chrome, phase 12) — the Slint `use_system_title_bar`
|
||||
// pref (crates/qbz/src/ui_prefs.rs): SAME shared file, additive key patch so
|
||||
// every other Slint key survives. Default TRUE on Linux (the Slint default
|
||||
// is `!macos` — Linux keeps the system decorations). Applied at startup
|
||||
// only: decorations negotiate at surface creation on Wayland, so a toggle
|
||||
// takes effect on the next launch (restart semantics, 1:1 Slint).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn use_system_title_bar() -> bool {
|
||||
let Some(path) = prefs_path() else {
|
||||
return true;
|
||||
};
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
|
||||
.and_then(|v| v.get("use_system_title_bar").and_then(|q| q.as_bool()))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Flip + persist the pref; returns the new value (for the menu state).
|
||||
pub fn toggle_system_title_bar() -> bool {
|
||||
let next = !use_system_title_bar();
|
||||
if let Some(path) = prefs_path() {
|
||||
let mut value: serde_json::Value = std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str(&text).ok())
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
if let Some(obj) = value.as_object_mut() {
|
||||
obj.insert(
|
||||
"use_system_title_bar".to_string(),
|
||||
serde_json::Value::Bool(next),
|
||||
);
|
||||
if let Ok(text) = serde_json::to_string_pretty(&value) {
|
||||
let _ = std::fs::write(&path, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
pub const STREAMING_QUALITY_KEYS: &[&str] = &["mp3", "cd", "hires", "hires_plus"];
|
||||
pub const STREAMING_QUALITY_LABELS: &[&str] = &["MP3", "CD Quality", "Hi-Res", "Hi-Res+"];
|
||||
|
||||
|
||||