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

+5 -1
View File
@@ -17,7 +17,7 @@ import { LoginScreen } from "login/LoginScreen.slint";
import { AppShell } from "shell/AppShell.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, AlbumState, ArtistState, NavState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions } from "state.slint";
export { HomeState, AlbumState, ArtistState, NavState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`.
@@ -46,6 +46,7 @@ export component AppWindow inherits Window {
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);
@@ -80,6 +81,9 @@ export component AppWindow inherits Window {
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
header-menu-navigate(route) => {
root.header-menu-navigate(route);
}
settings-bool(k, v) => {
root.settings-bool(k, v);
}