merge: integrate the QoL batch (label column, unavailable-release page, Copy submenus, Buy on Qobuz) into pre-release
cc
This commit is contained in:
28 files changed
+961
-22
No files matched your search
@@ -3863,6 +3863,7 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
Album {
|
Album {
|
||||||
favorited_at: None,
|
favorited_at: None,
|
||||||
|
purchasable: None,
|
||||||
id: String::new(),
|
id: String::new(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
artist: Artist {
|
artist: Artist {
|
||||||
@@ -3900,6 +3901,7 @@ mod tests {
|
|||||||
fn track_with(performer_id: Option<u64>, composer_id: Option<u64>) -> Track {
|
fn track_with(performer_id: Option<u64>, composer_id: Option<u64>) -> Track {
|
||||||
Track {
|
Track {
|
||||||
favorited_at: None,
|
favorited_at: None,
|
||||||
|
purchasable: None,
|
||||||
id: 0,
|
id: 0,
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
version: None,
|
version: None,
|
||||||
|
|||||||
@@ -7647,3 +7647,51 @@ msgstr "Basisdaten kopieren"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Alles kopieren"
|
msgstr "Alles kopieren"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Label anzeigen"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Bei Qobuz kaufen"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Diese Veröffentlichung wird bei Qobuz nicht verkauft."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Der Store konnte für diese Veröffentlichung nicht abgefragt werden."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Der Browser konnte nicht geöffnet werden"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Titelname"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Titel - Album - Künstler"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Diese Veröffentlichung ist bei Qobuz nicht mehr verfügbar."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Stattdessen könnten dich diese interessieren"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Diese Veröffentlichung konnte nicht geladen werden."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Suche nach anderen Versionen…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Diese Veröffentlichung"
|
||||||
@@ -3302,3 +3302,51 @@ msgstr "Copy basic data"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Copy full"
|
msgstr "Copy full"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "View label"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Buy on Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "This release isn't sold on Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Couldn't check the store for this release."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Could not open the browser"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Track name"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Track - Album - Artist"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "This release is no longer available on Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "You might be interested in these instead"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Couldn't load this release."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Looking for other versions…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "This release"
|
||||||
@@ -7653,3 +7653,51 @@ msgstr "Copiar datos básicos"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Copiar todo"
|
msgstr "Copiar todo"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Ver sello"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Comprar en Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Este lanzamiento no se vende en Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "No se pudo consultar la tienda para este lanzamiento."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "No se pudo abrir el navegador"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Nombre de la pista"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Pista - Álbum - Artista"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Este lanzamiento ya no está disponible en Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Quizá te interesen estas alternativas"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "No se pudo cargar este lanzamiento."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Buscando otras versiones…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Este lanzamiento"
|
||||||
@@ -7645,3 +7645,51 @@ msgstr "Copier les données de base"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Tout copier"
|
msgstr "Tout copier"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Voir le label"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Acheter sur Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Cette sortie n'est pas vendue sur Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Impossible de vérifier la boutique pour cette sortie."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Impossible d'ouvrir le navigateur"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Nom du titre"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Titre - Album - Artiste"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Cette sortie n'est plus disponible sur Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Ces alternatives pourraient vous intéresser"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Impossible de charger cette sortie."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Recherche d'autres versions…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Cette sortie"
|
||||||
@@ -7628,3 +7628,51 @@ msgstr "基本情報をコピー"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "すべてコピー"
|
msgstr "すべてコピー"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "レーベルを見る"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Qobuzで購入"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "このリリースはQobuzでは販売されていません。"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "このリリースのストア情報を確認できませんでした。"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "ブラウザーを開けませんでした"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "トラック名"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "トラック - アルバム - アーティスト"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "このリリースはQobuzで利用できなくなりました。"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "代わりにこちらはいかがでしょうか"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "このリリースを読み込めませんでした。"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "他のバージョンを探しています…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "このリリース"
|
||||||
@@ -7635,3 +7635,51 @@ msgstr "Basisgegevens kopiëren"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Alles kopiëren"
|
msgstr "Alles kopiëren"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Label bekijken"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Kopen bij Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Deze release wordt niet verkocht bij Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Kon de winkel niet controleren voor deze release."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Kon de browser niet openen"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Tracknaam"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Track - Album - Artiest"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Deze release is niet meer beschikbaar op Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Misschien zijn deze alternatieven interessant"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Kon deze release niet laden."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Andere versies zoeken…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Deze release"
|
||||||
@@ -7645,3 +7645,51 @@ msgstr "Copiar dados básicos"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Copiar tudo"
|
msgstr "Copiar tudo"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Ver editora"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Comprar na Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Este lançamento não está à venda na Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Não foi possível consultar a loja para este lançamento."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Não foi possível abrir o navegador"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Nome da faixa"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Faixa - Álbum - Artista"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Este lançamento já não está disponível na Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Talvez estas alternativas te interessem"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Não foi possível carregar este lançamento."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "A procurar outras versões…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Este lançamento"
|
||||||
@@ -7666,3 +7666,51 @@ msgstr "Копировать основные данные"
|
|||||||
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
#: crates/qbz-qt/qml/shell/TrackInfoBody.qml
|
||||||
msgid "Copy full"
|
msgid "Copy full"
|
||||||
msgstr "Копировать всё"
|
msgstr "Копировать всё"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "View label"
|
||||||
|
msgstr "Открыть лейбл"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Buy on Qobuz"
|
||||||
|
msgstr "Купить на Qobuz"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release isn't sold on Qobuz."
|
||||||
|
msgstr "Этот релиз не продаётся на Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't check the store for this release."
|
||||||
|
msgstr "Не удалось проверить магазин для этого релиза."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Could not open the browser"
|
||||||
|
msgstr "Не удалось открыть браузер"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track name"
|
||||||
|
msgstr "Название трека"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Track - Album - Artist"
|
||||||
|
msgstr "Трек - Альбом - Исполнитель"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release is no longer available on Qobuz."
|
||||||
|
msgstr "Этот релиз больше недоступен на Qobuz."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "You might be interested in these instead"
|
||||||
|
msgstr "Возможно, вас заинтересуют эти альтернативы"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Couldn't load this release."
|
||||||
|
msgstr "Не удалось загрузить этот релиз."
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "Looking for other versions…"
|
||||||
|
msgstr "Ищем другие версии…"
|
||||||
|
|
||||||
|
#: crates/qbz-qt/qml/views/AlbumView.qml
|
||||||
|
msgid "This release"
|
||||||
|
msgstr "Этот релиз"
|
||||||
@@ -518,6 +518,10 @@ pub struct Track {
|
|||||||
/// Favourites-list rows only (unix seconds); see `Album::favorited_at`.
|
/// Favourites-list rows only (unix seconds); see `Album::favorited_at`.
|
||||||
#[serde(default, deserialize_with = "crate::purchase_serde::lenient_option")]
|
#[serde(default, deserialize_with = "crate::purchase_serde::lenient_option")]
|
||||||
pub favorited_at: Option<i64>,
|
pub favorited_at: Option<i64>,
|
||||||
|
/// Store flag from the catalog (`purchasable`): `Some(false)` is the API
|
||||||
|
/// saying this release is not sold on Qobuz; absence is unknown, not no.
|
||||||
|
#[serde(default)]
|
||||||
|
pub purchasable: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
/// Subtitle/edition info from Qobuz (e.g. "Player's Ball Mix",
|
/// Subtitle/edition info from Qobuz (e.g. "Player's Ball Mix",
|
||||||
@@ -730,6 +734,10 @@ pub struct Album {
|
|||||||
/// it (unix seconds); every other album shape leaves it `None`.
|
/// it (unix seconds); every other album shape leaves it `None`.
|
||||||
#[serde(default, deserialize_with = "crate::purchase_serde::lenient_option")]
|
#[serde(default, deserialize_with = "crate::purchase_serde::lenient_option")]
|
||||||
pub favorited_at: Option<i64>,
|
pub favorited_at: Option<i64>,
|
||||||
|
/// Store flag from the catalog (`purchasable`): `Some(false)` is the API
|
||||||
|
/// saying this release is not sold on Qobuz; absence is unknown, not no.
|
||||||
|
#[serde(default)]
|
||||||
|
pub purchasable: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ mod tests {
|
|||||||
fn qobuz_track(id: u64, title: &str, artist: &str) -> Track {
|
fn qobuz_track(id: u64, title: &str, artist: &str) -> Track {
|
||||||
Track {
|
Track {
|
||||||
favorited_at: None,
|
favorited_at: None,
|
||||||
|
purchasable: None,
|
||||||
id,
|
id,
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
version: None,
|
version: None,
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ Rectangle {
|
|||||||
property string title: ""
|
property string title: ""
|
||||||
property string artist: ""
|
property string artist: ""
|
||||||
property string artistId: ""
|
property string artistId: ""
|
||||||
|
/// The release's label id when the host knows it (library feed rows do):
|
||||||
|
/// the "View label" menu entry (2026-09-13). "" = no entry.
|
||||||
|
property string labelId: ""
|
||||||
// Hosts with only a display snapshot (Home's persisted Pinned row) can
|
// Hosts with only a display snapshot (Home's persisted Pinned row) can
|
||||||
// still make the artist line a link and resolve the destination lazily.
|
// still make the artist line a link and resolve the destination lazily.
|
||||||
// Normal catalog cards keep the direct artistId path and pay no lookup.
|
// Normal catalog cards keep the direct artistId path and pay no lookup.
|
||||||
@@ -391,6 +394,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
// Host tail (see `extraMenuEntries`). `concat` so the host's array is
|
// Host tail (see `extraMenuEntries`). `concat` so the host's array is
|
||||||
// never mutated — it is usually a binding's return value.
|
// never mutated — it is usually a binding's return value.
|
||||||
|
if (root.labelId !== "")
|
||||||
|
m.push({ "label": t("View label", r), "icon": "tags", "action": "view-label" })
|
||||||
var extra = root.extraMenuEntries || []
|
var extra = root.extraMenuEntries || []
|
||||||
return extra.length > 0 ? m.concat(extra) : m
|
return extra.length > 0 ? m.concat(extra) : m
|
||||||
}
|
}
|
||||||
@@ -415,6 +420,7 @@ Rectangle {
|
|||||||
// denormalized snapshot and a file:// cache path is dead on any other
|
// denormalized snapshot and a file:// cache path is dead on any other
|
||||||
// machine, the same reason the pin payload uses it.
|
// machine, the same reason the pin payload uses it.
|
||||||
if (a === "favorite") { root.toggleFavorite(); return }
|
if (a === "favorite") { root.toggleFavorite(); return }
|
||||||
|
if (a === "view-label") { QbzHome.openLabel(root.labelId); return }
|
||||||
if (a === "find-release") {
|
if (a === "find-release") {
|
||||||
QbzTrackReplace.openRelease(JSON.stringify({
|
QbzTrackReplace.openRelease(JSON.stringify({
|
||||||
"targetKind": "album",
|
"targetKind": "album",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ QbzContextMenu {
|
|||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
height: parent.height
|
height: parent.height
|
||||||
width: parent.width - 23
|
width: parent.width - 23 - (row.modelData.submenu === true ? 22 : 0)
|
||||||
text: row.modelData.label || ""
|
text: row.modelData.label || ""
|
||||||
color: row.isDanger ? theme.danger
|
color: row.isDanger ? theme.danger
|
||||||
: (row.hot ? theme.textPrimary : theme.textSecondary)
|
: (row.hot ? theme.textPrimary : theme.textSecondary)
|
||||||
@@ -88,6 +88,16 @@ QbzContextMenu {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
// Submenu marker (`submenu: true`): the host answers the pick
|
||||||
|
// by opening a second menu on the same anchor.
|
||||||
|
QbzIcon {
|
||||||
|
visible: row.modelData.submenu === true
|
||||||
|
name: "chevron-right"
|
||||||
|
width: 14
|
||||||
|
height: 14
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
tintName: row.hot ? "textPrimary" : "muted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ Rectangle {
|
|||||||
/// owns — a worse bug than the one this treatment fixes, and it would bite
|
/// owns — a worse bug than the one this treatment fixes, and it would bite
|
||||||
/// hardest in offline mode. It gets `pulledCached` below instead.
|
/// hardest in offline mode. It gets `pulledCached` below instead.
|
||||||
readonly property bool pulledDead: root.pulled && root.cacheStatus !== 3
|
readonly property bool pulledDead: root.pulled && root.cacheStatus !== 3
|
||||||
|
/// The whole release is gone from the catalog (`releaseUnavailable`,
|
||||||
|
/// decided by the row's producer): the album link and "Go to album" go
|
||||||
|
/// dark instead of opening an empty page (2026-09-13).
|
||||||
|
readonly property bool releaseGone: root.item.releaseUnavailable === true
|
||||||
|
|
||||||
/// Pulled from the catalogue, but playable from the downloaded copy. Keeps
|
/// Pulled from the catalogue, but playable from the downloaded copy. Keeps
|
||||||
/// its play cell, its heart and its click; gains only an honest badge,
|
/// its play cell, its heart and its click; gains only an honest badge,
|
||||||
@@ -1058,15 +1062,18 @@ Rectangle {
|
|||||||
width: cols.colAlbum
|
width: cols.colAlbum
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.item.album || ""
|
text: root.item.album || ""
|
||||||
color: albumArea.containsMouse ? theme.accent : theme.textMuted
|
color: (albumArea.containsMouse && albumArea.enabled) ? theme.accent : theme.textMuted
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: albumArea
|
id: albumArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
enabled: !!root.item.albumId && !root.releaseGone
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: root.item.albumId ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: if (root.item.albumId) QbzAlbum.openAlbum(root.item.albumId)
|
// The names ride along so a release the catalog lost can
|
||||||
|
// still be named on its page (album_qt::publish_unavailable).
|
||||||
|
onClicked: QbzAlbum.openAlbumFrom(root.item.albumId, root.item.album || "", root.item.artist || "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Duration.
|
// Duration.
|
||||||
@@ -1268,10 +1275,53 @@ Rectangle {
|
|||||||
/// Build the popup on first use, then open it. Called by `openRowMenu`
|
/// Build the popup on first use, then open it. Called by `openRowMenu`
|
||||||
/// below, which owns the empty-menu guard.
|
/// below, which owns the empty-menu guard.
|
||||||
function openRowMenuLazy(anchor, x, y) {
|
function openRowMenuLazy(anchor, x, y) {
|
||||||
|
root._menuAnchor = anchor
|
||||||
|
root._menuX = x
|
||||||
|
root._menuY = y
|
||||||
rowMenuLoader.active = true
|
rowMenuLoader.active = true
|
||||||
rowMenuLoader.item.openAtCursor(anchor, x, y)
|
rowMenuLoader.item.openAtCursor(anchor, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Copy submenu + clipboard (2026-09-13) --------------------------------
|
||||||
|
// Both lazy, like the row menu: a list row must not carry a Popup tree
|
||||||
|
// or a TextEdit per delegate. Reopened where the row menu was.
|
||||||
|
property Item _menuAnchor: null
|
||||||
|
property real _menuX: 0
|
||||||
|
property real _menuY: 0
|
||||||
|
Loader {
|
||||||
|
id: copyMenuLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: CardMenu {
|
||||||
|
kioskHost: root.kioskHost
|
||||||
|
menuWidth: 224
|
||||||
|
entries: [
|
||||||
|
{ "label": QbzSession.tr("Track name", QbzSession.trRev), "icon": "copy", "action": "copy-title" },
|
||||||
|
{ "label": QbzSession.tr("Track - Album - Artist", QbzSession.trRev), "icon": "clipboard", "action": "copy-full" }
|
||||||
|
]
|
||||||
|
onPicked: function (a) { root.copyRow(a) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loader {
|
||||||
|
id: clipboardLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: QbzClipboard { }
|
||||||
|
}
|
||||||
|
function openCopyMenu() {
|
||||||
|
if (!root._menuAnchor)
|
||||||
|
return
|
||||||
|
copyMenuLoader.active = true
|
||||||
|
copyMenuLoader.item.openAtCursor(root._menuAnchor, root._menuX, root._menuY)
|
||||||
|
}
|
||||||
|
function copyRow(which) {
|
||||||
|
var parts = [root.item.title || ""]
|
||||||
|
if (which === "copy-full") {
|
||||||
|
if ((root.item.album || "") !== "") parts.push(root.item.album)
|
||||||
|
if ((root.item.artist || "") !== "") parts.push(root.item.artist)
|
||||||
|
}
|
||||||
|
clipboardLoader.active = true
|
||||||
|
clipboardLoader.item.copy(parts.join(" - "))
|
||||||
|
}
|
||||||
|
|
||||||
// TrackContextMenu.slint, in its order. Every row here reaches a live
|
// TrackContextMenu.slint, in its order. Every row here reaches a live
|
||||||
// seam; the seamless ones are gated off above.
|
// seam; the seamless ones are gated off above.
|
||||||
/// Open the row menu, unless it would come up EMPTY.
|
/// Open the row menu, unless it would come up EMPTY.
|
||||||
@@ -1362,7 +1412,7 @@ Rectangle {
|
|||||||
m.push({ "label": t("Make available offline", r), "icon": "cloud-download", "action": "cache" })
|
m.push({ "label": t("Make available offline", r), "icon": "cloud-download", "action": "cache" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (root.menuShowGoTo && root.item.albumId)
|
if (root.menuShowGoTo && root.item.albumId && !root.releaseGone)
|
||||||
m.push({ "label": t("Go to album", r), "icon": "disc-3", "action": "go-album" })
|
m.push({ "label": t("Go to album", r), "icon": "disc-3", "action": "go-album" })
|
||||||
if (root.menuShowGoTo && root.item.artistId)
|
if (root.menuShowGoTo && root.item.artistId)
|
||||||
m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
||||||
@@ -1370,6 +1420,13 @@ Rectangle {
|
|||||||
// opens on nothing.
|
// opens on nothing.
|
||||||
if (root.menuShowTrackInfo && !root.pulled)
|
if (root.menuShowTrackInfo && !root.pulled)
|
||||||
m.push({ "label": t("Track info", r), "icon": "info", "action": "track-info" })
|
m.push({ "label": t("Track info", r), "icon": "info", "action": "track-info" })
|
||||||
|
// "Buy on Qobuz" (2026-09-13): the store page; store_qt answers with a
|
||||||
|
// toast when the catalog says the release is not sold.
|
||||||
|
if (root.catalogRow && !root.localSourceRow && !root.pulledDead)
|
||||||
|
m.push({ "label": t("Buy on Qobuz", r), "icon": "shopping-bag", "action": "buy" })
|
||||||
|
// Copy (2026-09-13): a submenu — the track name, or "Track - Album - Artist".
|
||||||
|
if ((root.item.title || "") !== "")
|
||||||
|
m.push({ "label": t("Copy", r), "icon": "copy", "action": "copy", "submenu": true })
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1393,8 +1450,10 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
else if (a === "go-album") {
|
else if (a === "go-album") {
|
||||||
if (root.routeGoToExternally) root.goToRequested("album")
|
if (root.routeGoToExternally) root.goToRequested("album")
|
||||||
else QbzAlbum.openAlbum(root.item.albumId)
|
else QbzAlbum.openAlbumFrom(root.item.albumId, root.item.album || "", root.item.artist || "")
|
||||||
}
|
}
|
||||||
|
else if (a === "buy") QbzAlbum.buyTrack(root.item.id || "")
|
||||||
|
else if (a === "copy") root.openCopyMenu()
|
||||||
else if (a === "favorite") root.toggleFavorite()
|
else if (a === "favorite") root.toggleFavorite()
|
||||||
else if (a === "mixtape") root.mixtapeRequested()
|
else if (a === "mixtape") root.mixtapeRequested()
|
||||||
// Add to playlist. The internal arm is the CATALOG one and nothing
|
// Add to playlist. The internal arm is the CATALOG one and nothing
|
||||||
|
|||||||
@@ -657,7 +657,7 @@ Rectangle {
|
|||||||
favorite: root.npFavorite
|
favorite: root.npFavorite
|
||||||
ephemeral: root.npEphemeral
|
ephemeral: root.npEphemeral
|
||||||
tooltip: root.tooltip
|
tooltip: root.tooltip
|
||||||
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
|
onAddRequested: function (anchorItem) { root.addAnchor = anchorItem; addMenu.openBelowRight(anchorItem) }
|
||||||
onTrackInfoRequested: root.openTrackInfo()
|
onTrackInfoRequested: root.openTrackInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,7 +680,7 @@ Rectangle {
|
|||||||
playCircle: true
|
playCircle: true
|
||||||
favorite: root.npFavorite
|
favorite: root.npFavorite
|
||||||
tooltip: root.tooltip
|
tooltip: root.tooltip
|
||||||
onAddRequested: function (anchorItem) { addMenu.openBelowRight(anchorItem) }
|
onAddRequested: function (anchorItem) { root.addAnchor = anchorItem; addMenu.openBelowRight(anchorItem) }
|
||||||
onTrackInfoRequested: root.openTrackInfo()
|
onTrackInfoRequested: root.openTrackInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -908,6 +908,26 @@ Rectangle {
|
|||||||
// "Add to…" flyout behind the transport "+" (TransportControls.slint's
|
// "Add to…" flyout behind the transport "+" (TransportControls.slint's
|
||||||
// add-menu), on the shared CardMenu surface. Same seven entries, same
|
// add-menu), on the shared CardMenu surface. Same seven entries, same
|
||||||
// order, same icons.
|
// order, same icons.
|
||||||
|
// The "+" button that opened the add-menu: the Copy submenu reopens on it.
|
||||||
|
property Item addAnchor: null
|
||||||
|
QbzClipboard { id: npClipboard }
|
||||||
|
CardMenu {
|
||||||
|
id: npCopyMenu
|
||||||
|
menuWidth: 232
|
||||||
|
entries: [
|
||||||
|
{ "label": QbzSession.tr("Track name", QbzSession.trRev), "icon": "copy", "action": "copy-title" },
|
||||||
|
{ "label": QbzSession.tr("Track - Album - Artist", QbzSession.trRev), "icon": "clipboard", "action": "copy-full" }
|
||||||
|
]
|
||||||
|
onPicked: function (a) {
|
||||||
|
var parts = [QbzPlayer.npTitle || ""]
|
||||||
|
if (a === "copy-full") {
|
||||||
|
if ((QbzPlayer.npAlbum || "") !== "") parts.push(QbzPlayer.npAlbum)
|
||||||
|
if ((QbzPlayer.npArtist || "") !== "") parts.push(QbzPlayer.npArtist)
|
||||||
|
}
|
||||||
|
npClipboard.copy(parts.join(" - "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CardMenu {
|
CardMenu {
|
||||||
id: addMenu
|
id: addMenu
|
||||||
menuWidth: 232
|
menuWidth: 232
|
||||||
@@ -963,6 +983,23 @@ Rectangle {
|
|||||||
"action": "album-favorite"
|
"action": "album-favorite"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// "Buy on Qobuz" (2026-09-13): catalog tracks only; store_qt
|
||||||
|
// resolves whether the release is sold and toasts when not.
|
||||||
|
if (root.npSource === "qobuz") {
|
||||||
|
m.push({
|
||||||
|
"label": QbzSession.tr("Buy on Qobuz", QbzSession.trRev),
|
||||||
|
"icon": "shopping-bag",
|
||||||
|
"action": "buy"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Copy (2026-09-13): a submenu — the track name, or
|
||||||
|
// "Track - Album - Artist".
|
||||||
|
m.push({
|
||||||
|
"label": QbzSession.tr("Copy", QbzSession.trRev),
|
||||||
|
"icon": "copy",
|
||||||
|
"action": "copy",
|
||||||
|
"submenu": true
|
||||||
|
})
|
||||||
return m.concat(root.abEntries())
|
return m.concat(root.abEntries())
|
||||||
}
|
}
|
||||||
onPicked: function (a) {
|
onPicked: function (a) {
|
||||||
@@ -978,6 +1015,10 @@ Rectangle {
|
|||||||
} else if (a === "album-favorite") {
|
} else if (a === "album-favorite") {
|
||||||
if (QbzPlayer.npAlbumId !== "")
|
if (QbzPlayer.npAlbumId !== "")
|
||||||
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
|
QbzLibrary.libraryToggleFavorite("album", QbzPlayer.npAlbumId)
|
||||||
|
} else if (a === "buy") {
|
||||||
|
if (id !== "") QbzAlbum.buyTrack(id)
|
||||||
|
} else if (a === "copy") {
|
||||||
|
if (root.addAnchor) npCopyMenu.openBelowRight(root.addAnchor)
|
||||||
} else if (a === "ab-mark") {
|
} else if (a === "ab-mark") {
|
||||||
QbzPlayer.abMark()
|
QbzPlayer.abMark()
|
||||||
} else if (a === "ab-clear") {
|
} else if (a === "ab-clear") {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ Rectangle {
|
|||||||
|
|
||||||
// AlbumListCols: art 52, quality 150, year 64, overflow 36, gap 12.
|
// AlbumListCols: art 52, quality 150, year 64, overflow 36, gap 12.
|
||||||
readonly property int colArt: 52
|
readonly property int colArt: 52
|
||||||
|
/// The release's label, between the item and its quality (2026-09-13);
|
||||||
|
/// off on the label page itself, where every row would repeat the same.
|
||||||
|
property bool showLabel: true
|
||||||
|
readonly property int colLabel: showLabel ? 150 : 0
|
||||||
readonly property int colQuality: 150
|
readonly property int colQuality: 150
|
||||||
readonly property int colYear: 64
|
readonly property int colYear: 64
|
||||||
readonly property int colOverflow: 36
|
readonly property int colOverflow: 36
|
||||||
@@ -56,10 +60,16 @@ Rectangle {
|
|||||||
ColLabel {
|
ColLabel {
|
||||||
width: parent.width - root.colArt - root.colQuality - root.colYear
|
width: parent.width - root.colArt - root.colQuality - root.colYear
|
||||||
- root.colOverflow - 4 * root.colGap
|
- root.colOverflow - 4 * root.colGap
|
||||||
|
- (root.showLabel ? root.colLabel + root.colGap : 0)
|
||||||
- (root.selectMode ? 18 + root.colGap : 0)
|
- (root.selectMode ? 18 + root.colGap : 0)
|
||||||
text: QbzSession.tr("ITEM", QbzSession.trRev)
|
text: QbzSession.tr("ITEM", QbzSession.trRev)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
ColLabel {
|
||||||
|
visible: root.showLabel
|
||||||
|
width: root.colLabel
|
||||||
|
text: QbzSession.tr("LABEL", QbzSession.trRev)
|
||||||
|
}
|
||||||
ColLabel {
|
ColLabel {
|
||||||
width: root.colQuality
|
width: root.colQuality
|
||||||
text: QbzSession.tr("QUALITY", QbzSession.trRev)
|
text: QbzSession.tr("QUALITY", QbzSession.trRev)
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ Rectangle {
|
|||||||
|
|
||||||
// Same columns (and widths) as AlbumListHeader.qml.
|
// Same columns (and widths) as AlbumListHeader.qml.
|
||||||
readonly property int colArt: 52
|
readonly property int colArt: 52
|
||||||
|
/// See AlbumListHeader.showLabel.
|
||||||
|
property bool showLabel: true
|
||||||
|
readonly property int colLabel: (!root.showLabel || (root.kioskHost && width < 500)) ? 0 : 150
|
||||||
readonly property int colQuality: root.kioskHost && width < 500 ? 0 : 150
|
readonly property int colQuality: root.kioskHost && width < 500 ? 0 : 150
|
||||||
readonly property int colYear: root.kioskHost && width < 500 ? 0 : 64
|
readonly property int colYear: root.kioskHost && width < 500 ? 0 : 64
|
||||||
readonly property int colOverflow: root.kioskHost ? 44 : 36
|
readonly property int colOverflow: root.kioskHost ? 44 : 36
|
||||||
@@ -107,7 +110,7 @@ Rectangle {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (root.selectMode) root.toggleSelect(mouse.modifiers)
|
if (root.selectMode) root.toggleSelect(mouse.modifiers)
|
||||||
else if (!root.pulledDead) QbzAlbum.openAlbum(root.item.id || "")
|
else if (!root.pulledDead) QbzAlbum.openAlbumFrom(root.item.id || "", root.item.title || "", root.item.artist || "")
|
||||||
}
|
}
|
||||||
// Touch has no right button. Desktop-inert: `pressAndHold` never
|
// Touch has no right button. Desktop-inert: `pressAndHold` never
|
||||||
// fires for a mouse press that is released normally, and the guard
|
// fires for a mouse press that is released normally, and the guard
|
||||||
@@ -160,13 +163,17 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
if (!root.pulledDead && root.catalogRow)
|
if (!root.pulledDead && root.catalogRow)
|
||||||
m.push({ "label": t("Block this album", r), "icon": "blind-eye", "action": "block" })
|
m.push({ "label": t("Block this album", r), "icon": "blind-eye", "action": "block" })
|
||||||
|
// The label page outlives a pulled release, so this one stays.
|
||||||
|
if ((root.item.labelId || "") !== "")
|
||||||
|
m.push({ "label": t("View label", r), "icon": "tags", "action": "view-label" })
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
function menuAction(a) {
|
function menuAction(a) {
|
||||||
var id = root.item.id || ""
|
var id = root.item.id || ""
|
||||||
if (id === "") return
|
if (id === "") return
|
||||||
|
if (a === "view-label") { QbzHome.openLabel(root.item.labelId || ""); return }
|
||||||
if (root.pulledDead) return
|
if (root.pulledDead) return
|
||||||
if (a === "open") QbzAlbum.openAlbum(id)
|
if (a === "open") QbzAlbum.openAlbumFrom(id, root.item.title || "", root.item.artist || "")
|
||||||
else if (a === "play") QbzPlayer.playAlbum(id)
|
else if (a === "play") QbzPlayer.playAlbum(id)
|
||||||
// `artUrl`, never `artPath`: the store keeps a denormalized cover url
|
// `artUrl`, never `artPath`: the store keeps a denormalized cover url
|
||||||
// and a file:// cache path is dead on any other machine.
|
// and a file:// cache path is dead on any other machine.
|
||||||
@@ -274,6 +281,7 @@ Rectangle {
|
|||||||
// multi-select is switched on.
|
// multi-select is switched on.
|
||||||
width: parent.width - root.colArt - root.colQuality - root.colYear
|
width: parent.width - root.colArt - root.colQuality - root.colYear
|
||||||
- root.colOverflow - 4 * root.colGap
|
- root.colOverflow - 4 * root.colGap
|
||||||
|
- (root.colLabel > 0 ? root.colLabel + root.colGap : 0)
|
||||||
- (root.selectMode ? 18 + root.colGap : 0)
|
- (root.selectMode ? 18 + root.colGap : 0)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 2
|
spacing: 2
|
||||||
@@ -305,6 +313,32 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LABEL — the release's label, a link to its page when the feed
|
||||||
|
// carries the id (2026-09-13). Zero-width (and skipped by the Row)
|
||||||
|
// where the column is off.
|
||||||
|
Item {
|
||||||
|
visible: root.colLabel > 0
|
||||||
|
width: root.colLabel
|
||||||
|
height: parent.height
|
||||||
|
Text {
|
||||||
|
id: labelText
|
||||||
|
width: parent.width
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.item.label || ""
|
||||||
|
color: labelArea.containsMouse && labelArea.enabled ? theme.accent : theme.textMuted
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
MouseArea {
|
||||||
|
id: labelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: (root.item.labelId || "") !== ""
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: QbzHome.openLabel(root.item.labelId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// QUALITY — the SAME badge the album page shows (tier label over the
|
// QUALITY — the SAME badge the album page shows (tier label over the
|
||||||
// exact bit-depth / sample-rate line).
|
// exact bit-depth / sample-rate line).
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property var albumHeader: album.header || ({})
|
readonly property var albumHeader: album.header || ({})
|
||||||
|
/// The "cannot show this release" arm of the document (album_qt::publish_unavailable).
|
||||||
|
readonly property var unavailable: album.unavailable || null
|
||||||
readonly property var tracks: album.tracks || []
|
readonly property var tracks: album.tracks || []
|
||||||
readonly property var purchase: album.purchase || ({})
|
readonly property var purchase: album.purchase || ({})
|
||||||
readonly property var localPurchaseVariants: purchase.localVariants || []
|
readonly property var localPurchaseVariants: purchase.localVariants || []
|
||||||
@@ -889,12 +891,79 @@ Rectangle {
|
|||||||
// NavButtons is a 0px placeholder in the Slint source.
|
// NavButtons is a 0px placeholder in the Slint source.
|
||||||
Item { width: 1; height: 22 }
|
Item { width: 1; height: 22 }
|
||||||
|
|
||||||
|
// The release cannot be shown (album_qt::publish_unavailable,
|
||||||
|
// 2026-09-13): gone from the catalog, or a load failure. The
|
||||||
|
// heading comes from the row that opened it, then the reason, and
|
||||||
|
// for a gone release the best-ranked alternatives Qobuz still
|
||||||
|
// sells — the same ranking the playlist replacement flow uses.
|
||||||
|
Column {
|
||||||
|
visible: root.unavailable !== null
|
||||||
|
width: parent.width - 64
|
||||||
|
spacing: 12
|
||||||
|
Item { width: 1; height: 8 }
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.unavailable && (root.unavailable.title || "") !== ""
|
||||||
|
? root.unavailable.title
|
||||||
|
: QbzSession.tr("This release", QbzSession.trRev)
|
||||||
|
color: theme.textPrimary
|
||||||
|
font.pixelSize: theme.fontTitle
|
||||||
|
font.weight: theme.weightBold
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
visible: root.unavailable && (root.unavailable.artist || "") !== ""
|
||||||
|
width: parent.width
|
||||||
|
text: root.unavailable ? (root.unavailable.artist || "") : ""
|
||||||
|
color: theme.textSecondary
|
||||||
|
font.pixelSize: theme.fontSection
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
QbzIcon {
|
||||||
|
name: "circle-alert"
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
tintName: "muted"
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
width: parent.width - 26
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.unavailable && root.unavailable.gone === true
|
||||||
|
? QbzSession.tr("This release is no longer available on Qobuz.", QbzSession.trRev)
|
||||||
|
: QbzSession.tr("Couldn't load this release.", QbzSession.trRev)
|
||||||
|
+ ((root.unavailable && (root.unavailable.error || "") !== "")
|
||||||
|
? " " + root.unavailable.error : "")
|
||||||
|
color: theme.textMuted
|
||||||
|
font.pixelSize: theme.fontBody
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item { width: 1; height: 8 }
|
||||||
|
Text {
|
||||||
|
visible: root.unavailable && root.unavailable.loading === true
|
||||||
|
text: QbzSession.tr("Looking for other versions…", QbzSession.trRev)
|
||||||
|
color: theme.textMuted
|
||||||
|
font.pixelSize: theme.fontBody
|
||||||
|
}
|
||||||
|
SectionRail {
|
||||||
|
visible: root.unavailable && (root.unavailable.alternatives || []).length > 0
|
||||||
|
width: parent.width
|
||||||
|
title: QbzSession.tr("You might be interested in these instead", QbzSession.trRev)
|
||||||
|
items: root.unavailable ? (root.unavailable.alternatives || []) : []
|
||||||
|
coverMap: root.coverMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Album header skeleton ----------------------------------
|
// --- Album header skeleton ----------------------------------
|
||||||
// Mounted on the primary flag, and the real header is hidden by
|
// Mounted on the primary flag, and the real header is hidden by
|
||||||
// the same flag: opening album B never renders a half-empty
|
// the same flag: opening album B never renders a half-empty
|
||||||
// header frame while B's document is in flight.
|
// header frame while B's document is in flight.
|
||||||
Row {
|
Row {
|
||||||
visible: root.primaryLoading
|
visible: root.primaryLoading && root.unavailable === null
|
||||||
width: parent.width - 64
|
width: parent.width - 64
|
||||||
spacing: root.headerGapPx
|
spacing: root.headerGapPx
|
||||||
|
|
||||||
@@ -932,7 +1001,7 @@ Rectangle {
|
|||||||
|
|
||||||
// --- Album header -------------------------------------------
|
// --- Album header -------------------------------------------
|
||||||
Row {
|
Row {
|
||||||
visible: !root.primaryLoading
|
visible: !root.primaryLoading && root.unavailable === null
|
||||||
width: parent.width - 64
|
width: parent.width - 64
|
||||||
spacing: root.headerGapPx
|
spacing: root.headerGapPx
|
||||||
|
|
||||||
@@ -1322,6 +1391,23 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: albumInfo.openFor(albumHeader.id)
|
onClicked: albumInfo.openFor(albumHeader.id)
|
||||||
}
|
}
|
||||||
|
// "Buy on Qobuz" (2026-09-13): only when the catalog
|
||||||
|
// says the release is sold and it is not already
|
||||||
|
// owned; a link to the store page, not a purchase here.
|
||||||
|
QbzCircleAction {
|
||||||
|
id: buyButton
|
||||||
|
visible: albumHeader.purchasable === true
|
||||||
|
&& (root.purchase.entitlementState || "") !== "purchased"
|
||||||
|
name: "shopping-bag"
|
||||||
|
diameterOverride: root.compactHeaderPref ? 28 : 0
|
||||||
|
overlay: root.hdrOverlay
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: QbzAlbum.buyAlbum(albumHeader.id)
|
||||||
|
HoverHandler { id: buyHover }
|
||||||
|
ToolTip.visible: buyHover.hovered
|
||||||
|
ToolTip.text: QbzSession.tr("Buy on Qobuz", QbzSession.trRev)
|
||||||
|
ToolTip.delay: 350
|
||||||
|
}
|
||||||
QbzCircleAction {
|
QbzCircleAction {
|
||||||
id: albumMenuBtn
|
id: albumMenuBtn
|
||||||
name: "ellipsis"
|
name: "ellipsis"
|
||||||
@@ -1429,7 +1515,7 @@ Rectangle {
|
|||||||
// instead of a floating overlay.
|
// instead of a floating overlay.
|
||||||
QbzMultiSelectBar {
|
QbzMultiSelectBar {
|
||||||
id: bulkBar
|
id: bulkBar
|
||||||
visible: root.multiSelect && !root.primaryLoading
|
visible: root.multiSelect && !root.primaryLoading && root.unavailable === null
|
||||||
width: parent.width
|
width: parent.width
|
||||||
selectedCount: root.selectedCount
|
selectedCount: root.selectedCount
|
||||||
// The reference's full AlbumView inventory
|
// The reference's full AlbumView inventory
|
||||||
@@ -1449,7 +1535,7 @@ Rectangle {
|
|||||||
|
|
||||||
// Toolbar — quality badge + track search (+ inert select).
|
// Toolbar — quality badge + track search (+ inert select).
|
||||||
Row {
|
Row {
|
||||||
visible: !QbzAlbum.albumLoading
|
visible: !QbzAlbum.albumLoading && root.unavailable === null
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 52
|
height: 52
|
||||||
spacing: 16
|
spacing: 16
|
||||||
@@ -1560,7 +1646,7 @@ Rectangle {
|
|||||||
// the band so `centerIn` centres them, which is the fix
|
// the band so `centerIn` centres them, which is the fix
|
||||||
// this block used to document at length.
|
// this block used to document at length.
|
||||||
TrackListHeader {
|
TrackListHeader {
|
||||||
visible: !QbzAlbum.albumLoading
|
visible: !QbzAlbum.albumLoading && root.unavailable === null
|
||||||
width: parent.width
|
width: parent.width
|
||||||
bandHeight: 40
|
bandHeight: 40
|
||||||
labelSpacing: 0.5
|
labelSpacing: 0.5
|
||||||
|
|||||||
@@ -871,6 +871,9 @@ Rectangle {
|
|||||||
// row). Activated on demand, torn down on close (Qt.callLater so the
|
// row). Activated on demand, torn down on close (Qt.callLater so the
|
||||||
// Popup is not destroyed mid-signal) — the same shape rows/TrackRow.qml
|
// Popup is not destroyed mid-signal) — the same shape rows/TrackRow.qml
|
||||||
// carries per row.
|
// carries per row.
|
||||||
|
// One clipboard carrier for every Popular Tracks row's Copy submenu.
|
||||||
|
QbzClipboard { id: artistClipboard }
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: artistTrackInfo
|
id: artistTrackInfo
|
||||||
active: false
|
active: false
|
||||||
@@ -1692,7 +1695,7 @@ Rectangle {
|
|||||||
enabled: !popRow.selectMode
|
enabled: !popRow.selectMode
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: function (mouse) { popMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
onClicked: function (mouse) { popRow._menuAnchor = moreArea; popRow._menuX = mouse.x; popRow._menuY = mouse.y; popMenu.openAtCursor(moreArea, mouse.x, mouse.y) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1774,6 +1777,9 @@ Rectangle {
|
|||||||
m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
m.push({ "label": t("Go to artist", r), "icon": "user", "action": "go-artist" })
|
||||||
if (!popRow.pulled)
|
if (!popRow.pulled)
|
||||||
m.push({ "label": t("Track info", r), "icon": "info", "action": "track-info" })
|
m.push({ "label": t("Track info", r), "icon": "info", "action": "track-info" })
|
||||||
|
if (!popRow.pulledDead)
|
||||||
|
m.push({ "label": t("Buy on Qobuz", r), "icon": "shopping-bag", "action": "buy" })
|
||||||
|
m.push({ "label": t("Copy", r), "icon": "copy", "action": "copy", "submenu": true })
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
onPicked: function (a) {
|
onPicked: function (a) {
|
||||||
@@ -1804,9 +1810,31 @@ Rectangle {
|
|||||||
else if (a === "cache") QbzPlayer.cacheTrack(id)
|
else if (a === "cache") QbzPlayer.cacheTrack(id)
|
||||||
else if (a === "uncache") QbzPlayer.uncacheTrack(id)
|
else if (a === "uncache") QbzPlayer.uncacheTrack(id)
|
||||||
else if (a === "recache") QbzPlayer.recacheTrack(id)
|
else if (a === "recache") QbzPlayer.recacheTrack(id)
|
||||||
else if (a === "go-album") QbzAlbum.openAlbum(popRow.row.albumId)
|
else if (a === "go-album") QbzAlbum.openAlbumFrom(popRow.row.albumId, popRow.row.album || "", popRow.row.artist || "")
|
||||||
else if (a === "go-artist") QbzArtist.openArtist(popRow.row.artistId)
|
else if (a === "go-artist") QbzArtist.openArtist(popRow.row.artistId)
|
||||||
else if (a === "track-info") root.openTrackInfo(id)
|
else if (a === "track-info") root.openTrackInfo(id)
|
||||||
|
else if (a === "buy") QbzAlbum.buyTrack(id)
|
||||||
|
else if (a === "copy") popCopyMenu.openAtCursor(popRow._menuAnchor, popRow._menuX, popRow._menuY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Copy submenu (2026-09-13), reopened where the row menu was.
|
||||||
|
property Item _menuAnchor: null
|
||||||
|
property real _menuX: 0
|
||||||
|
property real _menuY: 0
|
||||||
|
CardMenu {
|
||||||
|
id: popCopyMenu
|
||||||
|
menuWidth: 224
|
||||||
|
entries: [
|
||||||
|
{ "label": QbzSession.tr("Track name", QbzSession.trRev), "icon": "copy", "action": "copy-title" },
|
||||||
|
{ "label": QbzSession.tr("Track - Album - Artist", QbzSession.trRev), "icon": "clipboard", "action": "copy-full" }
|
||||||
|
]
|
||||||
|
onPicked: function (a) {
|
||||||
|
var parts = [popRow.row.title || ""]
|
||||||
|
if (a === "copy-full") {
|
||||||
|
if ((popRow.row.album || "") !== "") parts.push(popRow.row.album)
|
||||||
|
if ((popRow.row.artist || "") !== "") parts.push(popRow.row.artist)
|
||||||
|
}
|
||||||
|
artistClipboard.copy(parts.join(" - "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1832,7 +1860,7 @@ Rectangle {
|
|||||||
id: popRcArea
|
id: popRcArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: function (mouse) { popMenu.openAtCursor(popRcArea, mouse.x, mouse.y) }
|
onClicked: function (mouse) { popRow._menuAnchor = popRcArea; popRow._menuX = mouse.x; popRow._menuY = mouse.y; popMenu.openAtCursor(popRcArea, mouse.x, mouse.y) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ Rectangle {
|
|||||||
height: root.kioskHost && (cr.kind === "album" || cr.kind === "playlist") ? 64 : 246
|
height: root.kioskHost && (cr.kind === "album" || cr.kind === "playlist") ? 64 : 246
|
||||||
|
|
||||||
KioskCoverSource { id: cellArt; remote: root.kioskHost ? (crCell.modelData.artUrl || "") : ""; local: crCell.modelData.artPath || ""; edge: 44 }
|
KioskCoverSource { id: cellArt; remote: root.kioskHost ? (crCell.modelData.artUrl || "") : ""; local: crCell.modelData.artPath || ""; edge: 44 }
|
||||||
Component { id: crKioskAlbum; AlbumListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
Component { id: crKioskAlbum; AlbumListRow { kioskHost: true; showLabel: false; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
||||||
Component { id: crKioskPlaylist; PlaylistListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
Component { id: crKioskPlaylist; PlaylistListRow { kioskHost: true; item: crCell.modelData; artSource: root.kioskHost ? cellArt.source : (crCell.modelData.artPath || "") } }
|
||||||
Component {
|
Component {
|
||||||
id: crAlbum
|
id: crAlbum
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Item {
|
|||||||
title: cell.item.title
|
title: cell.item.title
|
||||||
artist: cell.item.artist
|
artist: cell.item.artist
|
||||||
artistId: cell.item.artistId
|
artistId: cell.item.artistId
|
||||||
|
labelId: cell.item.labelId || ""
|
||||||
genre: cell.item.genre
|
genre: cell.item.genre
|
||||||
year: cell.item.year
|
year: cell.item.year
|
||||||
qualityTier: cell.item.qualityTier
|
qualityTier: cell.item.qualityTier
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ pub mod qbz_album_bridge {
|
|||||||
/// Open the album detail view (pushes "album" on the nav stack).
|
/// Open the album detail view (pushes "album" on the nav stack).
|
||||||
#[qinvokable]
|
#[qinvokable]
|
||||||
fn open_album(self: Pin<&mut QbzAlbum>, album_id: QString);
|
fn open_album(self: Pin<&mut QbzAlbum>, album_id: QString);
|
||||||
|
/// `open_album` with the title/artist the caller knows, so a release
|
||||||
|
/// the catalog no longer has can still be named on its page.
|
||||||
|
#[qinvokable]
|
||||||
|
fn open_album_from(self: Pin<&mut QbzAlbum>, album_id: QString, title: QString, artist: QString);
|
||||||
|
/// "Buy on Qobuz": the store page in the browser when the release is
|
||||||
|
/// sold, a toast otherwise (store_qt).
|
||||||
|
#[qinvokable]
|
||||||
|
fn buy_album(self: Pin<&mut QbzAlbum>, album_id: QString);
|
||||||
|
#[qinvokable]
|
||||||
|
fn buy_track(self: Pin<&mut QbzAlbum>, track_id: QString);
|
||||||
/// AlbumCard's picture-in-picture action. Fetches the compact album
|
/// AlbumCard's picture-in-picture action. Fetches the compact album
|
||||||
/// document without navigating or touching `album_json`.
|
/// document without navigating or touching `album_json`.
|
||||||
#[qinvokable]
|
#[qinvokable]
|
||||||
@@ -189,6 +199,20 @@ impl qbz_album_bridge::QbzAlbum {
|
|||||||
pub fn open_album(self: Pin<&mut Self>, album_id: QString) {
|
pub fn open_album(self: Pin<&mut Self>, album_id: QString) {
|
||||||
crate::open_album(album_id.to_string());
|
crate::open_album(album_id.to_string());
|
||||||
}
|
}
|
||||||
|
pub fn open_album_from(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
album_id: QString,
|
||||||
|
title: QString,
|
||||||
|
artist: QString,
|
||||||
|
) {
|
||||||
|
crate::open_album_from(album_id.to_string(), title.to_string(), artist.to_string());
|
||||||
|
}
|
||||||
|
pub fn buy_album(self: Pin<&mut Self>, album_id: QString) {
|
||||||
|
crate::store_qt::buy_album(album_id.to_string());
|
||||||
|
}
|
||||||
|
pub fn buy_track(self: Pin<&mut Self>, track_id: QString) {
|
||||||
|
crate::store_qt::buy_track(track_id.to_string());
|
||||||
|
}
|
||||||
pub fn open_quick_view(self: Pin<&mut Self>, album_id: QString) {
|
pub fn open_quick_view(self: Pin<&mut Self>, album_id: QString) {
|
||||||
crate::album_quick_view_qt::open(album_id.to_string());
|
crate::album_quick_view_qt::open(album_id.to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ pub struct AlbumHeader {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
#[serde(rename = "labelId")]
|
#[serde(rename = "labelId")]
|
||||||
pub label_id: String,
|
pub label_id: String,
|
||||||
|
/// The catalog's `purchasable` for the release: the header's "Buy on
|
||||||
|
/// Qobuz" action shows only on `true` (2026-09-13).
|
||||||
|
pub purchasable: bool,
|
||||||
/// (id, name) award pairs for the sidebar.
|
/// (id, name) award pairs for the sidebar.
|
||||||
pub awards: Vec<(String, String)>,
|
pub awards: Vec<(String, String)>,
|
||||||
#[serde(rename = "isFavorite")]
|
#[serde(rename = "isFavorite")]
|
||||||
@@ -619,11 +622,19 @@ pub async fn load_album(
|
|||||||
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
||||||
album_id: &str,
|
album_id: &str,
|
||||||
) -> Result<AlbumViewData, String> {
|
) -> Result<AlbumViewData, String> {
|
||||||
|
// A release the catalog no longer has is the one failure the page can
|
||||||
|
// explain (and offer alternatives for): it travels as `ALBUM_GONE` so
|
||||||
|
// `open_album` can tell it from a network error.
|
||||||
let album = runtime
|
let album = runtime
|
||||||
.core()
|
.core()
|
||||||
.get_album(album_id)
|
.get_album(album_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| match e {
|
||||||
|
qbz_core::CoreError::Api(ref error) if error.is_album_unavailable(album_id) => {
|
||||||
|
ALBUM_GONE.to_string()
|
||||||
|
}
|
||||||
|
other => other.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
let artist = album.artist.name.clone();
|
let artist = album.artist.name.clone();
|
||||||
let artist_id = album.artist.id.to_string();
|
let artist_id = album.artist.id.to_string();
|
||||||
@@ -770,6 +781,7 @@ pub async fn load_album(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|l| l.id.to_string())
|
.map(|l| l.id.to_string())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
purchasable: album.purchasable == Some(true),
|
||||||
awards,
|
awards,
|
||||||
has_booklet: !booklet_url.is_empty(),
|
has_booklet: !booklet_url.is_empty(),
|
||||||
// album.rs:677-679 — the whole album is cached when every track is.
|
// album.rs:677-679 — the whole album is cached when every track is.
|
||||||
@@ -1077,6 +1089,84 @@ pub(crate) fn format_album_title(title: &str, version: Option<&str>) -> String {
|
|||||||
/// `main-artist` role, else the first entry). `/album/suggest` returns items
|
/// `main-artist` role, else the first entry). `/album/suggest` returns items
|
||||||
/// whose flat `artist` is empty but whose `artists[]` is populated — without
|
/// whose flat `artist` is empty but whose `artists[]` is populated — without
|
||||||
/// the fallback the "Listening suggestions" cards render no artist line.
|
/// the fallback the "Listening suggestions" cards render no artist line.
|
||||||
|
/// `load_album`'s error for a release the catalog no longer has (see there).
|
||||||
|
pub(crate) const ALBUM_GONE: &str = "album-gone";
|
||||||
|
|
||||||
|
/// The album page when the release cannot be shown (2026-09-13). `gone` is
|
||||||
|
/// the catalog saying the release no longer exists — a pulled album that
|
||||||
|
/// still answers keeps its normal page, rows marked — and anything else is a
|
||||||
|
/// load failure. Published as the `unavailable` arm of the album document,
|
||||||
|
/// which AlbumView renders instead of a header; for a gone release whose
|
||||||
|
/// title is known (the row that opened it passes one, a 404 carries none),
|
||||||
|
/// the best-ranked alternatives Qobuz still sells follow, through the same
|
||||||
|
/// ranking the playlist replacement flow uses.
|
||||||
|
pub(crate) fn publish_unavailable(album_id: String, gone: bool, error: String) {
|
||||||
|
let (title, artist) = crate::album_hint(&album_id).unwrap_or_default();
|
||||||
|
let generation = ALBUM_GEN.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
let searching = gone && !title.trim().is_empty();
|
||||||
|
let doc = json!({
|
||||||
|
"unavailable": {
|
||||||
|
"id": album_id,
|
||||||
|
"title": title,
|
||||||
|
"artist": artist,
|
||||||
|
"gone": gone,
|
||||||
|
"error": if gone { String::new() } else { error },
|
||||||
|
"loading": searching,
|
||||||
|
"alternatives": [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Ok(mut guard) = ALBUM_DOC.lock() {
|
||||||
|
*guard = Some((generation, doc.clone()));
|
||||||
|
}
|
||||||
|
let json = doc.to_string();
|
||||||
|
crate::album_bridge::ui(move |mut b| {
|
||||||
|
b.as_mut().set_album_json(QString::from(json.as_str()));
|
||||||
|
b.as_mut().set_album_loading(false);
|
||||||
|
});
|
||||||
|
if !searching {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crate::spawn(async move {
|
||||||
|
let found =
|
||||||
|
crate::track_replace_qt::ranked_alternatives(&album_id, &title, &artist).await;
|
||||||
|
let cards: Vec<AlbumCardData> = found.iter().take(12).map(album_to_card).collect();
|
||||||
|
let value = serde_json::to_value(&cards).unwrap_or_else(|_| json!([]));
|
||||||
|
publish_patch(generation, move |doc| {
|
||||||
|
if let Some(arm) = doc.get_mut("unavailable") {
|
||||||
|
arm["alternatives"] = value;
|
||||||
|
arm["loading"] = json!(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A catalog album as one card of a rail (the alternatives rail above).
|
||||||
|
fn album_to_card(album: &Album) -> AlbumCardData {
|
||||||
|
let (artist, artist_id) = card_artist(album);
|
||||||
|
let date = album
|
||||||
|
.release_date_original
|
||||||
|
.as_deref()
|
||||||
|
.or_else(|| album.dates.as_ref().and_then(|d| d.original.as_deref()));
|
||||||
|
AlbumCardData {
|
||||||
|
id: album.id.clone(),
|
||||||
|
title: format_album_title(&album.title, album.version.as_deref()),
|
||||||
|
artist,
|
||||||
|
artist_id,
|
||||||
|
genre: album.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
|
||||||
|
year: qbz_text_utils::dates::release_label(date),
|
||||||
|
release_sort_key: qbz_text_utils::dates::release_sort_key(date),
|
||||||
|
quality_tier: home_qt::quality_tier_from_depth(album.maximum_bit_depth).to_string(),
|
||||||
|
quality_detail: home_qt::quality_detail_from_parts(
|
||||||
|
album.maximum_bit_depth,
|
||||||
|
album.maximum_sampling_rate,
|
||||||
|
),
|
||||||
|
art_url: album.image.best().cloned().unwrap_or_default(),
|
||||||
|
is_pinned: crate::sidebar_qt::is_pinned("album", &album.id),
|
||||||
|
is_favorite: crate::library_qt::is_favorite("album", &album.id),
|
||||||
|
default_order: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn card_artist(album: &Album) -> (String, String) {
|
fn card_artist(album: &Album) -> (String, String) {
|
||||||
if !album.artist.name.is_empty() {
|
if !album.artist.name.is_empty() {
|
||||||
return (album.artist.name.clone(), album.artist.id.to_string());
|
return (album.artist.name.clone(), album.artist.id.to_string());
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ pub struct FeedItem {
|
|||||||
pub genre: String,
|
pub genre: String,
|
||||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
/// Catalog label id behind `label` ("" when the release carries none) —
|
||||||
|
/// the Releases list column and the "View label" menu entry open it.
|
||||||
|
pub label_id: String,
|
||||||
#[serde(default, rename = "updatedAt", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "updatedAt", skip_serializing_if = "Option::is_none")]
|
||||||
pub updated_at: Option<i64>,
|
pub updated_at: Option<i64>,
|
||||||
#[serde(default, rename = "trackCount", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "trackCount", skip_serializing_if = "Option::is_none")]
|
||||||
@@ -722,6 +725,11 @@ fn map_album(album: Album, ready_offline_tracks: usize) -> FeedItem {
|
|||||||
subtitle: artist.clone(),
|
subtitle: artist.clone(),
|
||||||
artist,
|
artist,
|
||||||
artist_id: album.artist.id.to_string(),
|
artist_id: album.artist.id.to_string(),
|
||||||
|
label_id: album
|
||||||
|
.label
|
||||||
|
.as_ref()
|
||||||
|
.map(|label| label.id.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
label: album.label.map(|label| label.name).unwrap_or_default(),
|
label: album.label.map(|label| label.name).unwrap_or_default(),
|
||||||
duration_secs: album.duration.unwrap_or(0),
|
duration_secs: album.duration.unwrap_or(0),
|
||||||
track_count: Some(track_count as u32),
|
track_count: Some(track_count as u32),
|
||||||
@@ -1009,7 +1017,7 @@ async fn probe_release_availability(
|
|||||||
/// Resolve release-wide availability only for favourite tracks already known
|
/// Resolve release-wide availability only for favourite tracks already known
|
||||||
/// to be unavailable. Explicit embedded answers cost no request; unknown album
|
/// to be unavailable. Explicit embedded answers cost no request; unknown album
|
||||||
/// ids are de-duplicated and probed with a bounded fan-out.
|
/// ids are de-duplicated and probed with a bounded fan-out.
|
||||||
async fn unavailable_release_ids(
|
pub(crate) async fn unavailable_release_ids(
|
||||||
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
runtime: &Arc<AppRuntime<LoggingAdapter>>,
|
||||||
tracks: &[Track],
|
tracks: &[Track],
|
||||||
) -> HashSet<String> {
|
) -> HashSet<String> {
|
||||||
@@ -1662,6 +1670,7 @@ fn map_purchased_album(a: qbz_models::PurchaseAlbum) -> FeedItem {
|
|||||||
),
|
),
|
||||||
added_at: a.purchased_at.unwrap_or(0),
|
added_at: a.purchased_at.unwrap_or(0),
|
||||||
label: a.label.as_ref().map(|label| label.name.clone()).unwrap_or_default(),
|
label: a.label.as_ref().map(|label| label.name.clone()).unwrap_or_default(),
|
||||||
|
label_id: a.label.as_ref().map(|label| label.id.to_string()).unwrap_or_default(),
|
||||||
duration_secs: a.duration.unwrap_or(0),
|
duration_secs: a.duration.unwrap_or(0),
|
||||||
track_count: a.tracks_count,
|
track_count: a.tracks_count,
|
||||||
genre: a.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
|
genre: a.genre.as_ref().map(|g| g.name.clone()).unwrap_or_default(),
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ mod rip_qt;
|
|||||||
mod rip_wizard_qt;
|
mod rip_wizard_qt;
|
||||||
mod sacd_qt;
|
mod sacd_qt;
|
||||||
mod share_qt;
|
mod share_qt;
|
||||||
|
mod store_qt;
|
||||||
mod tag_editor_bridge;
|
mod tag_editor_bridge;
|
||||||
mod tag_editor_qt;
|
mod tag_editor_qt;
|
||||||
// Offline cache (downloads tier): state activation on login + the action
|
// Offline cache (downloads tier): state activation on login + the action
|
||||||
@@ -1448,7 +1449,8 @@ pub(crate) fn open_album(album_id: String) {
|
|||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("[qbz-qt] album view load failed: {e}");
|
log::warn!("[qbz-qt] album view load failed: {e}");
|
||||||
album_bridge::ui(move |mut b| b.as_mut().set_album_loading(false));
|
let gone = e == album_qt::ALBUM_GONE;
|
||||||
|
album_qt::publish_unavailable(album_id, gone, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2754,6 +2756,30 @@ pub(crate) fn navigate_to_tab(view: &str, tab: &str) {
|
|||||||
/// language switch so its Rust-built section headers re-translate.
|
/// language switch so its Rust-built section headers re-translate.
|
||||||
static LAST_DETAIL: Mutex<(String, String)> = Mutex::new((String::new(), String::new()));
|
static LAST_DETAIL: Mutex<(String, String)> = Mutex::new((String::new(), String::new()));
|
||||||
|
|
||||||
|
/// The names the row that opened an album knew (id, title, artist): a 404
|
||||||
|
/// carries none, and the "no longer available" page needs them for its
|
||||||
|
/// heading and its alternatives search (album_qt::publish_unavailable).
|
||||||
|
static ALBUM_HINT: Mutex<Option<(String, String, String)>> = Mutex::new(None);
|
||||||
|
|
||||||
|
pub(crate) fn album_hint(album_id: &str) -> Option<(String, String)> {
|
||||||
|
ALBUM_HINT
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.clone())
|
||||||
|
.filter(|(id, _, _)| id == album_id)
|
||||||
|
.map(|(_, title, artist)| (title, artist))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `open_album` with the title/artist the caller knows — every row that
|
||||||
|
/// opens an album passes them, so the page can name a release the catalog
|
||||||
|
/// no longer has.
|
||||||
|
pub(crate) fn open_album_from(album_id: String, title: String, artist: String) {
|
||||||
|
if let Ok(mut guard) = ALBUM_HINT.lock() {
|
||||||
|
*guard = Some((album_id.clone(), title, artist));
|
||||||
|
}
|
||||||
|
open_album(album_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Settings > Appearance > Language: the pref is already persisted by the
|
/// Settings > Appearance > Language: the pref is already persisted by the
|
||||||
/// settings arm; this applies it LIVE — "auto" resolves POSIX env — then:
|
/// settings arm; this applies it LIVE — "auto" resolves POSIX env — then:
|
||||||
/// 1. bumps trRev, so EVERY `QbzBridge.tr(msgid, QbzBridge.trRev)` binding
|
/// 1. bumps trRev, so EVERY `QbzBridge.tr(msgid, QbzBridge.trRev)` binding
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ pub struct PlaylistTrackRow {
|
|||||||
skip_serializing_if = "std::ops::Not::not"
|
skip_serializing_if = "std::ops::Not::not"
|
||||||
)]
|
)]
|
||||||
pub upcoming: bool,
|
pub upcoming: bool,
|
||||||
|
/// The whole RELEASE this row belongs to is gone from the catalog: every
|
||||||
|
/// track withdrawn, or `/album/get` answering unavailable — decided by
|
||||||
|
/// `library_qt::unavailable_release_ids` once the rows are built, one
|
||||||
|
/// probe per distinct album of the pulled rows. A pulled track on a live
|
||||||
|
/// album keeps `false`: its album page still opens. The row's album link
|
||||||
|
/// and "Go to album" go dark on `true` (2026-09-13).
|
||||||
|
#[serde(rename = "releaseUnavailable")]
|
||||||
|
pub release_unavailable: bool,
|
||||||
/// The recording identifier, carried so the replacement search's ISRC
|
/// The recording identifier, carried so the replacement search's ISRC
|
||||||
/// short-circuit can fire (`qbz-playlist-import/src/match_qobuz.rs:156-159`
|
/// short-circuit can fire (`qbz-playlist-import/src/match_qobuz.rs:156-159`
|
||||||
/// scores an ISRC hit 1.0). That is the owner's "a veces cambia el ID del
|
/// scores an ISRC hit 1.0). That is the owner's "a veces cambia el ID del
|
||||||
@@ -1199,6 +1207,8 @@ pub async fn load(
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mixed = !sidecar.is_empty();
|
let mixed = !sidecar.is_empty();
|
||||||
|
// Release-wide withdrawals among the pulled rows (see the field).
|
||||||
|
let gone_releases = crate::library_qt::unavailable_release_ids(runtime, &tracks).await;
|
||||||
let merged = interleave_rows(tracks, sidecar);
|
let merged = interleave_rows(tracks, sidecar);
|
||||||
|
|
||||||
// Display rows + the playable snapshot + the row positions in ONE pass, so
|
// Display rows + the playable snapshot + the row positions in ONE pass, so
|
||||||
@@ -1217,6 +1227,13 @@ pub async fn load(
|
|||||||
}
|
}
|
||||||
rows.push(item);
|
rows.push(item);
|
||||||
}
|
}
|
||||||
|
if !gone_releases.is_empty() {
|
||||||
|
for item in rows.iter_mut() {
|
||||||
|
if item.not_streamable && gone_releases.contains(&item.album_id) {
|
||||||
|
item.release_unavailable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Seam B: a mixed detail plays through local_playlist's merged queue
|
// Seam B: a mixed detail plays through local_playlist's merged queue
|
||||||
// snapshot (source-aware QueueTracks — a local file must never be queued as
|
// snapshot (source-aware QueueTracks — a local file must never be queued as
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//! "Buy on Qobuz" (2026-09-13): a link to the store page in the browser,
|
||||||
|
//! never a purchase inside QBZ. The album page shows the action only when the
|
||||||
|
//! catalog says the release is `purchasable`; the track menus and the player
|
||||||
|
//! bar resolve that at click time — not every row document carries the flag —
|
||||||
|
//! and say so in a toast when a release is not sold. The URL keeps the store
|
||||||
|
//! locale to Qobuz: `/album/-/{id}` answers 301 to the localised page
|
||||||
|
//! (verified 2026-09-13). Tracks are sold on their album's page.
|
||||||
|
|
||||||
|
pub(crate) fn album_store_url(album_id: &str) -> String {
|
||||||
|
format!("https://www.qobuz.com/album/-/{album_id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_store(url: String) {
|
||||||
|
if let Err(error) = open::that(&url) {
|
||||||
|
log::warn!("[qbz-qt] store link {url} failed to open: {error}");
|
||||||
|
crate::toast_qt::error(qbz_i18n::t("Could not open the browser"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_sold() {
|
||||||
|
crate::toast_qt::info(qbz_i18n::t("This release isn't sold on Qobuz."));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown() {
|
||||||
|
crate::toast_qt::info(qbz_i18n::t("Couldn't check the store for this release."));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn buy_album(album_id: String) {
|
||||||
|
if album_id.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let runtime = crate::app();
|
||||||
|
crate::spawn(async move {
|
||||||
|
match runtime.core().get_album(&album_id).await {
|
||||||
|
Ok(album) if album.purchasable == Some(false) => not_sold(),
|
||||||
|
Ok(_) => open_store(album_store_url(&album_id)),
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("[qbz-qt] store: album {album_id} lookup failed: {error}");
|
||||||
|
unknown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn buy_track(track_id: String) {
|
||||||
|
let Ok(id) = track_id.parse::<u64>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let runtime = crate::app();
|
||||||
|
crate::spawn(async move {
|
||||||
|
match runtime.core().get_track(id).await {
|
||||||
|
Ok(track) if track.purchasable == Some(false) => not_sold(),
|
||||||
|
Ok(track) => match track.album.as_ref().map(|a| a.id.clone()) {
|
||||||
|
Some(album_id) if !album_id.is_empty() => open_store(album_store_url(&album_id)),
|
||||||
|
_ => unknown(),
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("[qbz-qt] store: track {id} lookup failed: {error}");
|
||||||
|
unknown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -656,6 +656,46 @@ async fn run_release_search(release: ReleaseSeed, query: String, generation: u64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ranked release candidates for a title (and artist) the catalog no
|
||||||
|
/// longer has — the album page's "you might be interested in these" rail
|
||||||
|
/// (2026-09-13). Same query normalisation, search and ranking as the
|
||||||
|
/// replacement flow; no modal state is touched. Empty when nothing to ask.
|
||||||
|
pub(crate) async fn ranked_alternatives(album_id: &str, title: &str, artist: &str) -> Vec<Album> {
|
||||||
|
let seed: ReleaseSeed = match serde_json::from_value(serde_json::json!({
|
||||||
|
"targetKind": "album",
|
||||||
|
"albumId": album_id,
|
||||||
|
"albumTitle": title,
|
||||||
|
"artist": artist,
|
||||||
|
"albumArtist": artist,
|
||||||
|
})) {
|
||||||
|
Ok(seed) => seed,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
let normalized = qbz_external_reco::normalize_catalog_name(title);
|
||||||
|
let query = if normalized.is_empty() {
|
||||||
|
title.trim().to_string()
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
};
|
||||||
|
if query.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let runtime = crate::app();
|
||||||
|
let found = runtime
|
||||||
|
.core()
|
||||||
|
.search_albums(&query, SEARCH_LIMIT, 0, None)
|
||||||
|
.await
|
||||||
|
.map(|page| page.items)
|
||||||
|
.unwrap_or_else(|error| {
|
||||||
|
log::warn!("[qbz-qt] album alternatives: search '{query}' failed: {error}");
|
||||||
|
Vec::new()
|
||||||
|
});
|
||||||
|
rank_release_candidates(&seed, &found)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(album, _)| album)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn release_quality(album: &Album) -> (u32, f64) {
|
fn release_quality(album: &Album) -> (u32, f64) {
|
||||||
let bit_depth = album
|
let bit_depth = album
|
||||||
.audio_info
|
.audio_info
|
||||||
|
|||||||
Reference in new issue
Block a user