diff --git a/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po index e3cba3cff..3cda7fea6 100644 --- a/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po @@ -4765,3 +4765,9 @@ msgstr "Album-Gruppierung" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Ordner: eine Karte pro Album-Ordner — ideal für Compilations und Boxsets. Metadaten: nach Album- + Künstler-Tags aufteilen." + +msgid "+{} more artists" +msgstr "+{} weitere Künstler" + +msgid "Show less" +msgstr "Weniger anzeigen" diff --git a/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po index 1303549ae..edd8915b4 100644 --- a/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po @@ -558,3 +558,9 @@ msgstr "Album grouping" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." + +msgid "+{} more artists" +msgstr "+{} more artists" + +msgid "Show less" +msgstr "Show less" diff --git a/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po index 8fbc23248..8b24a03c3 100644 --- a/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po @@ -4795,3 +4795,9 @@ msgstr "Agrupación de álbumes" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Carpetas: una tarjeta por carpeta de álbum — ideal para compilaciones y box sets. Metadatos: separar por etiquetas de álbum + artista." + +msgid "+{} more artists" +msgstr "+{} artistas más" + +msgid "Show less" +msgstr "Ver menos" diff --git a/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po index 9990a64e1..c0bcd6323 100644 --- a/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po @@ -4765,3 +4765,9 @@ msgstr "Regroupement des albums" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Dossiers : une carte par dossier d'album — idéal pour les compilations et coffrets. Métadonnées : séparer par tags album + artiste." + +msgid "+{} more artists" +msgstr "+{} artistes de plus" + +msgid "Show less" +msgstr "Afficher moins" diff --git a/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po index 1d7e96e64..99eba1cb8 100644 --- a/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po @@ -4753,3 +4753,9 @@ msgstr "アルバムのグループ化" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "フォルダー: アルバムフォルダーごとに1枚のカード — コンピレーションやボックスセットに最適。メタデータ: アルバム+アーティストのタグで分割。" + +msgid "+{} more artists" +msgstr "他 {} 人のアーティスト" + +msgid "Show less" +msgstr "表示を減らす" diff --git a/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po index 6a5ae84cb..c5af5e08f 100644 --- a/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po @@ -4777,3 +4777,9 @@ msgstr "Albumgroepering" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Mappen: één kaart per albummap — ideaal voor compilaties en boxsets. Metadata: splitsen op album- + artiesttags." + +msgid "+{} more artists" +msgstr "+{} artiesten meer" + +msgid "Show less" +msgstr "Minder tonen" diff --git a/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po index 617f6b274..68eb0c318 100644 --- a/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po @@ -4765,3 +4765,9 @@ msgstr "Agrupamento de álbuns" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Pastas: um cartão por pasta de álbum — ideal para compilações e box sets. Metadados: dividir por etiquetas de álbum + artista." + +msgid "+{} more artists" +msgstr "+{} artistas mais" + +msgid "Show less" +msgstr "Mostrar menos" diff --git a/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po b/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po index f88faecb0..8affcc07f 100644 --- a/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po @@ -4779,3 +4779,9 @@ msgstr "Группировка альбомов" msgid "Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags." msgstr "Папки: одна карточка на папку альбома — лучше для сборников и бокс-сетов. Метаданные: разделение по тегам альбома + исполнителя." + +msgid "+{} more artists" +msgstr "+ ещё {} исполнителей" + +msgid "Show less" +msgstr "Показать меньше" diff --git a/crates/qbz-ui/ui/album/LocalAlbumView.slint b/crates/qbz-ui/ui/album/LocalAlbumView.slint index c6b708617..b7f944d38 100644 --- a/crates/qbz-ui/ui/album/LocalAlbumView.slint +++ b/crates/qbz-ui/ui/album/LocalAlbumView.slint @@ -150,6 +150,14 @@ export component LocalAlbumView inherits Rectangle { callback open-artist(string); background: ShellState.app-background-active ? transparent : Theme.surface-main; + // Expanded multi-artist header list (spec §B2) — view-local, resets when + // another album opens (watched via the mirror property below). + property artists-expanded: false; + property watched-album-id: LocalAlbumState.id; + changed watched-album-id => { + root.artists-expanded = false; + } + // Homologated 1:1 with the Qobuz AlbumPageView: whole-page Flickable, the // same header proportions + action row, the toolbar (quality badge + track // search), the column header, and source-aware TrackRows. The only @@ -238,6 +246,7 @@ export component LocalAlbumView inherits Rectangle { Rectangle { height: 4px; } HorizontalLayout { alignment: start; + spacing: 8px; la-artist := TouchArea { width: la-artist-text.preferred-width; height: la-artist-text.preferred-height; @@ -254,6 +263,46 @@ export component LocalAlbumView inherits Rectangle { font-weight: Typography.bold; } } + // Multi-artist album (compilations): a "+N more + // artists" chevron expands the full track-artist list + // below (spec §B2). Single-artist albums never see it. + if LocalAlbumState.all-artists.length > 1: VerticalLayout { + alignment: center; + more-ta := TouchArea { + width: more-text.preferred-width; + height: more-text.preferred-height; + mouse-cursor: pointer; + clicked => { + root.artists-expanded = !root.artists-expanded; + } + more-text := Text { + text: root.artists-expanded + ? @tr("Show less") + : @tr("+{} more artists", LocalAlbumState.all-artists.length - 1); + color: more-ta.has-hover ? Theme.accent : Theme.text-muted; + font-size: Typography.body; + } + } + } + } + // Expanded track-artist list — each entry routes to the + // LocalLibrary Artists tab (name-based open-artist). + if root.artists-expanded && LocalAlbumState.all-artists.length > 1: VerticalLayout { + spacing: 0px; + padding-top: 6px; + for artist in LocalAlbumState.all-artists: TouchArea { + height: 24px; + mouse-cursor: pointer; + clicked => { + root.open-artist(artist); + } + Text { + text: artist; + color: parent.has-hover ? Theme.accent : Theme.text-secondary; + font-size: Typography.body; + vertical-alignment: center; + } + } } Rectangle { height: 10px; } Text { diff --git a/crates/qbz-ui/ui/state.slint b/crates/qbz-ui/ui/state.slint index c8191ecb2..ddfd732f1 100644 --- a/crates/qbz-ui/ui/state.slint +++ b/crates/qbz-ui/ui/state.slint @@ -1456,6 +1456,10 @@ export global LocalAlbumState { in property quality-detail; // "24-bit / 96 kHz" in property cover-url; in property cover; + // Distinct track artists in first-appearance order (compilations); the + // header shows the single artist line + a "+N more artists" chevron that + // expands into the clickable full list (spec §B2). Empty/1 = no chevron. + in property <[string]> all-artists: []; in property loading: false; in property <[TrackItem]> tracks: []; // Versions (distinct copies). >1 -> show the picker. Sorted best-quality diff --git a/crates/qbz/src/local_library.rs b/crates/qbz/src/local_library.rs index 6d9f24393..c0bcaa90e 100644 --- a/crates/qbz/src/local_library.rs +++ b/crates/qbz/src/local_library.rs @@ -1942,6 +1942,16 @@ pub fn apply_album_version(window: &AppWindow, index: i32) { None => String::new(), }; let total_secs: u64 = tracks.iter().map(|t| t.duration_secs).sum(); + // Distinct track artists, first-appearance order — the collapsable + // multi-artist header list (spec §B2). Raw `artist` (NOT album_artist): + // a compilation's 10 track artists are the list's whole point. + let mut all_artists: Vec = Vec::new(); + for t in tracks { + let a = t.artist.trim(); + if !a.is_empty() && !all_artists.iter().any(|x| x.as_str() == a) { + all_artists.push(a.into()); + } + } let track_count = qbz_i18n::tf("{} track", "{} tracks", tracks.len() as i64, &[&tracks.len().to_string()]); let info_line = format!("{} · {}", track_count, fmt_album_duration(total_secs)); let (tier, detail) = match tracks.iter().max_by_key(|t| t.bit_depth.unwrap_or(0)) { @@ -2004,6 +2014,7 @@ pub fn apply_album_version(window: &AppWindow, index: i32) { .collect(); s.set_title(title.into()); s.set_artist(artist.into()); + s.set_all_artists(ModelRc::new(VecModel::from(all_artists))); s.set_info_line(info_line.into()); s.set_quality_tier(tier.into()); s.set_quality_detail(detail.into()); @@ -3519,6 +3530,10 @@ pub fn ensure_artists_loaded( // branch is skipped and the path is byte-for-byte the pre-Plex flow. let plex_enabled = crate::plex_settings::get().enabled; let plex_path = plex_cache_db_path(); + // Album-identity mode for the album cache below — the Artists tab + // must group albums the same way as the Albums tab (a folder-mode + // compilation cross-lists under every artist in all_artists). + let group_mode = current_group_mode(&w); handle.spawn(async move { let items = tokio::task::spawn_blocking(move || { // Same network flag as every browse tab: connectivity-keyed