feat(reco): Recommendations in all Discover flyouts + opt-out setting

Surface the new Recommendations tab everywhere Discover sub-sections are listed,
and make it opt-out.

- Flyouts: the sidebar section-nav flyout and both header nav flyouts (full text
  + compact icon) now list "Recommendations" alongside Home / Editor's Picks /
  For You. The Rust route handler already maps "discover-recommendations"
  (strip_prefix + select_tab + ensure_loaded), so no routing change was needed.
- Opt-out: a show_recommendations flag (default on) persisted in the per-user
  discover-prefs store (qbz_app), seeded into SettingsState on session start.
  Settings > Integrations gains a "Show Recommendations in Discover" toggle whose
  description nudges connecting Last.fm + ListenBrainz (MusicBrainz is used
  automatically to match releases). Off => the tab pill and every flyout entry
  disappear.
- Gating uses two mounted instances per flyout (one with the entry, one without)
  because Slint can't type-infer [NavMenuEntry] from a ternary of array literals;
  the HomeView pill uses a plain if.

cargo check -p qbz green.

cc
This commit is contained in:
vicrodh committed 2026-06-27 11:57:46 -06:00
1 parent 05df99b713
commit 332fa430e5
9 files changed
+117 -9

No files matched your search

@@ -157,6 +157,8 @@ pub struct DiscoverPrefs {
pub home: Vec<SectionPref>,
pub editor_picks: Vec<SectionPref>,
pub for_you: Vec<SectionPref>,
/// Opt-out: show the external "Recommendations" tab in Discover. Default on.
pub show_recommendations: bool,
}
/// The exact `DEFAULT_PREFS` from `sectionPrefs.ts`.
@@ -204,6 +206,7 @@ pub fn default_prefs() -> DiscoverPrefs {
pref(ArtistsToFollow, true),
pref(ArtistSpotlight, true),
],
show_recommendations: true,
}
}
@@ -295,6 +298,7 @@ impl DiscoverPrefs {
"home": arr(&self.home),
"editorPicks": arr(&self.editor_picks),
"forYou": arr(&self.for_you),
"showRecommendations": self.show_recommendations,
})
}
@@ -313,6 +317,8 @@ impl DiscoverPrefs {
home: reconcile_list(Some(arr), &defaults.home),
editor_picks: defaults.editor_picks,
for_you: defaults.for_you,
// Legacy V1 (home-only array) predates the flag -> default on.
show_recommendations: true,
}
} else if value.is_object() {
let get = |key: &str| value.get(key).and_then(|v| v.as_array());
@@ -320,6 +326,11 @@ impl DiscoverPrefs {
home: reconcile_list(get("home"), &defaults.home),
editor_picks: reconcile_list(get("editorPicks"), &defaults.editor_picks),
for_you: reconcile_list(get("forYou"), &defaults.for_you),
// Missing key (older persisted blob) -> default on.
show_recommendations: value
.get("showRecommendations")
.and_then(|v| v.as_bool())
.unwrap_or(true),
}
} else {
defaults