feat(reco): wire the external-recommendations 4th Discover tab (Slint)

Adds "Recommendations" as a 4th Discover tab (alongside Home / Editor's Picks /
For You), rendering the qbz-external-reco engine output. NOT part of For You.

- ui: new ExternalRecoState global + ExternalRecoView (4 personalized carousels
  C1-C4 with length>0 self-hide gates + the editorial cold-start rows), 4th tab
  pill button in HomeView, mount on active-tab == "recommendations", skeleton
  gate. Export the global from app.slint.
- bin: external_reco controller — RecoCatalog over QbzCore, per-user resolution
  cache lifecycle, LocalHistory from reco_store (known artists + recent plays),
  scrobbler-username gate (is_authed), build + apply into ExternalRecoState
  (models built on the UI thread; slint::Image is !Send) + artwork jobs.
- bin: 6 ArtworkTarget variants + resolution arms; reco::recent_track_ids
  wrapper; home::select_tab + discover_prefs::push_descriptors treat the new tab
  like For You (own view, Home/Editor pushed empty); lazy-load hooks at the tab
  click + both Discover nav-restore sites.

cargo check -p qbz green. Last.fm reads still need the proxy allowedMethods
deploy (separate qbz-api-proxy repo) before they return data.

cc
This commit is contained in:
vicrodh committed 2026-06-27 03:12:05 -06:00
1 parent 408b3c8308
commit ec98245364
12 files changed
+588 -7

No files matched your search

+24 -1
View File
@@ -15,6 +15,7 @@ import { PlaylistCarousel } from "PlaylistCarousel.slint";
import { PlaylistTagFilter } from "PlaylistTagFilter.slint";
import { HomeSkeleton } from "HomeSkeleton.slint";
import { ForYouView } from "ForYouView.slint";
import { ExternalRecoView } from "ExternalRecoView.slint";
import { GenreFilterPopup } from "GenreFilterPopup.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
@@ -210,6 +211,11 @@ export component HomeView inherits Rectangle {
active: HomeState.active-tab == "forYou";
clicked => { HomeActions.select-tab("forYou"); }
}
Tab {
label: @tr("Recommendations");
active: HomeState.active-tab == "recommendations";
clicked => { HomeActions.select-tab("recommendations"); }
}
}
}
}
@@ -287,7 +293,7 @@ export component HomeView inherits Rectangle {
// would suppress it — gate on `loading` alone. The empty-data
// placeholders below are gated off while loading, so ONLY the
// skeleton shows during the fetch (1:1 Tauri).
if DiscoverState.active-tab != "forYou" && HomeState.loading: HomeSkeleton { }
if DiscoverState.active-tab != "forYou" && DiscoverState.active-tab != "recommendations" && HomeState.loading: HomeSkeleton { }
// Prefs-driven render loop (Slice 5). Rust pushes the ordered
// ENABLED descriptor list for the active tab (home-sections /
@@ -445,6 +451,23 @@ export component HomeView inherits Rectangle {
root.media-action("mix", which, "open");
}
}
// Recommendations (4th tab) — external-reco carousels from
// ExternalRecoState (lazy-loaded on first open).
if DiscoverState.active-tab == "recommendations": ExternalRecoView {
open-album(id) => {
root.album-clicked(id);
}
open-artist(id) => {
root.open-artist(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
play-track(id) => {
root.media-action("track", id, "play");
}
}
}
}
// Shared scrollbar in the right gutter (homologated cross-view).