// Library > Favorites — the user's saved tracks / albums / artists / // playlists / labels. Mirrors Tauri's FavoritesView.svelte core: a // tab bar over per-tab content. Tracks reuse the shared // TrackRow, albums the Discover AlbumCard, artists a round- // portrait grid. Playlists + Labels are stubbed pending their // backing views. import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { FavoritesState, FavoritesActions, AlbumCardItem, TrackItem, FavoriteArtistItem, FavoritePlaylistItem, FavoriteLabelItem, DragState, } from "../state.slint"; import { AlbumCard } from "../discover/AlbumCard.slint"; import { TrackRow } from "../primitives/TrackRow.slint"; import { ExpandableSearch } from "../primitives/ExpandableSearch.slint"; import { CircleAction } from "../primitives/CircleAction.slint"; import { QbzIcon } from "../primitives/QbzIcon.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { ListScrollbar } from "../primitives/ListScrollbar.slint"; import { NavButtons } from "../shell/NavButtons.slint"; // One tab in the favorites tab bar. component FavTab inherits Rectangle { in property label; in property id; in property count; callback selected(string); property active: FavoritesState.active-tab == root.id; height: 34px; width: tab-row.preferred-width; tab-row := HorizontalLayout { padding-left: 4px; padding-right: 4px; spacing: 6px; Text { text: root.label; color: root.active ? Theme.text-primary : Theme.text-muted; font-size: Typography.body; font-weight: root.active ? Typography.semibold : Typography.regular; vertical-alignment: center; } if root.count > 0: Text { text: root.count; color: Theme.text-muted; font-size: Typography.legal; vertical-alignment: center; } } // Active underline. Rectangle { y: parent.height - 2px; height: 2px; width: parent.width; background: root.active ? Theme.accent : transparent; border-radius: 1px; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.selected(root.id); } } } component FavPlaylistCard inherits Rectangle { in property item; callback clicked(string); width: 180px; height: 234px; VerticalLayout { spacing: 8px; Rectangle { width: 180px; height: 180px; border-radius: Radius.sm; background: Theme.surface-elevated; clip: true; Image { width: 100%; height: 100%; source: root.item.cover; image-fit: cover; } if root.item.cover-url == "": QbzIcon { source: @image-url("../assets/icons/list-music.svg"); width: 44px; height: 44px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: Theme.text-muted; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(root.item.id); } } } Text { text: root.item.name; color: Theme.text-primary; font-size: Typography.body; font-weight: Typography.medium; overflow: elide; width: 180px; } Text { text: root.item.tracks-line; color: Theme.text-muted; font-size: Typography.legal; overflow: elide; width: 180px; } } } component FavLabelRow inherits Rectangle { in property item; in property index; callback clicked(string, string); height: 52px; border-radius: 8px; background: ta.has-hover ? #ffffff16 : (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent); HorizontalLayout { padding-left: 12px; padding-right: 12px; spacing: 12px; VerticalLayout { alignment: center; Rectangle { width: 32px; height: 32px; border-radius: 6px; background: Theme.surface-elevated; QbzIcon { source: @image-url("../assets/icons/disc-3.svg"); width: 18px; height: 18px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: Theme.text-muted; } } } VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: root.item.name; color: Theme.text-primary; font-size: Typography.body; font-weight: Typography.medium; overflow: elide; } if root.item.albums-line != "": Text { text: root.item.albums-line; color: Theme.text-muted; font-size: Typography.legal; } } } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(root.item.id, root.item.name); } } } component FavArtistCard inherits Rectangle { in property item; callback clicked(string); width: 160px; height: 200px; VerticalLayout { spacing: 10px; Rectangle { width: 140px; height: 140px; x: (parent.width - self.width) / 2; border-radius: 70px; background: Theme.surface-elevated; clip: true; Image { width: 100%; height: 100%; source: root.item.image; image-fit: cover; } if root.item.image-url == "": QbzIcon { source: @image-url("../assets/icons/user.svg"); width: 44px; height: 44px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; tint: Theme.text-muted; } ta := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(root.item.id); } } } Text { text: root.item.name; color: Theme.text-primary; font-size: Typography.body; font-weight: Typography.medium; horizontal-alignment: center; overflow: elide; width: 160px; } if root.item.albums-line != "": Text { text: root.item.albums-line; color: Theme.text-muted; font-size: Typography.legal; horizontal-alignment: center; width: 160px; } } } export component FavoritesView inherits Rectangle { callback open-album(string); callback open-artist(string); property album-w: 220px; property album-h: 266px; property artist-w: 160px; property artist-h: 200px; property gap: 24px; background: Theme.surface-main; flick := Flickable { interactive: !DragState.active; viewport-width: self.width; viewport-height: page.preferred-height; page := VerticalLayout { width: parent.width; padding-left: 32px; padding-right: 32px; padding-top: 11px; padding-bottom: 100px; spacing: 0px; alignment: start; // Back / forward nav buttons — same position as every other // page (11px top padding, then a 22px gap to the header). HorizontalLayout { alignment: start; NavButtons { } } Rectangle { height: 22px; } // Page title — same size as the album name in AlbumView // (Typography.section), not the larger Typography.title. HorizontalLayout { spacing: 12px; VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: @tr("Favorites"); color: Theme.text-primary; font-size: Typography.section; font-weight: Typography.bold; } } // Tracks header actions — Play all + Shuffle the favorites // queue. Shown only when the Tracks tab has rendered rows. if FavoritesState.active-tab == "tracks" && FavoritesState.tracks-visible.length > 0: HorizontalLayout { spacing: 8px; VerticalLayout { alignment: center; CircleAction { icon: @image-url("../assets/icons/play-fill.svg"); primary: true; clicked => { FavoritesActions.play-all-tracks(); } } } VerticalLayout { alignment: center; CircleAction { icon: @image-url("../assets/icons/shuffle.svg"); clicked => { FavoritesActions.shuffle-tracks(); } } } } } Rectangle { height: 16px; } // --- Tab bar ----------------------------------------------- HorizontalLayout { spacing: 20px; alignment: start; FavTab { label: @tr("Tracks"); id: "tracks"; count: FavoritesState.tracks-total; selected(id) => { FavoritesActions.select-tab(id); } } FavTab { label: @tr("Albums"); id: "albums"; count: FavoritesState.albums-total; selected(id) => { FavoritesActions.select-tab(id); } } FavTab { label: @tr("Artists"); id: "artists"; count: FavoritesState.artists-total; selected(id) => { FavoritesActions.select-tab(id); } } FavTab { label: @tr("Playlists"); id: "playlists"; count: FavoritesState.playlists-total; selected(id) => { FavoritesActions.select-tab(id); } } FavTab { label: @tr("Labels"); id: "labels"; count: FavoritesState.labels-total; selected(id) => { FavoritesActions.select-tab(id); } } } Rectangle { height: 1px; background: Theme.border-subtle; } Rectangle { height: 20px; } if FavoritesState.loading: HorizontalLayout { alignment: center; LoadingSpinner { size: 36px; } } // --- Tracks tab -------------------------------------------- if !FavoritesState.loading && FavoritesState.active-tab == "tracks": VerticalLayout { spacing: 0px; // Toolbar — results count (left) + expandable search (right). if FavoritesState.tracks.length > 0: HorizontalLayout { spacing: 8px; VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: FavoritesState.tracks-visible.length + (FavoritesState.tracks-search != "" ? " / " + FavoritesState.tracks.length : "") + " " + @tr("tracks"); color: Theme.text-muted; font-size: Typography.legal; } } VerticalLayout { alignment: center; ExpandableSearch { placeholder: @tr("Search"); query <=> FavoritesState.tracks-search; edited(q) => { FavoritesActions.search-tracks(q); } } } } if FavoritesState.tracks.length > 0: Rectangle { height: 12px; } if FavoritesState.tracks.length == 0: Text { text: @tr("No favorite tracks yet."); color: Theme.text-muted; font-size: Typography.body; } // Search filtered everything out. if FavoritesState.tracks.length > 0 && FavoritesState.tracks-visible.length == 0: Text { text: @tr("No tracks match your search."); color: Theme.text-muted; font-size: Typography.body; } for track[i] in FavoritesState.tracks-visible: TrackRow { track: track; index: i; show-artwork: true; show-album: true; show-favorite: true; // Album / artist name links open the respective page; the // row drops `kind`, so route those explicitly (the // track-action path forces kind="track" and would treat an // album-id as an artist-id). media-action(kind, id, action) => { if (kind == "album" && action == "open") { FavoritesActions.open-album(id); } else if (kind == "artist" && action == "open") { FavoritesActions.open-artist(id); } else { FavoritesActions.track-action(id, action); } } } } // --- Albums tab -------------------------------------------- if !FavoritesState.loading && FavoritesState.active-tab == "albums": albums-grid := Rectangle { property columns: Math.max( 1, Math.floor((self.width + root.gap) / (root.album-w + root.gap))); property rows: Math.ceil(FavoritesState.albums.length / self.columns); height: self.rows > 0 ? self.rows * root.album-h + Math.max(0, self.rows - 1) * root.gap : 0px; if FavoritesState.albums.length == 0: Text { text: @tr("No favorite albums yet."); color: Theme.text-muted; font-size: Typography.body; } for album[i] in FavoritesState.albums: AlbumCard { x: Math.mod(i, albums-grid.columns) * (root.album-w + root.gap); y: Math.floor(i / albums-grid.columns) * (root.album-h + root.gap); width: root.album-w; height: root.album-h; album: album; clicked(id) => { root.open-album(id); } media-action(kind, id, action) => { if (kind == "artist") { root.open-artist(id); } } } } // --- Artists tab ------------------------------------------- if !FavoritesState.loading && FavoritesState.active-tab == "artists": artists-grid := Rectangle { property columns: Math.max( 1, Math.floor((self.width + root.gap) / (root.artist-w + root.gap))); property rows: Math.ceil(FavoritesState.artists.length / self.columns); height: self.rows > 0 ? self.rows * root.artist-h + Math.max(0, self.rows - 1) * root.gap : 0px; if FavoritesState.artists.length == 0: Text { text: @tr("No favorite artists yet."); color: Theme.text-muted; font-size: Typography.body; } for artist[i] in FavoritesState.artists: FavArtistCard { x: Math.mod(i, artists-grid.columns) * (root.artist-w + root.gap); y: Math.floor(i / artists-grid.columns) * (root.artist-h + root.gap); item: artist; clicked(id) => { root.open-artist(id); } } } // --- Playlists tab ----------------------------------------- if !FavoritesState.loading && FavoritesState.active-tab == "playlists": playlists-grid := Rectangle { property pl-w: 180px; property pl-h: 234px; property columns: Math.max( 1, Math.floor((self.width + root.gap) / (self.pl-w + root.gap))); property rows: Math.ceil(FavoritesState.playlists.length / self.columns); height: self.rows > 0 ? self.rows * self.pl-h + Math.max(0, self.rows - 1) * root.gap : 0px; if FavoritesState.playlists.length == 0: Text { text: @tr("No favorite playlists yet."); color: Theme.text-muted; font-size: Typography.body; } for pl[i] in FavoritesState.playlists: FavPlaylistCard { x: Math.mod(i, playlists-grid.columns) * (playlists-grid.pl-w + root.gap); y: Math.floor(i / playlists-grid.columns) * (playlists-grid.pl-h + root.gap); item: pl; clicked(id) => { FavoritesActions.open-playlist(id); } } } // --- Labels tab -------------------------------------------- if !FavoritesState.loading && FavoritesState.active-tab == "labels": VerticalLayout { spacing: 0px; if FavoritesState.labels.length == 0: Text { text: @tr("No favorite labels yet."); color: Theme.text-muted; font-size: Typography.body; } for label[i] in FavoritesState.labels: FavLabelRow { item: label; index: i; clicked(id, name) => { FavoritesActions.open-label(id, name); } } } } } // Shared scrollbar in the right gutter (homologated cross-view). ListScrollbar { x: parent.width - self.width - 4px; width: 14px; height: 100%; viewport-height: flick.viewport-height; visible-height: flick.height; viewport-y <=> flick.viewport-y; } }