Merge branch 'bugfix/internal/discover-reco-follow-state' into chore/internal/smoke-lyrics-discover

# Conflicts:
#	crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/nl/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po
This commit is contained in:
vicrodh committed 2026-07-22 23:05:16 -06:00
commit 3eb93ec4cb
28 files changed
+1436 -52

No files matched your search

@@ -4794,3 +4794,22 @@ msgstr "Automatisch (Kontosprache)"
msgid "Translation is not available right now. Please try again later."
msgstr "Die Übersetzung ist gerade nicht verfügbar. Bitte versuche es später erneut."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Abgelehnte Künstler verschwinden nur aus deinen Empfehlungen — sie erscheinen weiterhin in der Suche und überall sonst."
msgid "No dismissed artists"
msgstr "Keine abgelehnten Künstler"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Um einen Künstler nicht mehr in Empfehlungen zu sehen, öffne das Menü seiner Karte und wähle Nicht interessiert."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} erscheint nicht mehr in Empfehlungen"
msgid "Artist dismissed from Recommendations"
msgstr "Künstler aus Empfehlungen abgelehnt"
msgid "{} restored to Recommendations"
msgstr "{} wieder in Empfehlungen aufgenommen"
@@ -587,3 +587,25 @@ msgstr "Auto (account language)"
msgid "Translation is not available right now. Please try again later."
msgstr "Translation is not available right now. Please try again later."
# Discover Recommendations — Not interested (reco-scoped dismissals)
msgid "Not interested"
msgstr "Not interested"
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgid "No dismissed artists"
msgstr "No dismissed artists"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} won't appear in Recommendations anymore"
msgid "Artist dismissed from Recommendations"
msgstr "Artist dismissed from Recommendations"
msgid "{} restored to Recommendations"
msgstr "{} restored to Recommendations"
@@ -4824,3 +4824,22 @@ msgstr "Automático (idioma de la cuenta)"
msgid "Translation is not available right now. Please try again later."
msgstr "La traducción no está disponible ahora mismo. Inténtalo de nuevo más tarde."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Los artistas descartados solo desaparecen de tus Recomendaciones — siguen apareciendo en la búsqueda y en el resto de la app."
msgid "No dismissed artists"
msgstr "No hay artistas descartados"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Para dejar de ver un artista en Recomendaciones, abre el menú de su tarjeta y elige No me interesa."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} ya no aparecerá en Recomendaciones"
msgid "Artist dismissed from Recommendations"
msgstr "Artista descartado de Recomendaciones"
msgid "{} restored to Recommendations"
msgstr "{} restaurado en Recomendaciones"
@@ -4794,3 +4794,22 @@ msgstr "Automatique (langue du compte)"
msgid "Translation is not available right now. Please try again later."
msgstr "La traduction n'est pas disponible pour le moment. Veuillez réessayer plus tard."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Les artistes écartés disparaissent uniquement de vos Recommandations — ils restent visibles dans la recherche et partout ailleurs."
msgid "No dismissed artists"
msgstr "Aucun artiste écarté"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Pour ne plus voir un artiste dans Recommandations, ouvrez le menu de sa carte et choisissez Pas intéressé."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} n'apparaîtra plus dans Recommandations"
msgid "Artist dismissed from Recommendations"
msgstr "Artiste écarté des Recommandations"
msgid "{} restored to Recommendations"
msgstr "{} rétabli dans Recommandations"
@@ -4782,3 +4782,22 @@ msgstr "自動(アカウントの言語)"
msgid "Translation is not available right now. Please try again later."
msgstr "翻訳は現在利用できません。後でもう一度お試しください。"
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "除外したアーティストは「おすすめ」からのみ消えます — 検索やその他の場所には引き続き表示されます。"
msgid "No dismissed artists"
msgstr "除外したアーティストはいません"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "おすすめにアーティストを表示しないようにするには、そのカードのメニューを開いて「興味なし」を選んでください。"
msgid "{} won't appear in Recommendations anymore"
msgstr "{} はおすすめに表示されなくなります"
msgid "Artist dismissed from Recommendations"
msgstr "アーティストをおすすめから除外しました"
msgid "{} restored to Recommendations"
msgstr "{} をおすすめに復元しました"
@@ -4806,3 +4806,22 @@ msgstr "Automatisch (accounttaal)"
msgid "Translation is not available right now. Please try again later."
msgstr "Vertaling is nu niet beschikbaar. Probeer het later opnieuw."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Afgewezen artiesten verdwijnen alleen uit je Aanbevelingen — ze blijven zichtbaar in zoekresultaten en overal elders."
msgid "No dismissed artists"
msgstr "Geen afgewezen artiesten"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Om een artiest niet meer in Aanbevelingen te zien, open je het kaartmenu en kies je Niet geïnteresseerd."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} verschijnt niet meer in Aanbevelingen"
msgid "Artist dismissed from Recommendations"
msgstr "Artiest afgewezen voor Aanbevelingen"
msgid "{} restored to Recommendations"
msgstr "{} hersteld in Aanbevelingen"
@@ -4794,3 +4794,22 @@ msgstr "Automático (idioma da conta)"
msgid "Translation is not available right now. Please try again later."
msgstr "A tradução não está disponível de momento. Tente novamente mais tarde."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Artistas dispensados só saem das suas Recomendações — continuam aparecendo na busca e em todo o resto."
msgid "No dismissed artists"
msgstr "Nenhum artista dispensado"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Para deixar de ver um artista em Recomendações, abra o menu do seu cartão e escolha Não tenho interesse."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} não aparecerá mais em Recomendações"
msgid "Artist dismissed from Recommendations"
msgstr "Artista dispensado das Recomendações"
msgid "{} restored to Recommendations"
msgstr "{} restaurado em Recomendações"
@@ -4808,3 +4808,22 @@ msgstr "Автоматически (язык аккаунта)"
msgid "Translation is not available right now. Please try again later."
msgstr "Перевод сейчас недоступен. Попробуйте ещё раз позже."
# Discover Recommendations — reco-scoped dismissals (Not interested management)
msgid "Dismissed artists only leave your Recommendations — they still appear in search and everywhere else."
msgstr "Отклонённые исполнители исчезают только из Рекомендаций — они по-прежнему видны в поиске и повсюду остальном."
msgid "No dismissed artists"
msgstr "Нет отклонённых исполнителей"
msgid "To stop seeing an artist in Recommendations, open its card menu and choose Not interested."
msgstr "Чтобы больше не видеть исполнителя в Рекомендациях, откройте меню его карточки и выберите Не интересно."
msgid "{} won't appear in Recommendations anymore"
msgstr "{} больше не появится в Рекомендациях"
msgid "Artist dismissed from Recommendations"
msgstr "Исполнитель отклонён из Рекомендаций"
msgid "{} restored to Recommendations"
msgstr "{} возвращён в Рекомендации"
+10 -1
View File
@@ -203,7 +203,7 @@ export component ArtistGridCard inherits Rectangle {
x: 21px;
y: 143px;
width: 196px;
height: 3 * 33px + 10px;
height: 4 * 33px + 10px;
ContextMenu {
ContextMenuItem {
icon: @image-url("../assets/icons/user.svg");
@@ -228,6 +228,15 @@ export component ArtistGridCard inherits Rectangle {
root.media-action(root.follow-kind, root.artist.id, "follow");
}
}
// Reco-scoped dismissal (NOT the blacklist): the artist
// leaves the Recommendations rails only.
ContextMenuItem {
icon: @image-url("../assets/icons/thumbs-down.svg");
label: @tr("Not interested");
clicked => {
root.media-action("artist", root.artist.id, "not-interested");
}
}
}
}
}
@@ -245,6 +245,96 @@ component AlbumRow inherits Rectangle {
}
}
// One reco-dismissed artist row (the Recommendations tab). Lighter than
// ArtistRow: the dismissal store carries no timestamp/notes, and the avatar
// is the generic user glyph (NOT blind-eye — this is not the blacklist).
component DismissedRow inherits Rectangle {
in property <string> artist-name;
property <bool> hovered: ta.has-hover || rm-ta.has-hover;
callback select();
callback remove();
callback hover-changed(bool);
height: 64px;
border-radius: Radius.md;
background: root.hovered ? Theme.surface-elevated : Theme.surface-card;
changed hovered => {
root.hover-changed(self.hovered);
}
HorizontalLayout {
padding-left: 16px;
padding-right: 8px;
spacing: 12px;
// Clickable artist-info region — opens the artist page.
Rectangle {
horizontal-stretch: 1;
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.select();
}
}
HorizontalLayout {
spacing: 12px;
VerticalLayout {
alignment: center;
Rectangle {
width: 44px;
height: 44px;
border-radius: 22px;
background: Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/user.svg");
width: 20px;
height: 20px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: Theme.text-muted;
}
}
}
VerticalLayout {
alignment: center;
Text {
text: root.artist-name;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.medium;
overflow: elide;
}
}
}
}
// Per-row undo (optimistic, no confirm — mirrors the blacklist rows).
VerticalLayout {
alignment: center;
Rectangle {
width: 36px;
height: 36px;
border-radius: Radius.sm;
background: rm-ta.has-hover ? Theme.danger-bg : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 18px;
height: 18px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: rm-ta.has-hover ? Theme.danger : Theme.text-muted;
}
rm-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.remove();
}
}
}
}
}
}
export component BlacklistManagerView inherits Rectangle {
background: ShellState.app-background-active ? transparent : Theme.surface-main;
@@ -347,6 +437,28 @@ export component BlacklistManagerView inherits Rectangle {
}
}
}
// Recommendations tab (reco-scoped "Not interested" dismissals).
Rectangle {
width: reco-lbl.preferred-width + 28px;
border-radius: Radius.sm;
background: BlacklistState.active-tab == 2
? Theme.surface-card
: (reco-ta.has-hover ? Theme.surface-hover : transparent);
reco-lbl := Text {
text: @tr("Recommendations");
color: BlacklistState.active-tab == 2 ? Theme.text-primary : Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.medium;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
reco-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
BlacklistActions.set-tab(2);
}
}
}
}
}
}
@@ -356,7 +468,9 @@ export component BlacklistManagerView inherits Rectangle {
vertical-stretch: 0;
text: BlacklistState.active-tab == 0
? @tr("Blacklisted artists are hidden from search results, radio, suggestions, and similar artists.")
: @tr("Blocked albums are hidden from search, discovery, and listings — even when their artist is allowed.");
: BlacklistState.active-tab == 1
? @tr("Blocked albums are hidden from search, discovery, and listings — even when their artist is allowed.")
: @tr("Dismissed artists only leave your Recommendations — they still appear in search and everywhere else.");
color: Theme.text-muted;
font-size: Typography.body;
wrap: word-wrap;
@@ -473,7 +587,9 @@ export component BlacklistManagerView inherits Rectangle {
}
// Clear-All — only when the active tab has something to clear.
if (BlacklistState.active-tab == 0 ? BlacklistState.count : BlacklistState.album-count) > 0: Rectangle {
// The Recommendations tab has no bulk clear (undo is per-row).
if (BlacklistState.active-tab == 0 ? BlacklistState.count
: BlacklistState.active-tab == 1 ? BlacklistState.album-count : 0) > 0: Rectangle {
width: clear-all-row.preferred-width + 28px;
height: 34px;
border-radius: Radius.sm;
@@ -519,7 +635,9 @@ export component BlacklistManagerView inherits Rectangle {
Text {
text: BlacklistState.active-tab == 0
? @tr("{} artists", BlacklistState.count)
: @tr("{} albums", BlacklistState.album-count);
: BlacklistState.active-tab == 1
? @tr("{} albums", BlacklistState.album-count)
: @tr("{} artists", BlacklistState.dismissed-count);
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: right;
@@ -774,6 +892,100 @@ export component BlacklistManagerView inherits Rectangle {
hover-active: root.list-hover-rows > 0;
}
}
// --- Recommendations tab branches (reco-scoped dismissals) -----
// 2c) Empty (nothing dismissed).
if BlacklistState.active-tab == 2 && !BlacklistState.loading && BlacklistState.dismissed-count == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/thumbs-down.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No dismissed artists");
color: Theme.text-muted;
font-size: Typography.heading;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("To stop seeing an artist in Recommendations, open its card menu and choose Not interested.");
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
}
}
}
// 3c) No search results (dismissed).
if BlacklistState.active-tab == 2 && !BlacklistState.loading && BlacklistState.dismissed-count > 0 && BlacklistState.dismissed-items.length == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No results for \"{}\"", BlacklistState.search-query);
color: Theme.text-muted;
font-size: Typography.heading;
}
}
}
// 4c) The dismissed-artists list (per-row undo).
if BlacklistState.active-tab == 2 && !BlacklistState.loading && BlacklistState.dismissed-items.length > 0: Rectangle {
dismissed-flick := Flickable {
width: 100%;
height: 100%;
viewport-width: self.width;
viewport-height: dismissed-content.preferred-height;
dismissed-content := VerticalLayout {
width: parent.width - 18px;
spacing: 4px;
for item in BlacklistState.dismissed-items: DismissedRow {
artist-name: item.artist-name;
select => {
BlacklistActions.artist-select(item.artist-id);
}
remove => {
BlacklistActions.remove-dismissed(item.artist-id);
}
hover-changed(h) => {
root.list-hover-rows = h ? root.list-hover-rows + 1 : Math.max(0, root.list-hover-rows - 1);
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: dismissed-flick.viewport-height;
visible-height: dismissed-flick.height;
viewport-y <=> dismissed-flick.viewport-y;
auto-hide: true;
hover-active: root.list-hover-rows > 0;
}
}
}
}
+18 -2
View File
@@ -2462,6 +2462,16 @@ export struct BlacklistedAlbumItem {
has-notes: bool,
}
// One reco-dismissed artist ("Not interested"). Reco-SCOPED — NOT the
// blacklist: the artist only leaves the Recommendations rails. The store
// carries no timestamp/notes, so the row is just the name (+ a generic
// avatar); removal is the undo.
export struct DismissedArtistItem {
// Numeric Qobuz artist id (same int pass-through as BlacklistedArtistItem).
artist-id: int,
artist-name: string,
}
export global BlacklistState {
// The CURRENTLY-VISIBLE rows (already search-filtered by the controller).
in property <[BlacklistedArtistItem]> items: [];
@@ -2481,7 +2491,11 @@ export global BlacklistState {
in property <[BlacklistedAlbumItem]> album-items: [];
// FULL blocked-album count (drives the Albums tab badge + empty state).
in property <int> album-count: 0;
// Active manager tab: 0 = Artists, 1 = Albums.
// --- Reco-dismissal axis ("Not interested" — the Recommendations tab) ---
in property <[DismissedArtistItem]> dismissed-items: [];
// FULL dismissed count (drives the tab badge + empty state).
in property <int> dismissed-count: 0;
// Active manager tab: 0 = Artists, 1 = Albums, 2 = Recommendations.
in property <int> active-tab: 0;
}
@@ -2498,12 +2512,14 @@ export global BlacklistActions {
callback search-changed(string /* query */);
// --- Album axis ---
callback set-tab(int /* 0=artists, 1=albums */);
callback set-tab(int /* 0=artists, 1=albums, 2=recommendations */);
callback album-select(string /* album id */);
callback remove-album(string /* album id */);
callback clear-all-albums();
// Block an album from a context menu (id + display fields for the manager).
callback block-album(string /* id */, string /* title */, string /* artist */, string /* cover-url */);
// --- Reco-dismissal axis ---
callback remove-dismissed(int /* artist id */);
}
// === My QBZ — Mixtapes & Collections (read-only index grids) ============