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:
9 files changed
+353
-33
No files matched your search
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user