feat(qbz-qt): add global compact album headers

cx
This commit is contained in:
vicrodh committed 2026-08-25 20:50:39 -06:00
1 parent 1f15f988cd
commit 1517b4c567
15 files changed
+287 -36

No files matched your search

@@ -5985,3 +5985,15 @@ msgstr "Titel-Wellenform"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Zeigt die Wellenform des gesamten Titels in der Suchleiste des Players an."
msgid "Compact header"
msgstr "Kompakter Kopfbereich"
msgid "Show less data in album headers."
msgstr "Weniger Daten in Albumkopfbereichen anzeigen."
msgid "Show full album header"
msgstr "Vollständigen Albumkopf anzeigen"
msgid "Show compact album header"
msgstr "Kompakten Albumkopf anzeigen"
@@ -1587,3 +1587,15 @@ msgstr "Show the full-track waveform in the player seek bar."
msgid "The typeface used across the app. System follows your operating system; the other choices are bundled with QBZ (requires restart)"
msgstr "The typeface used across the app. System follows your operating system; the other choices are bundled with QBZ (requires restart)"
msgid "Compact header"
msgstr "Compact header"
msgid "Show less data in album headers."
msgstr "Show less data in album headers."
msgid "Show full album header"
msgstr "Show full album header"
msgid "Show compact album header"
msgstr "Show compact album header"
@@ -5991,3 +5991,15 @@ msgstr "Forma de onda de la pista"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Muestra la forma de onda completa de la pista en la barra de búsqueda del reproductor."
msgid "Compact header"
msgstr "Encabezado compacto"
msgid "Show less data in album headers."
msgstr "Muestra menos datos en los encabezados de los álbumes."
msgid "Show full album header"
msgstr "Mostrar el encabezado completo del álbum"
msgid "Show compact album header"
msgstr "Mostrar el encabezado compacto del álbum"
@@ -5983,3 +5983,15 @@ msgstr "Forme d’onde du morceau"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Affiche la forme d’onde complète du morceau dans la barre de lecture."
msgid "Compact header"
msgstr "En-tête compact"
msgid "Show less data in album headers."
msgstr "Afficher moins d’informations dans les en-têtes d’album."
msgid "Show full album header"
msgstr "Afficher l’en-tête complet de l’album"
msgid "Show compact album header"
msgstr "Afficher l’en-tête compact de l’album"
@@ -5967,3 +5967,15 @@ msgstr "トラック波形"
msgid "Show the full-track waveform in the player seek bar."
msgstr "プレーヤーのシークバーにトラック全体の波形を表示します。"
msgid "Compact header"
msgstr "コンパクトヘッダー"
msgid "Show less data in album headers."
msgstr "アルバムヘッダーに表示する情報を減らします。"
msgid "Show full album header"
msgstr "アルバムヘッダーをすべて表示"
msgid "Show compact album header"
msgstr "アルバムヘッダーをコンパクトに表示"
@@ -5973,3 +5973,15 @@ msgstr "Golfvorm van nummer"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Toon de golfvorm van het volledige nummer in de zoekbalk van de speler."
msgid "Compact header"
msgstr "Compacte kop"
msgid "Show less data in album headers."
msgstr "Toon minder gegevens in albumkoppen."
msgid "Show full album header"
msgstr "Volledige albumkop tonen"
msgid "Show compact album header"
msgstr "Compacte albumkop tonen"
@@ -5983,3 +5983,15 @@ msgstr "Forma de onda da faixa"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Mostra a forma de onda completa da faixa na barra de busca do player."
msgid "Compact header"
msgstr "Cabeçalho compacto"
msgid "Show less data in album headers."
msgstr "Mostrar menos dados nos cabeçalhos de álbuns."
msgid "Show full album header"
msgstr "Mostrar cabeçalho completo do álbum"
msgid "Show compact album header"
msgstr "Mostrar cabeçalho compacto do álbum"
@@ -6003,3 +6003,15 @@ msgstr "Форма волны трека"
msgid "Show the full-track waveform in the player seek bar."
msgstr "Показывать форму волны всего трека на шкале воспроизведения."
msgid "Compact header"
msgstr "Компактный заголовок"
msgid "Show less data in album headers."
msgstr "Показывать меньше данных в заголовках альбомов."
msgid "Show full album header"
msgstr "Показать полный заголовок альбома"
msgid "Show compact album header"
msgstr "Показать компактный заголовок альбома"
@@ -117,6 +117,14 @@ Column {
onToggled: function (v) { QbzBridge.settingsBool("album-header-gradient", v) }
}
}
SettingRow {
label: QbzSession.tr("Compact header", QbzSession.trRev)
description: QbzSession.tr("Show less data in album headers.", QbzSession.trRev)
QbzToggle {
checked: root.doc.compactAlbumHeader === true
onToggled: function (v) { QbzBridge.settingsBool("compact-album-header", v) }
}
}
SettingRow {
label: QbzSession.tr("Dynamic background", QbzSession.trRev)
description: QbzSession.tr("Animated album-art background behind the whole app. High resource use — GPU accelerated.", QbzSession.trRev)
+82 -16
View File
@@ -48,7 +48,7 @@ Rectangle {
/// twin and both text columns all measure off it — they drifted apart at
/// five separate literals before. The Row has no explicit height, so the
/// header grows with this on its own.
readonly property int coverPx: 260
readonly property int coverPx: compactHeaderPref ? 130 : 260
/// How far past the header divider the artwork atmosphere reaches: the 8px
/// spacer below the divider plus half of the 52px track toolbar band, so
@@ -106,6 +106,21 @@ Rectangle {
readonly property var albumHeader: album.header || ({})
readonly property var tracks: album.tracks || []
// One global opt-in for every album page. `settingsJson` is the live
// channel (including clicks made in another open AlbumView); the album
// document carries the persisted cold-start fallback until that snapshot
// arrives.
readonly property bool compactHeaderPref: {
var raw = QbzBridge.settingsJson
if (raw && raw.length > 2) {
try {
var d = JSON.parse(raw)
if (d.compactAlbumHeader !== undefined)
return d.compactAlbumHeader === true
} catch (e) { /* fall through to the document copy */ }
}
return album.compactHeader === true
}
// `header.awards`, NOT `album.awards`. It is a field of AlbumHeader
// (src/album_qt.rs:171), not of AlbumViewData — read one level too high it
// is `undefined`, `|| []` swallows that, and the AWARDS block simply never
@@ -785,13 +800,36 @@ Rectangle {
anchors.topMargin: 4
spacing: 0
Text {
Item {
width: parent.width
text: albumHeader.title || ""
color: root.hdrStrong
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
height: Math.max(albumTitle.implicitHeight, 32)
Text {
id: albumTitle
anchors.left: parent.left
anchors.right: headerModeButton.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: albumHeader.title || ""
color: root.hdrStrong
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
QbzCircleAction {
id: headerModeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
name: root.compactHeaderPref ? "maximize-2" : "minimize-2"
overlay: root.hdrOverlay
onClicked: QbzBridge.settingsBool(
"compact-album-header", !root.compactHeaderPref)
HoverHandler { id: headerModeHover }
ToolTip.visible: headerModeHover.hovered
ToolTip.text: root.compactHeaderPref
? QbzSession.tr("Show full album header", QbzSession.trRev)
: QbzSession.tr("Show compact album header", QbzSession.trRev)
ToolTip.delay: 350
}
}
Item { width: 1; height: 4 }
// Credited-artist line (links + role suffixes).
@@ -799,7 +837,9 @@ Rectangle {
width: parent.width
spacing: 0
Repeater {
model: albumHeader.credits || []
model: root.compactHeaderPref
? [[albumHeader.artist || "", albumHeader.artistId || "", ""]]
: (albumHeader.credits || [])
delegate: Row {
required property var modelData
required property int index
@@ -834,11 +874,13 @@ Rectangle {
}
}
}
Item { width: 1; height: 10 }
Item { width: 1; height: root.compactHeaderPref ? 4 : 10 }
// Meta line (label as a clickable link when navigable).
Row {
spacing: 0
visible: (albumHeader.labelId || "") !== "" && (albumHeader.label || "") !== ""
visible: !root.compactHeaderPref
&& (albumHeader.labelId || "") !== ""
&& (albumHeader.label || "") !== ""
Text {
visible: (albumHeader.metaPre || "") !== ""
text: (albumHeader.metaPre || "") + " • "
@@ -865,27 +907,51 @@ Rectangle {
}
}
Text {
visible: (albumHeader.labelId || "") === "" || (albumHeader.label || "") === ""
visible: !root.compactHeaderPref
&& ((albumHeader.labelId || "") === ""
|| (albumHeader.label || "") === "")
width: parent.width
text: albumHeader.infoLine || ""
color: root.hdrBody
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
Text {
visible: root.compactHeaderPref
width: parent.width
text: albumHeader.compactInfoLine || ""
color: root.hdrBody
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
// Editorial description + Read more.
Item { visible: (albumHeader.description || "") !== ""; width: 1; height: 12 }
Item {
visible: !root.compactHeaderPref
&& (albumHeader.description || "") !== ""
width: 1
height: 12
}
Text {
visible: (albumHeader.description || "") !== ""
visible: !root.compactHeaderPref
&& (albumHeader.description || "") !== ""
width: parent.width
text: albumHeader.descriptionShort || ""
color: root.hdrBody
font.pixelSize: theme.fontLegal
wrapMode: Text.WordWrap
}
Item { visible: (albumHeader.description || "") !== (albumHeader.descriptionShort || ""); width: 1; height: 4 }
Item {
visible: !root.compactHeaderPref
&& (albumHeader.description || "")
!== (albumHeader.descriptionShort || "")
width: 1
height: 4
}
Text {
visible: (albumHeader.description || "") !== (albumHeader.descriptionShort || "")
visible: !root.compactHeaderPref
&& (albumHeader.description || "")
!== (albumHeader.descriptionShort || "")
text: QbzSession.tr("Read more", QbzSession.trRev)
color: readMoreArea.containsMouse ? theme.accentHover : theme.accent
font.pixelSize: theme.fontLegal
@@ -902,7 +968,7 @@ Rectangle {
}
}
Item { width: 1; height: 20 }
Item { width: 1; height: root.compactHeaderPref ? 8 : 20 }
// Action row — AlbumPageView.slint:504-640. One shared
// CircleAction for every button including Play (the
// hand-rolled 44px disc it used to be drifted from the
+29 -3
View File
@@ -44,6 +44,20 @@ Rectangle {
readonly property var album: doc ? doc.album : null
readonly property var tracks: doc ? doc.tracks : []
readonly property var versions: album && album.versions ? album.versions : []
// Same global preference as the Qobuz AlbumView. The document value is
// only the cold-start fallback; settingsJson makes a toggle in any open
// album update every other album immediately.
readonly property bool compactHeaderPref: {
var raw = QbzBridge.settingsJson
if (raw && raw.length > 2) {
try {
var d = JSON.parse(raw)
if (d.compactAlbumHeader !== undefined)
return d.compactAlbumHeader === true
} catch (e) { /* fall through to the document copy */ }
}
return doc && doc.compactHeader === true
}
readonly property var allArtists: {
if (!album) return []
var raw = (album.allArtists || "").split(",")
@@ -66,6 +80,14 @@ Rectangle {
if ((album.format || "") !== "") parts.push(album.format.toUpperCase())
return parts.join(" • ")
}
readonly property string compactInfoLine: {
if (!album) return ""
var parts = []
if ((album.year || "") !== "") parts.push(album.year)
parts.push((album.trackCount || 0) + " "
+ QbzSession.tr("tracks", QbzSession.trRev))
return parts.join(" • ")
}
property string trackQuery: ""
// Client-side track search: an album's track list is bounded, so the
@@ -378,9 +400,9 @@ Rectangle {
visible: root.pageLoading && !root.album
variant: "header"
width: parent.width
height: 224
coverSize: 224
coverGap: 32
height: root.compactHeaderPref ? 128 : 224
coverSize: root.compactHeaderPref ? 128 : 224
coverGap: root.compactHeaderPref ? 20 : 32
actionCount: 5
phase: root.skelPhase
}
@@ -390,12 +412,16 @@ Rectangle {
album: root.album
allArtists: root.allArtists
infoLine: root.infoLine
compactInfoLine: root.compactInfoLine
compact: root.compactHeaderPref
versions: root.versions
coverSource: root.album ? (root.artMap[root.album.artKey] || "") : ""
coverPending: root.coverPending
skelPhase: root.skelPhase
artSettleMs: root.artSettleMs
onOpenArtist: function (name) { root.openArtist(name) }
onCompactToggled: QbzBridge.settingsBool(
"compact-album-header", !root.compactHeaderPref)
}
Item { width: 1; height: 20 }
@@ -7,6 +7,7 @@
// Slint watches its own mirror property for exactly this (:156).
import QtQuick
import QtQuick.Controls
import com.blitzfc.qbz
import "../../controls"
import "../../theme"
@@ -18,6 +19,8 @@ Row {
/// De-duplicated contributor list (>1 turns the expander on).
property var allArtists: []
property string infoLine: ""
property string compactInfoLine: ""
property bool compact: false
property var versions: []
property string coverSource: ""
/// Per-item cover state, owned by the page (LocalAlbumView).
@@ -33,17 +36,19 @@ Row {
property bool skelPhase: false
property int artSettleMs: 0
signal openArtist(string name)
signal compactToggled()
QbzTheme { id: theme }
property bool artistsExpanded: false
onAlbumChanged: artistsExpanded = false
spacing: 32
readonly property int coverPx: compact ? 128 : 224
spacing: compact ? 20 : 32
Rectangle {
width: 224
height: 224
width: root.coverPx
height: root.coverPx
radius: 12
color: theme.surfaceElevated
clip: true
@@ -68,17 +73,38 @@ Row {
}
Column {
width: parent.width - 224 - 32
width: parent.width - root.coverPx - root.spacing
topPadding: 4
spacing: 0
Text {
Item {
width: parent.width
text: root.album ? (root.album.title || "") : ""
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
height: Math.max(localAlbumTitle.implicitHeight, 32)
Text {
id: localAlbumTitle
anchors.left: parent.left
anchors.right: localHeaderModeButton.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root.album ? (root.album.title || "") : ""
color: theme.textPrimary
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
elide: Text.ElideRight
}
QbzCircleAction {
id: localHeaderModeButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
name: root.compact ? "maximize-2" : "minimize-2"
onClicked: root.compactToggled()
HoverHandler { id: localHeaderModeHover }
ToolTip.visible: localHeaderModeHover.hovered
ToolTip.text: root.compact
? QbzSession.tr("Show full album header", QbzSession.trRev)
: QbzSession.tr("Show compact album header", QbzSession.trRev)
ToolTip.delay: 350
}
}
Item { width: 1; height: 4 }
Row {
@@ -99,7 +125,7 @@ Row {
// Multi-artist album (compilations): "+N more artists" expands
// the full track-artist list below.
Text {
visible: root.allArtists.length > 1
visible: !root.compact && root.allArtists.length > 1
anchors.verticalCenter: parent.verticalCenter
text: root.artistsExpanded
? QbzSession.tr("Show less", QbzSession.trRev)
@@ -119,7 +145,8 @@ Row {
// Expanded track-artist list — each entry routes to the Local
// Library Artists tab (name-based).
Column {
visible: root.artistsExpanded && root.allArtists.length > 1
visible: !root.compact && root.artistsExpanded
&& root.allArtists.length > 1
width: parent.width
topPadding: 6
spacing: 0
@@ -143,15 +170,15 @@ Row {
}
}
}
Item { width: 1; height: 10 }
Item { width: 1; height: root.compact ? 4 : 10 }
Text {
width: parent.width
text: root.infoLine
text: root.compact ? root.compactInfoLine : root.infoLine
color: theme.textSecondary
font.pixelSize: theme.fontBody
elide: Text.ElideRight
}
Item { width: 1; height: 20 }
Item { width: 1; height: root.compact ? 8 : 20 }
// ---- Local action row ----
Row {
@@ -186,12 +213,12 @@ Row {
// ---- Version picker (multiple physical copies only) ----
Item {
visible: root.versions.length > 1
visible: !root.compact && root.versions.length > 1
width: 1
height: visible ? 16 : 0
}
Row {
visible: root.versions.length > 1
visible: !root.compact && root.versions.length > 1
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
+16
View File
@@ -166,6 +166,10 @@ pub struct AlbumHeader {
pub credits: Vec<(String, String, String)>,
#[serde(rename = "infoLine")]
pub info_line: String,
/// Compact header metadata: release date + track count only. Label,
/// genre and duration intentionally stay in the expanded header.
#[serde(rename = "compactInfoLine")]
pub compact_info_line: String,
#[serde(rename = "metaPre")]
pub meta_pre: String,
#[serde(rename = "metaPost")]
@@ -234,6 +238,11 @@ pub struct AlbumHeader {
pub struct AlbumViewData {
pub header: AlbumHeader,
pub tracks: Vec<TrackRow>,
/// Cold-start fallback for the live `settingsJson` preference consumed by
/// AlbumView.qml. The view switches to the settings document as soon as it
/// is published, so toggling one open album updates every other instance.
#[serde(rename = "compactHeader")]
pub compact_header: bool,
#[serde(rename = "moreFromArtist")]
pub more_from_artist: Vec<AlbumCardData>,
pub suggestions: Vec<AlbumCardData>,
@@ -601,6 +610,11 @@ pub async fn load_album(
}
let meta_pre = pre_parts.join(" • ");
let meta_post = post_parts.join(" • ");
let mut compact_parts = pre_parts.clone();
if let Some(tc) = &tracks_str {
compact_parts.push(tc.clone());
}
let compact_info_line = compact_parts.join(" • ");
let mut all_parts = pre_parts.clone();
if let Some(l) = &label_name {
all_parts.push(l.clone());
@@ -679,6 +693,7 @@ pub async fn load_album(
artist_id,
credits,
info_line,
compact_info_line,
meta_pre,
meta_post,
quality_tier: home_qt::quality_tier_from_depth(album.maximum_bit_depth).to_string(),
@@ -1229,6 +1244,7 @@ pub async fn load_album_view(
// lands as a patch a moment later. `headerColor` stays "" until then and
// the band simply does not paint — no colour pop.
data.header_gradient = crate::settings_qt::pref_bool("album_header_gradient", true);
data.compact_header = crate::settings_qt::pref_bool("compact_album_header", false);
let header_art = data.header.artwork_url.clone();
let json = serde_json::to_string(&data).map_err(|e| e.to_string())?;
+5
View File
@@ -116,6 +116,10 @@ pub struct AlbumDetailDoc {
/// EMPTY for a single-disc album. Present only so a multi-disc box can
/// label and illustrate its dividers — see `DiscRow`.
pub discs: Vec<DiscRow>,
/// Cold-start fallback for the global compact-header appearance pref.
/// LocalAlbumView switches to the live settings document once published.
#[serde(rename = "compactHeader")]
pub compact_header: bool,
}
/// The bounded payload consumed by one expanded row in the Genres browser.
@@ -346,6 +350,7 @@ pub fn version_doc(id: &str, index: usize) -> Option<AlbumDetailDoc> {
},
tracks: rows,
discs,
compact_header: crate::settings_qt::pref_bool("compact_album_header", false),
})
}
+7
View File
@@ -1658,6 +1658,8 @@ pub struct SettingsDoc {
// Appearance (phase 19; the ui_prefs defaults mirror ui_prefs.rs)
#[serde(rename = "albumHeaderGradient")]
pub album_header_gradient: bool,
#[serde(rename = "compactAlbumHeader")]
pub compact_album_header: bool,
#[serde(rename = "appBackgroundModes")]
pub app_background_modes: Vec<String>,
#[serde(rename = "appBackgroundIndex")]
@@ -2111,6 +2113,7 @@ pub async fn publish_snapshot() {
qconnect_device_name: qconnect_load_device_name().unwrap_or_default(),
qconnect_device_name_default: qconnect_default_name(),
album_header_gradient: pref_bool("album_header_gradient", true),
compact_album_header: pref_bool("compact_album_header", false),
app_background_modes: APP_BACKGROUND_LABELS
.iter()
.map(|l| qbz_i18n::t(l))
@@ -2401,6 +2404,10 @@ pub async fn settings_bool(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &str,
save_pref("album_header_gradient", serde_json::json!(value));
Ok(Apply::None)
}
"compact-album-header" => {
save_pref("compact_album_header", serde_json::json!(value));
Ok(Apply::None)
}
"intelligent-search" => {
save_pref("intelligent_search", serde_json::json!(value));
// Flip the LIVE kill switch too, not just the pref: the service is