Files
qbz/crates/qbz-qt/qml/shell/AboutModal.qml
T
vicrodh ed7fa588ea fix(qt): elevated controls lift on hover instead of darkening
Buttons that rest on surfaceElevated used surfaceHover for their hover
fill: a 6% white wash meant for rows that rest transparent. Over an
elevated button it lands DARKER than the rest fill — on a modal's dark
ground the Cancel/Save/footer buttons went near-black under the pointer.

QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived
so every palette has it), its ambient half-alpha twin, and the
elevatedFill / elevatedHoverFill pair an ambient-aware control paints.
Every site with the old pairing (65 across modals, toolbars, selects,
circle actions, chips and cards) now hovers to the lift.

cc
2026-09-14 00:49:54 -06:00

705 lines
29 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// About QBZ — QML port of crates/qbz-ui/ui/shell/AboutModal.slint (531 lines).
//
// Opened from the header hamburger menu ("About QBZ"). Everything that is not
// a translated literal comes from ONE document, QbzAbout.aboutJson
// (src/about_qt.rs): version, platform label, build stamp, release URL,
// codename, the author chip and the row-grouped contributor chips.
//
// Structure follows LogViewerModal.qml, which already solves the four things
// this file would otherwise get wrong: the guarded JSON.parse (a bare parse in
// a binding throws on the pre-publish frame), the self-gated mount (ADR-010 —
// an invisible, non-interactive Item while closed), the faked drop shadow (the
// port has no blurred shadow) and the panel sizing. z: 3000 explicitly, per
// ADR-009 as this port spells it.
//
// ── DELIBERATE DIVERGENCES FROM THE SLINT REFERENCE ────────────────────────
//
// 1. CONTRIBUTORS USE `Flow`, NOT THE PRE-GROUPED ROWS. Slint pre-groups the
// chips into fixed rows of five ONLY because it has no flex-wrap
// (AboutModal.slint:432-436 says so). QML has `Flow`, which is what the
// original Tauri build used. The rows stay in the JSON so both frontends
// read one document shape; this file flattens them.
// 2. THE STRUCK-THROUGH "love" USES `font.strikeout`. Slint fakes it with a
// 1px Rectangle laid over the glyphs (AboutModal.slint:474-492) because
// Slint Text has no text-decoration. Qt Text has the real property.
// 3. THE ACKNOWLEDGMENTS LINE SAYS "Qt", NOT "Slint" — see its comment below.
// 4. Colour literals are `Qt.rgba(...)`, not the reference's `#RRGGBBAA`.
// Slint is #RRGGBBAA and Qt is #AARRGGBB, and copying a Slint literal
// verbatim is how this port shipped invisible scrims and invisible
// shadows across five modals, all of which read `#000000bf` / `#00000080`
// — ZERO alpha under Qt — where 20 other files spelled the same scrim
// `#bf000000`. Fixed 2026-08-14. Floats cannot be misread in either
// direction, which is why everything new here uses them.
//
// The 128px logo is deliberate and load-bearing: the 512px qbz-logo.png
// already in qml/assets aliases when scaled down to 56 (AboutModal.slint:184-186
// records exactly that finding for femtovg; a 2.3x downscale is clean, a 9x one
// is not).
//
// No pills (ADR-008): the author + contributor chips are bordered radiusSm
// rectangles, and so is nothing else here.
import QtQuick
import com.blitzfc.qbz
import "../theme"
Item {
id: root
// GUARDED — the property is seeded "{}" on the bridge, but a redeploy
// order or a future "" default must not poison every binding in the file.
readonly property var doc: {
try {
return JSON.parse(QbzAbout.aboutJson)
} catch (e) {
return ({})
}
}
// The contributor chips as ONE list. See divergence 1 in the header: the
// document keeps Slint's row grouping, `Flow` does the wrapping here.
readonly property var contributors: {
var rows = root.doc.contributorRows || []
var out = []
for (var i = 0; i < rows.length; i++)
for (var j = 0; j < (rows[i] || []).length; j++)
out.push(rows[i][j])
return out
}
anchors.fill: parent
z: 3000
visible: root.doc.open === true
enabled: root.visible
QbzTheme { id: theme }
onVisibleChanged: {
if (visible)
keyScope.forceActiveFocus()
}
// Escape closes. The LOCAL FocusScope idiom (LogViewerModal.qml:73-77)
// rather than growing hotkeys_qt.rs's central EscapeState — that walk is
// pinned by a unit test whose ordering is not this lane's to renegotiate.
FocusScope {
id: keyScope
anchors.fill: parent
Keys.onEscapePressed: QbzAbout.aboutClose()
}
// ---- Sub-components (the Slint file's LinkButton / HandleChip) --------
// A bordered icon+label link button (the external-links row).
component LinkButton: Rectangle {
id: lb
property string label: ""
property string iconName: "external-link"
signal clicked()
height: 34
// Slint sizes this to `row.preferred-width`; the Row's implicitWidth
// is the same measure, plus the 14px side padding either side.
width: lbRow.implicitWidth + 28
radius: theme.radiusSm
border.width: 1
border.color: theme.borderSubtle
color: lbArea.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
Row {
id: lbRow
anchors.centerIn: parent
spacing: 8
QbzIcon {
anchors.verticalCenter: parent.verticalCenter
name: lb.iconName
width: 15
height: 15
// Theme-following, both arms: the reference hands
// Theme.text-primary / Theme.text-secondary here.
tintName: lbArea.containsMouse ? "textPrimary" : "secondary"
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: lb.label
color: lbArea.containsMouse ? theme.textPrimary : theme.textSecondary
font.pixelSize: 13
font.weight: theme.weightMedium
}
QbzIcon {
anchors.verticalCenter: parent.verticalCenter
name: "external-link"
width: 11
height: 11
tintName: "muted"
}
}
MouseArea {
id: lbArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: lb.clicked()
}
}
// A bordered, clickable handle chip with a round GitHub avatar. NOT a pill
// (ADR-008): radiusSm + a subtle border. The avatar arrives asynchronously
// (about_qt.rs downloads it to the user cache and publishes a file:// path
// — this port loads no https:// into an Image.source anywhere), so the
// circle stays blank until then, exactly like the reference.
component HandleChip: Rectangle {
id: hc
property string name: ""
property string url: ""
property string avatar: ""
property int textSize: 13
property int avatarSize: 20
height: 28
width: hcRow.implicitWidth + 16
radius: theme.radiusSm
border.width: 1
border.color: theme.borderSubtle
color: hcArea.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
Row {
id: hcRow
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
spacing: 7
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: hc.avatarSize
height: hc.avatarSize
radius: width / 2
color: theme.surfaceHover
// QML `clip` is rectangular, so the circle has to come from a
// mask — that is what RoundedImage exists for (see its header).
RoundedImage {
anchors.fill: parent
source: hc.avatar
radius: width / 2
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: hc.name
color: hcArea.containsMouse ? theme.accent : theme.textPrimary
font.pixelSize: hc.textSize
font.weight: theme.weightMedium
}
QbzIcon {
anchors.verticalCenter: parent.verticalCenter
name: "external-link"
width: 10
height: 10
tintName: "muted"
}
}
MouseArea {
id: hcArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzShell.openExternalUrl(hc.url)
}
}
// A sponsor chip — HandleChip minus the avatar circle (sponsors are
// names, not GitHub identities; only the GitHub-sponsor rows carry a
// url and become clickable). NOT a pill (ADR-008): radiusSm + border.
component SponsorChip: Rectangle {
id: sc
property string name: ""
property string url: ""
readonly property bool clickable: sc.url !== ""
height: 28
width: scText.implicitWidth + 24
radius: theme.radiusSm
border.width: 1
border.color: theme.borderSubtle
color: (sc.clickable && scArea.containsMouse) ? theme.surfaceElevatedHover : theme.surfaceElevated
Text {
id: scText
anchors.centerIn: parent
text: sc.name
color: (sc.clickable && scArea.containsMouse) ? theme.accent : theme.textPrimary
font.pixelSize: 13
font.weight: theme.weightMedium
}
MouseArea {
id: scArea
anchors.fill: parent
enabled: sc.clickable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzShell.openExternalUrl(sc.url)
}
}
// A small uppercase-ish section heading (11px semibold, +0.5 tracking).
component SectionHeading: Text {
color: theme.textMuted
font.pixelSize: 11
font.weight: theme.weightSemibold
font.letterSpacing: 0.5
}
// A label/value row of the build-info grid.
component InfoRow: Item {
id: ir
property string label: ""
property string value: ""
property bool italic: false
property color valueColor: theme.textPrimary
height: 18
Text {
id: irLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 84
text: ir.label
color: theme.textMuted
font.pixelSize: 13
}
Text {
anchors.left: irLabel.right
anchors.leftMargin: 12
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: ir.value
color: ir.valueColor
font.pixelSize: 13
font.italic: ir.italic
elide: Text.ElideRight
}
}
// ---- Scrim / shadow / panel ------------------------------------------
Rectangle {
anchors.fill: parent
// Slint `#000000bf` CONVERTED. Floats, not a hex — see divergence 4.
color: Qt.rgba(0, 0, 0, 0.75)
MouseArea {
anchors.fill: parent
onClicked: QbzAbout.aboutClose()
// Wheel-lock (the DiscoverConfigModal rule): a MouseArea consumes
// presses but lets wheel events fall through, so without this the
// page underneath kept scrolling behind the modal.
onWheel: function (wheel) { wheel.accepted = true }
}
}
// Faked 32px drop shadow (`drop-shadow-color: #00000080`) — the
// LogViewerModal/QbzConfirmModal precedent; a real blurred shadow owes its
// own parity pass.
Rectangle {
anchors.centerIn: panel
width: panel.width + 8
height: panel.height + 8
radius: theme.radiusMd
color: Qt.rgba(0, 0, 0, 0.5)
}
Rectangle {
id: panel
anchors.centerIn: parent
// The Slint reference was 840x770 (AboutModal.slint:163-164); grown
// for the Sponsors section (QoL round) so Contributors + Sponsors fit
// without crowding. Still capped by an 80px window margin.
width: Math.min(root.width - 80, 920)
height: Math.min(root.height - 80, 860)
radius: theme.radiusMd
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
// Swallow clicks so they never reach the closing backdrop — and the
// wheel, so a scroll outside the body Flickable (header, margins)
// does not reach the page behind the modal.
MouseArea {
anchors.fill: parent
onWheel: function (wheel) { wheel.accepted = true }
}
Item {
id: stack
anchors.fill: parent
anchors.margins: 24
// ---- Header: branding + close X ---------------------------
Item {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 56
Image {
id: logo
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 56
height: 56
// The 128px source, NOT the 512px qbz-logo.png sitting
// beside it: a ~2.3x downscale renders crisp at 56px where
// the 9x downscale aliases (AboutModal.slint:184-186).
source: "../assets/qbz-logo-128.png"
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
}
Column {
anchors.left: logo.right
anchors.leftMargin: 16
anchors.right: aboutClose.left
anchors.rightMargin: 16
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: "QBZ"
color: theme.textPrimary
font.pixelSize: theme.fontTitle
font.weight: theme.weightBold
}
Text {
text: "v" + (root.doc.version || "")
color: theme.textMuted
font.pixelSize: theme.fontLegal
}
}
Item {
id: aboutClose
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
QbzIcon {
anchors.centerIn: parent
name: "x"
width: 17
height: 17
tintName: aboutCloseArea.containsMouse ? "textPrimary" : "muted"
}
MouseArea {
id: aboutCloseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: QbzAbout.aboutClose()
}
}
}
Rectangle {
id: headerRule
anchors.top: headerRow.bottom
anchors.topMargin: 16
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: theme.borderSubtle
}
// ---- Scrolling body ---------------------------------------
Flickable {
id: flick
anchors.top: headerRule.bottom
anchors.topMargin: 16
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
contentWidth: width
contentHeight: bodyCol.height
boundsBehavior: Flickable.StopAtBounds
Column {
id: bodyCol
width: flick.width
// Description.
Text {
width: parent.width
text: QbzSession.tr("A native music streaming client for Linux, designed for audiophiles who need bit-perfect Hi-Fi playback.", QbzSession.trRev)
color: theme.textSecondary
font.pixelSize: 14
wrapMode: Text.WordWrap
}
Item { width: 1; height: 16 }
// Qobuz legal notice (branding + trademark lines).
Text {
width: parent.width
text: QbzSession.tr("This application uses the Qobuz API but is not certified by Qobuz.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
wrapMode: Text.WordWrap
}
Item { width: 1; height: 8 }
Text {
width: parent.width
text: QbzSession.tr("Qobuz™ is a trademark of Qobuz. QBZ is an open-source application licensed under the MIT License and is not affiliated with, endorsed by, or certified by Qobuz.", QbzSession.trRev)
color: theme.textMuted
// 11px so the trademark notice wraps to two lines
// instead of three.
font.pixelSize: 11
wrapMode: Text.WordWrap
}
Item { width: 1; height: 22 }
// External links. "GitHub" is deliberately NOT translated
// in the reference; the other two are.
Row {
spacing: 12
LinkButton {
label: "GitHub"
onClicked: QbzShell.openExternalUrl("https://github.com/vicrodh/qbz")
}
LinkButton {
label: QbzSession.tr("Release", QbzSession.trRev)
onClicked: QbzShell.openExternalUrl(root.doc.releaseUrl || "")
}
LinkButton {
label: QbzSession.tr("Website", QbzSession.trRev)
iconName: "globe"
onClicked: QbzShell.openExternalUrl("https://qbz.lol")
}
}
Item { width: 1; height: 24 }
// Build info — two label/value columns side by side.
SectionHeading { text: QbzSession.tr("Build Info", QbzSession.trRev) }
Item { width: 1; height: 12 }
Row {
width: parent.width
spacing: 16
Column {
width: (parent.width - 16) / 2
spacing: 6
InfoRow {
width: parent.width
label: QbzSession.tr("Version", QbzSession.trRev)
value: root.doc.version || ""
}
InfoRow {
width: parent.width
label: QbzSession.tr("License", QbzSession.trRev)
// Literal in the reference too (AboutModal.slint:334).
value: "MIT"
}
InfoRow {
width: parent.width
label: QbzSession.tr("Build", QbzSession.trRev)
value: {
var d = root.doc.buildDate || ""
var c = root.doc.buildCommit || ""
return c === "" ? d : d + " (" + c + ")"
}
}
}
Column {
width: (parent.width - 16) / 2
spacing: 6
InfoRow {
width: parent.width
label: QbzSession.tr("Codename", QbzSession.trRev)
// Hand-set literal, on the Rust side
// (about_qt.rs CODENAME) so the release
// checklist has ONE Qt place to bump.
value: root.doc.codename || ""
italic: true
valueColor: theme.textSecondary
}
InfoRow {
width: parent.width
label: QbzSession.tr("Platform", QbzSession.trRev)
value: root.doc.platformLabel || ""
}
}
}
Item { width: 1; height: 24 }
// Acknowledgments — one prose paragraph.
SectionHeading { text: QbzSession.tr("Acknowledgments", QbzSession.trRev) }
Item { width: 1; height: 12 }
Text {
width: parent.width
// The msgid names Slint because it was written for the
// Slint build; this IS the Qt build. "Slint" is a
// proper noun and survives verbatim in all seven
// translated catalogs (checked: es de fr pt ru ja nl),
// so one substitution fixes every locale and adds ZERO
// new msgids to the shared catalog. If a future
// translation ever localises the word the replace is a
// no-op and the string simply reads as it does today.
text: QbzSession.tr("Built with Rust and Slint, and powered by Rodio and Symphonia. With thanks to Qobuz, MusicBrainz, Song.link/Odesli, LRCLIB, lyrics.ovh, and Lucide.", QbzSession.trRev)
.replace("Slint", "Qt")
color: theme.textSecondary
font.pixelSize: 12
wrapMode: Text.WordWrap
}
Item { width: 1; height: 24 }
// Author.
SectionHeading { text: QbzSession.tr("Author", QbzSession.trRev) }
Item { width: 1; height: 12 }
HandleChip {
name: root.doc.authorName || ""
url: root.doc.authorUrl || ""
avatar: root.doc.authorAvatar || ""
textSize: 14
avatarSize: 22
}
Item { width: 1; height: 24 }
// Contributors — `Flow`, see divergence 1 in the header.
SectionHeading { text: QbzSession.tr("Contributors", QbzSession.trRev) }
Item { width: 1; height: 12 }
Flow {
width: parent.width
spacing: 8
Repeater {
model: root.contributors
delegate: HandleChip {
id: contribChip
required property var modelData
name: contribChip.modelData.name || ""
url: contribChip.modelData.url || ""
avatar: contribChip.modelData.avatar || ""
}
}
}
Item { width: 1; height: 24 }
// Sponsors (QoL round) — GitHub sponsors first (linked),
// then Ko-fi supporters by their public display name.
// FIXED-HEIGHT strip of exactly three chip rows
// (3×28 + 2×8); the strip scrolls ITSELF, the modal
// never grows with the list.
SectionHeading { text: QbzSession.tr("Sponsors", QbzSession.trRev) }
Item { width: 1; height: 12 }
Flickable {
width: parent.width
height: 100
clip: true
contentWidth: width
contentHeight: sponsorFlow.height
boundsBehavior: Flickable.StopAtBounds
Flow {
id: sponsorFlow
width: parent.width
spacing: 8
Repeater {
model: root.doc.sponsors || []
delegate: SponsorChip {
id: sponsorChip
required property var modelData
name: sponsorChip.modelData.name || ""
url: sponsorChip.modelData.url || ""
}
}
}
}
Item { width: 1; height: 8 }
Text {
width: parent.width
text: QbzSession.tr("And all those anonymous sponsors...", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
font.italic: true
wrapMode: Text.WordWrap
}
Item { width: 1; height: 24 }
Rectangle { width: parent.width; height: 1; color: theme.borderSubtle }
Item { width: 1; height: 18 }
// ---- Signature ----------------------------------------
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("Made with", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
}
Text {
anchors.verticalCenter: parent.verticalCenter
// NOT translated in the reference, deliberately.
text: "love"
color: theme.textDisabled
font.pixelSize: 12
// Slint fakes this with a hairline Rectangle laid
// over the glyphs; Qt has the real property.
font.strikeout: true
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "hate"
color: theme.textSecondary
font.pixelSize: 12
font.weight: theme.weightSemibold
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: QbzSession.tr("in", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 12
}
// An SVG, never a 🇲🇽 emoji — regional-indicator flags
// degrade to "MX" on many Linux font stacks
// (AboutModal.slint:506-508). The port already carries
// a full ISO-3166 flag set, so this reuses mx.svg
// rather than copying the reference's mexico-flag.svg.
Image {
anchors.verticalCenter: parent.verticalCenter
source: "qrc:/qt/qml/com/blitzfc/qbz/qml/assets/flags/mx.svg"
width: 18
height: 12
fillMode: Image.PreserveAspectFit
sourceSize: Qt.size(36, 24)
}
}
Item { width: 1; height: 8 }
Text {
width: parent.width
text: QbzSession.tr("Hatred towards all those companies that discriminate against the Linux community and don't provide us with a decent client for their product.", QbzSession.trRev)
color: theme.textMuted
font.pixelSize: 11
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item { width: 1; height: 4 }
}
}
// A SIBLING of the Flickable, not an attached ScrollBar — that is
// this port's scrollbar contract (QbzScrollBar.qml:4-6).
QbzScrollBar {
target: flick
anchors.right: parent.right
anchors.top: flick.top
anchors.bottom: flick.bottom
}
}
}
}