feat(slint): Library > Favorites view (Tracks / Albums / Artists)

Wires the Library header dropdown to a real Favorites view. Mirrors
Tauri's FavoritesView.svelte core: a tab bar over per-tab content,
each tab lazy-loaded on first open.

Slint:
  - ContentView gains `favorites`. NavMenuEntry gains a `route`
    field; the Library dropdown rows carry "favorites-tracks" /
    "-albums" / "-artists" / "-playlists" / "-labels". The
    HeaderMenuOverlay click bubbles the route up through AppShell ->
    AppWindow -> Rust (new header-menu-navigate callback).
  - FavoritesState / FavoritesActions globals. Tracks reuse
    SearchTrackItem (so the shared SearchTrackRow renders them with
    per-row cover art), albums reuse AlbumCardItem + the Discover
    AlbumCard, artists use a new FavoriteArtistItem round-portrait
    grid.
  - New ui/favorites/FavoritesView.slint: title, tab bar with live
    counts + active underline, and the three populated tabs.
    Playlists / Labels show "Coming soon" pending their backing
    views. AppShell routes ContentView.favorites.

Rust:
  - crate::favorites controller — load_favorites parses the raw
    get_favorites JSON (`{ <type>: { items, total } }`) into typed
    qbz-models Track / Album / Artist, maps to the Slint structs.
    Lazy per-tab: select-tab and the header route both call
    navigate_favorites, which fetches only the opened tab.
  - artwork::ArtworkTarget gains FavoriteTrack / FavoriteAlbum /
    FavoriteArtist; covers stream in via the shared pipeline.
  - main.rs: navigate_favorites + on_header_menu_navigate +
    FavoritesActions (select-tab / open-album / open-artist /
    play-track / track-action). No new qbz-core method —
    get_favorites already existed.

Deferred: Playlists + Labels tab content, full pagination (one 500
page is fetched per tab), favorites preferences (tab order).

cc
This commit is contained in:
vicrodh committed 2026-05-20 20:53:37 -06:00
1 parent cdb93341d0
commit a40c6ce569
9 files changed
+831 -10

No files matched your search

+15 -1
View File
@@ -19,6 +19,7 @@ import { ArtistPageView } from "../artist/ArtistPageView.slint";
import { MusicianPageView } from "../musician/MusicianPageView.slint";
import { LabelReleasesView } from "../label/LabelReleasesView.slint";
import { ArtistsByLocationView } from "../location/ArtistsByLocationView.slint";
import { FavoritesView } from "../favorites/FavoritesView.slint";
import { SettingsView } from "../settings/SettingsView.slint";
import { SearchResultsView } from "../search/SearchResultsView.slint";
@@ -26,6 +27,7 @@ export component AppShell inherits Rectangle {
callback open-album(string);
callback open-artist(string);
callback media-action(string, string, string);
callback header-menu-navigate(string);
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
@@ -115,6 +117,14 @@ export component AppShell inherits Rectangle {
root.open-artist(id);
}
}
if NavState.view == ContentView.favorites: FavoritesView {
open-album(id) => {
root.open-album(id);
}
open-artist(id) => {
root.open-artist(id);
}
}
if NavState.view == ContentView.settings: SettingsView {
settings-bool(k, v) => {
root.settings-bool(k, v);
@@ -163,5 +173,9 @@ export component AppShell inherits Rectangle {
// the header and the content region. A plain in-window overlay (not
// a PopupWindow) — it does not grab the pointer, so the header tabs
// stay hoverable and menus can be hover-switched instantly.
HeaderMenuOverlay { }
HeaderMenuOverlay {
navigate(route) => {
root.header-menu-navigate(route);
}
}
}
+6 -6
View File
@@ -266,7 +266,7 @@ export component HeaderBar inherits Rectangle {
show-icon: root.show-tab-icons;
// Discover rows are text-only (no glyphs).
items: [
{ label: @tr("Home"), has-glyph: false },
{ label: @tr("Home"), has-glyph: false, route: "home" },
{ label: @tr("Editor's Picks"), has-glyph: false },
{ label: @tr("For You"), has-glyph: false },
];
@@ -279,11 +279,11 @@ export component HeaderBar inherits Rectangle {
label: @tr("Library");
show-icon: root.show-tab-icons;
items: [
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true },
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true },
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true },
{ label: @tr("Playlists"), icon: @image-url("../assets/icons/list-music.svg"), has-glyph: true },
{ label: @tr("Labels"), icon: @image-url("../assets/icons/disc-3.svg"), has-glyph: true },
{ label: @tr("Tracks"), icon: @image-url("../assets/icons/music.svg"), has-glyph: true, route: "favorites-tracks" },
{ label: @tr("Albums"), icon: @image-url("../assets/icons/disc.svg"), has-glyph: true, route: "favorites-albums" },
{ label: @tr("Artists"), icon: @image-url("../assets/icons/user.svg"), has-glyph: true, route: "favorites-artists" },
{ label: @tr("Playlists"), icon: @image-url("../assets/icons/list-music.svg"), has-glyph: true, route: "favorites-playlists" },
{ label: @tr("Labels"), icon: @image-url("../assets/icons/disc-3.svg"), has-glyph: true, route: "favorites-labels" },
];
}
NavTabWithMenu {
@@ -26,6 +26,10 @@ export component HeaderMenuOverlay inherits Rectangle {
// while a menu is open.
visible: HeaderMenuState.open-index >= 0;
// Bubbles a row's destination route (e.g. "favorites-tracks") up
// to the shell so Rust can run the navigation + data load.
callback navigate(string);
// (Re)arms the idle countdown — called on any pointer activity over
// the panel or its rows.
function reset-idle() {
@@ -111,7 +115,9 @@ export component HeaderMenuOverlay inherits Rectangle {
icon: entry.icon;
has-glyph: entry.has-glyph;
clicked => {
if (idx == HeaderMenuState.home-item) {
if (entry.route != "") {
root.navigate(entry.route);
} else if (idx == HeaderMenuState.home-item) {
NavState.view = ContentView.home;
}
root.close-menu();