feat(home): Recently Played Albums gets a View-all full page

The rail's 'View all' opens a dedicated page listing the recently
played albums (the dedicated album history from the #567 fix, cap 24).
Built on the AwardAlbumsView skeleton stripped to essentials: fixed
header + windowed AlbumCollectionView wired per the new sampler
contract, scroll-restore, shared scrollbar. Works offline (local
history — the AppShell arm deliberately skips the offline gate) with
mixed-source rows routing through the existing open-album funnel
(qobuz/plex/local) and source-split artwork dispatch mirroring the
rail's. Full nav integration: NavEntry::RecentAlbums round-trips
through last_nav so view persistence restores it (a local JSON read —
cannot crash-loop), ADR-004 back button via the standard record +
navigate + flags shape. Zero new msgids — title, empty state and the
View-all link all reuse existing strings across the 8 locales.

cc
This commit is contained in:
vicrodh committed 2026-07-08 14:07:16 -06:00
1 parent addc18b219
commit d3b00b78ef
9 files changed
+353 -33

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ export { ThemeState } from "foundation/theme-state.slint";
export { Typography } from "foundation/typography.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, HomeActions, DiscoverState, DiscoverActions, SectionDescriptor, ConfigRow, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, ExternalRecoState, ExternalRecoActions, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, LyricsState, LyricsLineItem, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, AwardState, AwardActions, AwardEntry, ArtistReleasesState, ArtistReleasesActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, DuplicateConfirmState, DuplicateConfirmActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, BlacklistState, BlacklistActions, BlacklistedArtistItem, MyQbzState, MyQbzActions, MixtapeCardItem, MyQbzAddState, MyQbzAddActions, MyQbzAddRow, MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, MyQbzEditState, MyQbzEditActions, MyQbzMixState, MyQbzMixActions, DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, CastState, CastDevice, CastActions, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, ScrobbleState, ScrobbleActions, DiscordState, OfflineState, LoginState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions, ImportLogEntry, PlaylistImportState, PlaylistImportActions, DacWizardState, DacWizardActions, DacCandidateRow, RemediationRow, DacConfigRow, InfoCreditRow, InfoCreditPair, AlbumCreditPerformer, AlbumCreditTrack, TrackInfoState, TrackInfoActions, AlbumInfoState, AlbumInfoActions, BookletState, BookletActions, SuggestionsState, SuggestionsActions, SuggestionCard, PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, VisualizerState, ImmersiveState, ImmersiveSearchActions, ImmersiveActions, MiniPlayerState, WindowControlActions, PurchasesState, PurchasesActions, PurchaseAlbumItem, PurchaseTrackItem, PurchaseAlbumGroup, PurchaseTrackGroup, PurchaseFormatItem, PurchaseDetailState, PurchaseDetailActions, PurchaseDetailTrack, KeybindingRow, KeybindingCategoryGroup, KeybindingsState, KeybindingsActions, KeyboardShortcutsState, LinkResolverState, LinkResolverActions, UiFocusState, UiScale, SleepTimerState, SleepTimerActions, LogRow, LogViewerState, DiagRow, DiagnosticsState, ReportIssueState, ReportIssueActions, AboutState, AboutActions, AboutContributorRow, AboutContributorGroup, WhatsNewState, WhatsNewActions, WhatsNewBlock, WhatsNewTocEntry } from "state.slint";
export { HomeState, HomeActions, RecentAlbumsState, DiscoverState, DiscoverActions, SectionDescriptor, ConfigRow, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, ExternalRecoState, ExternalRecoActions, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, LyricsState, LyricsLineItem, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, AwardState, AwardActions, AwardEntry, ArtistReleasesState, ArtistReleasesActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, DuplicateConfirmState, DuplicateConfirmActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, BlacklistState, BlacklistActions, BlacklistedArtistItem, MyQbzState, MyQbzActions, MixtapeCardItem, MyQbzAddState, MyQbzAddActions, MyQbzAddRow, MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, MyQbzEditState, MyQbzEditActions, MyQbzMixState, MyQbzMixActions, DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, CastState, CastDevice, CastActions, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, ScrobbleState, ScrobbleActions, DiscordState, OfflineState, LoginState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions, ImportLogEntry, PlaylistImportState, PlaylistImportActions, DacWizardState, DacWizardActions, DacCandidateRow, RemediationRow, DacConfigRow, InfoCreditRow, InfoCreditPair, AlbumCreditPerformer, AlbumCreditTrack, TrackInfoState, TrackInfoActions, AlbumInfoState, AlbumInfoActions, BookletState, BookletActions, SuggestionsState, SuggestionsActions, SuggestionCard, PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, VisualizerState, ImmersiveState, ImmersiveSearchActions, ImmersiveActions, MiniPlayerState, WindowControlActions, PurchasesState, PurchasesActions, PurchaseAlbumItem, PurchaseTrackItem, PurchaseAlbumGroup, PurchaseTrackGroup, PurchaseFormatItem, PurchaseDetailState, PurchaseDetailActions, PurchaseDetailTrack, KeybindingRow, KeybindingCategoryGroup, KeybindingsState, KeybindingsActions, KeyboardShortcutsState, LinkResolverState, LinkResolverActions, UiFocusState, UiScale, SleepTimerState, SleepTimerActions, LogRow, LogViewerState, DiagRow, DiagnosticsState, ReportIssueState, ReportIssueActions, AboutState, AboutActions, AboutContributorRow, AboutContributorGroup, WhatsNewState, WhatsNewActions, WhatsNewBlock, WhatsNewTocEntry } 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`.
+7
View File
@@ -386,12 +386,19 @@ export component HomeView inherits Rectangle {
title: @tr("Recently Played Albums"),
albums: HomeState.recent-albums,
};
// "View all" -> the full RecentAlbumsView page (local
// play-history, not a /discover endpoint — so it goes
// through its own action, not discover-view-all).
show-view-all: true;
album-clicked(id) => {
root.album-clicked(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
view-all-clicked => {
HomeActions.open-recent-albums();
}
}
if desc.id == "recentlyPlayedAlbums" && HomeState.recent-albums.length == 0 && !HomeState.loading: RecentPlaceholder {
title: @tr("Recently Played Albums");
@@ -0,0 +1,173 @@
// Recently Played Albums — the full-page listing opened from the Home
// rail's "View all". Renders the LOCAL play-history album store (capped at
// 24, see crate::recently) as a flat grid, so it carries none of the
// catalog-listing baggage: no pagination, no search, no error/retry branch
// (a local file read cannot fail into a retryable state — missing store =
// empty list). Template: AwardAlbumsView (fixed header + AlbumCollectionView
// inside a Flickable + the shared ListScrollbar), minus the search box and
// the infinite scroll.
//
// The grid reuses the Home rail's msgids (title + empty hint) — no new
// strings. Cards route through the shell's source-aware open-album funnel
// (Qobuz id vs local/Plex metadata group key), same as the rail.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { RecentAlbumsState, NavState, ShellState } from "../state.slint";
import { AlbumCollectionView } from "AlbumCollectionView.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { NavButtons } from "../shell/NavButtons.slint";
export component RecentAlbumsView inherits Rectangle {
callback open-album(string);
callback open-artist(string);
callback media-action(string, string, string);
property <length> album-w: 200px;
property <length> album-h: 266px;
property <length> gap: 24px;
background: Theme.surface-main;
VerticalLayout {
spacing: 0px;
// --- Fixed header (always visible) -------------------------------
// NavButtons + the page title on the LEFT, homologated with
// AwardAlbumsView: the back/forward pair keeps the same screen
// position as every page (center at y=25 = 11px top + 14px
// half-button). Only the list below scrolls.
Rectangle {
height: 56px;
background: Theme.surface-main;
HorizontalLayout {
x: 32px;
y: 25px - self.height / 2;
width: self.preferred-width;
height: self.preferred-height;
spacing: 16px;
VerticalLayout {
alignment: center;
NavButtons { }
}
VerticalLayout {
alignment: center;
// Reused Home-rail msgid (discover.recentlyPlayedAlbums).
Text {
text: @tr("Recently Played Albums");
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
vertical-alignment: center;
}
}
}
}
// --- Scrolling grid (only the listing scrolls) --------------------
flick := Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: page.preferred-height;
// Scroll-position restore on back/forward (see nav::scope_for).
property <string> sr-armed: NavState.restore-scope;
function sr-restore() {
if (NavState.restore-scope == "recent-albums" && self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
NavState.restore-scope = "";
}
}
init => { self.sr-restore(); }
changed sr-armed => { self.sr-restore(); }
changed viewport-height => { self.sr-restore(); }
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 8px;
padding-bottom: 100px;
spacing: 0px;
alignment: start;
// Loading spinner (the local read is near-instant; this only
// flashes on the navigation frame).
if RecentAlbumsState.loading: HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
Text {
text: @tr("Loading…");
color: Theme.text-muted;
font-size: 14px;
horizontal-alignment: center;
}
}
}
// Empty history — reuse the Home rail's placeholder hint.
if !RecentAlbumsState.loading
&& RecentAlbumsState.albums.length == 0: HorizontalLayout {
alignment: center;
Text {
text: @tr("Albums you play will appear here.");
color: Theme.text-muted;
font-size: 14px;
}
}
// --- Album grid (plain flat grid; no toolbar) -------------
if !RecentAlbumsState.loading: AlbumCollectionView {
albums: RecentAlbumsState.albums;
view-mode: "grid";
is-grouped: false;
show-source: false;
card-width: root.album-w;
card-height: root.album-h;
card-gap: root.gap;
// Viewport windowing (element-count only; artwork is
// dispatched for the whole 24-card set up front, so a
// card mounting later picks its cover off the model).
// The offset is the page's padding-top (8px) — the
// spinner/empty branches are mutually exclusive with
// the grid, so nothing else sits above it.
windowed: true;
visible-top: -flick.viewport-y;
visible-height: flick.height;
content-offset: 8px;
open-album(id) => { root.open-album(id); }
open-artist(id) => { root.open-artist(id); }
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
}
}
}
// Shared scrollbar in the right gutter (homologated cross-view). Sits
// below the fixed 56px header so it tracks only the scrolling list.
ListScrollbar {
x: parent.width - self.width - 4px;
y: 56px;
width: 14px;
height: parent.height - 56px;
viewport-height: flick.viewport-height;
visible-height: flick.height;
viewport-y <=> flick.viewport-y;
auto-hide: true;
hover-active: ShellState.pointer-in-window
&& ShellState.pointer-x >= flick.absolute-position.x
&& ShellState.pointer-x <= flick.absolute-position.x + flick.width
&& ShellState.pointer-y >= flick.absolute-position.y
&& ShellState.pointer-y <= flick.absolute-position.y + flick.height;
}
}
+15
View File
@@ -54,6 +54,7 @@ import { QueueSidebar } from "QueueSidebar.slint";
import { LyricsSidebar } from "LyricsSidebar.slint";
import { HomeView } from "../discover/HomeView.slint";
import { DiscoverBrowseView } from "../discover/DiscoverBrowseView.slint";
import { RecentAlbumsView } from "../discover/RecentAlbumsView.slint";
import { AlbumPageView } from "../album/AlbumPageView.slint";
import { LocalAlbumView } from "../album/LocalAlbumView.slint";
import { ArtistPageView } from "../artist/ArtistPageView.slint";
@@ -311,6 +312,20 @@ export component AppShell inherits Rectangle {
root.media-action(kind, id, action);
}
}
// Recently Played Albums full page — LOCAL play-history data,
// so it mounts offline too (like local-album below); opening a
// Qobuz card from it offline hits the album view's own gate.
if NavState.view == ContentView.recent-albums: RecentAlbumsView {
open-album(id) => {
root.open-album(id);
}
open-artist(id) => {
root.open-artist(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
if NavState.view == ContentView.album && !OfflineState.offline: AlbumPageView {
open-artist(id) => {
root.open-artist(id);
+13 -1
View File
@@ -201,6 +201,18 @@ export global HomeActions {
callback toggle-playlist-tag(string);
// Clear all selected category tags (back to showing every playlist).
callback clear-playlist-tags();
// "View all" on the Recently Played Albums rail -> the full-page listing
// (RecentAlbumsView), fed from the local play-history store.
callback open-recent-albums();
}
// RecentAlbumsView — the full "Recently Played Albums" page opened from the
// Home rail's "View all". Fed from the LOCAL play-history store (capped at
// 24, see crate::recently), so it needs no paging/search state; the loader
// repopulates `albums` on every navigation.
export global RecentAlbumsState {
in property <[AlbumCardItem]> albums: [];
in property <bool> loading: false;
}
// ---------------------------------------------------------------------------
@@ -967,7 +979,7 @@ export global DragActions {
}
// Which content view the shell shows.
export enum ContentView { home, album, artist, artist-releases, settings, search, musician, label, label-releases, award, award-albums, location, favorites, mix, playlist, discover-browse, playlist-manager, offline-manager, blacklist-manager, local-library, local-album, mixtapes, collections, mixtape-detail, discography-builder, purchases, purchase-album }
export enum ContentView { home, album, artist, artist-releases, settings, search, musician, label, label-releases, award, award-albums, location, favorites, mix, playlist, discover-browse, recent-albums, playlist-manager, offline-manager, blacklist-manager, local-library, local-album, mixtapes, collections, mixtape-detail, discography-builder, purchases, purchase-album }
// In-app toast (transient notification). Mirrors Tauri's toastStore:
// a single toast at a time (a new one replaces the current), auto-hidden
+13
View File
@@ -65,6 +65,11 @@ pub enum ArtworkTarget {
Recent { idx: usize },
/// A card in `HomeState.recent-albums[idx]`.
RecentAlbum { idx: usize },
/// A card in `RecentAlbumsState.albums[idx]` — the full "Recently Played
/// Albums" page (the Home rail's "View all"). Own target because the page
/// model has its own lifecycle, separate from the rail's (same split as
/// HomeFavoriteAlbum vs ForYouFavoriteAlbum).
RecentAlbumsPage { idx: usize },
/// A card in `HomeState.favorite-albums.albums[idx]` — the Home tab's
/// "Library Albums" rail (#566). Separate from `ForYouFavoriteAlbum`:
/// the two rails share the data pipeline but not the model lifecycle.
@@ -928,6 +933,14 @@ fn apply_artwork(
item.artwork = image;
albums.set_row_data(idx, item);
}
ArtworkTarget::RecentAlbumsPage { idx } => {
let albums = window.global::<crate::RecentAlbumsState>().get_albums();
let Some(mut item) = albums.row_data(idx) else {
return;
};
item.artwork = image;
albums.set_row_data(idx, item);
}
ArtworkTarget::HomeFavoriteAlbum { idx } => {
let section = home.get_favorite_albums();
let Some(mut item) = section.albums.row_data(idx) else {
+39 -31
View File
@@ -291,37 +291,7 @@ where
artwork_url: track.artwork_url,
})
.collect();
let recent_albums = crate::recently::load_albums()
.into_iter()
// Drop blocked albums (own id) at the SOURCE so the model + artwork jobs
// stay index-aligned. Recently Played is Qobuz album ids.
.filter(|album| !crate::artist_blacklist::is_album_blacklisted(&album.id))
.map(|album| CardData {
id: album.id,
title: album.title,
artist: album.artist,
artist_id: String::new(),
genre: album.genre,
// Localize the stored ISO release date to "MMM D, YYYY" the
// same way the discover cards do (empty stays empty).
year: if album.release_date.is_empty() {
String::new()
} else {
crate::dates::release_label(Some(&album.release_date))
},
quality_tier: album.quality_tier,
quality_label: album.quality_label,
ribbon: String::new(),
ribbon_kind: String::new(),
artwork_url: album.artwork_url,
// Carry the origin so the card resolves source-aware artwork
// (PlexThumb / local file) and the play/open route correctly.
source: album.source,
// Recently-played cards render in the grid only — list-row
// extras stay default.
..CardData::default()
})
.collect();
let recent_albums = recent_album_cards();
// Qobuz Playlists row — both the Home and Editor's Picks tabs draw from
// the SAME `containers.playlists` (one fetch). Capped at 40 (raised from
@@ -363,6 +333,44 @@ where
})
}
/// The recently-played album history mapped to card data, newest first.
/// Shared by the Home "Recently Played Albums" rail (via [`load_home`]) and
/// the full "View all" page (`main::navigate_recent_albums`), so both apply
/// the same blacklist filter and date localization. Local file read — cheap.
pub fn recent_album_cards() -> Vec<CardData> {
crate::recently::load_albums()
.into_iter()
// Drop blocked albums (own id) at the SOURCE so the model + artwork jobs
// stay index-aligned. Recently Played is Qobuz album ids.
.filter(|album| !crate::artist_blacklist::is_album_blacklisted(&album.id))
.map(|album| CardData {
id: album.id,
title: album.title,
artist: album.artist,
artist_id: String::new(),
genre: album.genre,
// Localize the stored ISO release date to "MMM D, YYYY" the
// same way the discover cards do (empty stays empty).
year: if album.release_date.is_empty() {
String::new()
} else {
crate::dates::release_label(Some(&album.release_date))
},
quality_tier: album.quality_tier,
quality_label: album.quality_label,
ribbon: String::new(),
ribbon_kind: String::new(),
artwork_url: album.artwork_url,
// Carry the origin so the card resolves source-aware artwork
// (PlexThumb / local file) and the play/open route correctly.
source: album.source,
// Recently-played cards render in the grid only — list-row
// extras stay default.
..CardData::default()
})
.collect()
}
fn push_section(
out: &mut Vec<SectionData>,
id: DiscoverySectionId,
+87
View File
@@ -2338,6 +2338,7 @@ fn scope_for(entry: &nav::NavEntry) -> String {
nav::NavEntry::Favorites { tab } => format!("fav:{tab}"),
nav::NavEntry::LocalLibrary { tab } => format!("ll:{tab}"),
nav::NavEntry::DiscoverBrowse { .. } => "discover-browse".into(),
nav::NavEntry::RecentAlbums => "recent-albums".into(),
nav::NavEntry::Mix { .. } => "mix".into(),
nav::NavEntry::Playlist(_) => "playlist".into(),
nav::NavEntry::PlaylistManager => "playlist-manager".into(),
@@ -2519,6 +2520,9 @@ fn apply_entry(
current_genre_filter(),
);
}
nav::NavEntry::RecentAlbums => {
navigate_recent_albums(weak.clone(), handle, image_cache.clone());
}
nav::NavEntry::Mix { kind } => {
navigate_mix(runtime.clone(), weak.clone(), handle, image_cache.clone(), kind);
}
@@ -2870,6 +2874,71 @@ fn navigate_award_albums(
});
}
/// Open the full "Recently Played Albums" page (the Home rail's "View all").
/// LOCAL data: the play-history album store (crate::recently) mapped through
/// the same card funnel as the rail (`home::recent_album_cards` — blacklist
/// filter + date localization; `home::card_to_item` — is-favorite seeding),
/// so no runtime and no error branch (missing store = empty list). Artwork
/// splits Qobuz covers (plain loader) from Plex/local covers (source-aware
/// funnel), mirroring the rail's dispatch in `reload_home`.
fn navigate_recent_albums(
weak: slint::Weak<AppWindow>,
handle: &tokio::runtime::Handle,
image_cache: artwork::ImageCache,
) {
handle.spawn(async move {
let _ = weak.upgrade_in_event_loop(|w| {
let s = w.global::<RecentAlbumsState>();
s.set_loading(true);
s.set_albums(slint::ModelRc::new(slint::VecModel::from(
Vec::<AlbumCardItem>::new(),
)));
w.global::<NavState>().set_view(ContentView::RecentAlbums);
});
// Local file read + blacklist filter — cheap, but keep it off the UI
// thread like the sibling loaders.
let cards = home::recent_album_cards();
let mut jobs: Vec<artwork::ArtworkJob> = Vec::new();
let mut plex_jobs: Vec<artwork::ArtworkJob> = Vec::new();
for (idx, card) in cards.iter().enumerate() {
if card.artwork_url.is_empty() {
continue;
}
let job = artwork::ArtworkJob {
target: artwork::ArtworkTarget::RecentAlbumsPage { idx },
url: card.artwork_url.clone(),
};
if card.source == "plex" || card.source == "local" {
plex_jobs.push(job);
} else {
jobs.push(job);
}
}
let weak_for_plex = weak.clone();
let image_cache_plex = image_cache.clone();
let _ = weak.clone().upgrade_in_event_loop(move |w| {
// card_to_item seeds is-favorite from the login cache — UI thread,
// same as apply_home.
let items: Vec<AlbumCardItem> =
cards.into_iter().map(home::card_to_item).collect();
let s = w.global::<RecentAlbumsState>();
s.set_albums(slint::ModelRc::new(slint::VecModel::from(items)));
s.set_loading(false);
});
artwork::spawn_loads(jobs, weak, image_cache);
if !plex_jobs.is_empty() {
let plex = crate::plex_settings::get();
artwork::spawn_local_or_plex_loads(
plex_jobs,
plex.base_url,
plex.token,
weak_for_plex,
image_cache_plex,
);
}
});
}
/// Open the My-Purchases surface and lazy-load the active tab. Mirrors
/// `navigate_favorites`: the view mounts immediately (spinner), then the active
/// tab's data + the metadata (dlIds + per-type totals) load and project onto
@@ -14914,6 +14983,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
// Home "Recently Played Albums" rail "View all" -> the full page listing
// the local play-history albums (mirrors AwardActions.open-albums: record
// history, navigate, refresh the nav flags).
{
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
let image_cache = image_cache.clone();
window
.global::<HomeActions>()
.on_open_recent_albums(move || {
nav::record(nav::NavEntry::RecentAlbums);
navigate_recent_albums(weak.clone(), &handle, image_cache.clone());
if let Some(w) = weak.upgrade() {
update_nav_flags(&w);
}
});
}
// Qobuz Playlists category filter (multi-select, client-side). Toggling /
// clearing a tag re-filters the cached playlists row and re-fires the
// artwork for the new (filtered) positions — no re-fetch.
+5
View File
@@ -50,6 +50,11 @@ pub enum NavEntry {
endpoint: String,
title: String,
},
/// RecentAlbumsView — the full "Recently Played Albums" listing reached
/// from the Home rail's "View all". Renders the LOCAL play-history album
/// store (crate::recently), reloaded on every navigation, so the entry
/// carries no payload.
RecentAlbums,
/// A Qobuz mix detail page ("daily" | "weekly" | "fav" | "top").
Mix {
kind: String,