**Deferred external rows.** The album page awaited the Last.fm row before publishing anything, so the tracklist waited on an external service plus MusicBrainz. It publishes in stages now — header and tracks first, then each Qobuz rail as it lands, then Last.fm — with every patch merged under a generation counter so a late reply for album A cannot paint onto album B. The artist page was already staged this way; its views were the part still showing bare "Loading…" text. **Skeletons as the standard.** One control, new variants rather than new components, mounted across the views. Its second job is the one the owner named: cleanup. A cover that never resolves settles the placeholder once and reveals the card's own empty state instead of shimmering forever — local albums with no embedded art produce no artwork entry at all, so a placeholder gated only on "not arrived yet" would never stop. **Local artwork streams.** Fifteen seconds to first cover on a 1274-album grid was one batch emitted after the whole window resolved. Each cover emits as it resolves now, visible band before prefetch margin. **Lyrics migrated.** Smooth sync instead of a 1 Hz tick, word-level karaoke where the document carries it, the surrounding-line dimming, the controls flyout and the translation layer the v10 backend already supported. Exactly one row subscribes to the 30 Hz output; the timer drops to 250ms when paused or hidden. **Navigation by mouse and touchpad.** Side buttons and a two-finger horizontal swipe, matching #653 — including the part that matters, which is that a swipe over a carousel scrolls the carousel. The layer is declared first in the shell so everything else gets first refusal. **Context menus.** Brought to the Slint inventory, Play later included, with right-click opening the same menu as the button everywhere. Entries whose seam does not exist are hidden rather than shipped inert. Two controls that rendered and no-opped are now honestly disabled: the My QBZ nav row, which routes to a view the shell has no case for, and three sidebar menu entries with no backing call. The sidebar caret was never a condition bug. The row is a positioner and the name cell's width ignored the count label, so a folder WITH content pushed the chevron past the clip — which is why it vanished exactly when it mattered. cc
371 lines
13 KiB
QML
371 lines
13 KiB
QML
// Lyrics display-settings flyout — QML port of
|
|
// crates/qbz-ui/ui/shell/LyricsControlsFlyout.slint (`LyricsControlsPanel`).
|
|
//
|
|
// Row order is the reference's exactly: Auto-follow / Font / Size /
|
|
// Translation language / Active color / Uppercase / Lite fill / Dimming /
|
|
// footer (Copy lyrics + Reset). Selects instead of segmented pill rows
|
|
// (ADR-008), and the active color is a fixed swatch palette because there is
|
|
// no native color input (deviation D6) — the "Theme" reset link shows only
|
|
// while a custom color is set.
|
|
//
|
|
// Every mutation writes the QbzLyrics pref property directly (live preview)
|
|
// and fires `prefsChanged()` so Rust persists it into the SAME per-user
|
|
// lyrics_prefs.json the Slint build reads. Changing the translation language
|
|
// while the translation is ON re-requests the track in the new language
|
|
// (Rust side) — the row is never a dead control.
|
|
//
|
|
// This component is the popup CONTENT; the Popup shell lives at the anchor
|
|
// site in LyricsPanel.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
import "../controls"
|
|
|
|
Rectangle {
|
|
id: panel
|
|
|
|
// Copy gate — ready && lines > 0 (the reference's `can-copy`).
|
|
property bool canCopy: false
|
|
|
|
readonly property int rowWidth: 232 // 260 - 2 * 14 padding
|
|
readonly property var swatches: [
|
|
"#8b5cf6", "#ec4899", "#ef4444", "#f97316",
|
|
"#eab308", "#22c55e", "#06b6d4", "#3b82f6"
|
|
]
|
|
|
|
implicitWidth: 260
|
|
implicitHeight: column.implicitHeight
|
|
color: theme.surfaceMain
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderMuted
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// Clipboard seam: this shell has no Rust clipboard bridge, so the copy
|
|
// goes through the standard QML route (a hidden TextEdit's copy()).
|
|
TextEdit {
|
|
id: clipboard
|
|
visible: false
|
|
width: 0
|
|
height: 0
|
|
}
|
|
|
|
function copyLyrics() {
|
|
var text = QbzLyrics.plainText()
|
|
if (text === "")
|
|
return
|
|
clipboard.text = text
|
|
clipboard.selectAll()
|
|
clipboard.copy()
|
|
clipboard.text = ""
|
|
QbzLyrics.notice = QbzSession.tr("Lyrics copied", QbzSession.trRev)
|
|
}
|
|
|
|
Column {
|
|
id: column
|
|
width: parent.width
|
|
padding: 14
|
|
spacing: 12
|
|
|
|
// --- Auto-follow ------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 22
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: afToggle.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Auto-follow", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzToggle {
|
|
id: afToggle
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: QbzLyrics.autoFollow
|
|
onToggled: function (value) {
|
|
QbzLyrics.autoFollow = value
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Font --------------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 34
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: fontSelect.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Font", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzSelect {
|
|
id: fontSelect
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
menuWidth: 150
|
|
options: [
|
|
QbzSession.tr("System", QbzSession.trRev),
|
|
"LINE Seed JP",
|
|
"Montserrat",
|
|
"Noto Sans",
|
|
"Source Sans 3"
|
|
]
|
|
currentIndex: QbzLyrics.fontIndex
|
|
onSelected: function (i) {
|
|
QbzLyrics.fontIndex = i
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Size --------------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 34
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: sizeSelect.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Size", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzSelect {
|
|
id: sizeSelect
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
menuWidth: 110
|
|
options: ["S", "M", "L", "XL"]
|
|
currentIndex: QbzLyrics.sizeIndex
|
|
onSelected: function (i) {
|
|
QbzLyrics.sizeIndex = i
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Translation language (Qobuz v10) ----------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 34
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: langSelect.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Translation language", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzSelect {
|
|
id: langSelect
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
menuWidth: 118
|
|
popupWidth: 178
|
|
options: [
|
|
QbzSession.tr("Auto (account language)", QbzSession.trRev),
|
|
"English", "Español", "Français", "Deutsch", "Italiano",
|
|
"Português", "Nederlands", "日本語", "Русский"
|
|
]
|
|
currentIndex: QbzLyrics.translationLanguageIndex
|
|
onSelected: function (i) {
|
|
QbzLyrics.translationLanguageIndex = i
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Active color -------------------------------------------------
|
|
Column {
|
|
width: panel.rowWidth
|
|
spacing: 8
|
|
Item {
|
|
width: parent.width
|
|
height: 18
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Active color", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
}
|
|
Text {
|
|
visible: QbzLyrics.useCustomColor
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("Theme", QbzSession.trRev)
|
|
color: themeLinkArea.containsMouse ? theme.accentHover : theme.accent
|
|
font.pixelSize: 12
|
|
MouseArea {
|
|
id: themeLinkArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
QbzLyrics.useCustomColor = false
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Row {
|
|
anchors.right: parent.right
|
|
spacing: 6
|
|
Repeater {
|
|
model: panel.swatches
|
|
delegate: Rectangle {
|
|
id: sw
|
|
required property var modelData
|
|
readonly property bool picked: QbzLyrics.useCustomColor
|
|
&& Qt.colorEqual(QbzLyrics.customColor, sw.modelData)
|
|
width: 18
|
|
height: 18
|
|
radius: 9
|
|
color: sw.modelData
|
|
border.width: sw.picked ? 2 : (swArea.containsMouse ? 1 : 0)
|
|
border.color: sw.picked ? theme.textPrimary : theme.borderMuted
|
|
MouseArea {
|
|
id: swArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
QbzLyrics.useCustomColor = true
|
|
QbzLyrics.customColor = sw.modelData
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Uppercase -----------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 22
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: upToggle.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Uppercase", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzToggle {
|
|
id: upToggle
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: QbzLyrics.uppercase
|
|
onToggled: function (value) {
|
|
QbzLyrics.uppercase = value
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Lite fill (perf) -----------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 22
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: liteToggle.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Lite fill (saves CPU)", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzToggle {
|
|
id: liteToggle
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: QbzLyrics.liteFill
|
|
onToggled: function (value) {
|
|
QbzLyrics.liteFill = value
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Dimming --------------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 34
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: dimSelect.left
|
|
anchors.rightMargin: 8
|
|
height: parent.height
|
|
text: QbzSession.tr("Dimming", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: 12
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
QbzSelect {
|
|
id: dimSelect
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
menuWidth: 120
|
|
options: [
|
|
QbzSession.tr("Off", QbzSession.trRev),
|
|
QbzSession.tr("Soft", QbzSession.trRev),
|
|
QbzSession.tr("Strong", QbzSession.trRev)
|
|
]
|
|
currentIndex: QbzLyrics.dimmingMode
|
|
onSelected: function (i) {
|
|
QbzLyrics.dimmingMode = i
|
|
QbzLyrics.prefsChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Footer ----------------------------------------------------------
|
|
Item {
|
|
width: panel.rowWidth
|
|
height: 34
|
|
Row {
|
|
anchors.right: parent.right
|
|
spacing: 8
|
|
SettingsButton {
|
|
width: 112
|
|
text: QbzSession.tr("Copy lyrics", QbzSession.trRev)
|
|
enabled: panel.canCopy
|
|
onClicked: panel.copyLyrics()
|
|
}
|
|
SettingsButton {
|
|
width: 84
|
|
text: QbzSession.tr("Reset", QbzSession.trRev)
|
|
onClicked: QbzLyrics.resetPrefs()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|