feat(slint): playlist snapshot (names+membership) and offline favorites rail
Qobuz playlist snapshot, captured opportunistically with zero new API traffic: every online sidebar/manager list load upserts id+name+owner+ track_count for the user's playlists; opening a playlist detail online full-replaces its membership track ids (refusing playlists never listed, so viewed public playlists don't land). Offline consumers: synthesized sidebar/manager entries resolve the persisted name (killing the cold-start 'Playlist (N local)' fallback when a snapshot exists), and the mixed- playlist offline detail now renders the snapshot tracks that are playable (snapshot intersect cached intersect grace) from the offline-cache index - snapshot block first in snapshot order, sidecar local/plex block after. D11 availability widens: a playlist with zero sidecar rows but cached snapshot tracks now shows offline. 4 repo tests. Offline favorites rail: the Favorites placeholder gains an 'Available offline (N)' block - disk-cached favorite ids intersected with the offline-cache READY index and LocalLibrary qobuz_download copies, compact rows with the offline cover chain, click plays the rail as a queue through the cache tier. Shared OfflinePlaceholder gains an @children slot; only the Favorites mount injects the rail. Single commit: the two features interleave in main.rs/state wiring and ship together to keep every commit compiling. cc
This commit is contained in:
17 files changed
+1154
-95
No files matched your search
@@ -18,7 +18,7 @@ import { AppShell } from "shell/AppShell.slint";
|
||||
import { Toast } from "primitives/Toast.slint";
|
||||
|
||||
// Re-export the state globals so the Rust layer can populate them.
|
||||
export { HomeState, HomeActions, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, 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, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, OfflineState, OfflineModeActions } from "state.slint";
|
||||
export { HomeState, HomeActions, DiscoverBrowseState, DiscoverBrowseActions, ForYouState, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, PlaylistPickerState, PlaylistPickerActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, 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, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, OfflineState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions } 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`.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// B9 — "playable favorites" rail, shown INSIDE the offline placeholder on
|
||||
// the Favorites view only (the AppShell mounts it as the placeholder's
|
||||
// children — ADR-010: both `if`s live at the AppShell mount site). Lists
|
||||
// the favorite tracks that are still playable offline (offline-cache READY
|
||||
// copies + library qobuz_download rows, intersected with the disk-seeded
|
||||
// favorite set on the Rust side) as compact SlimCard rows; clicking a row
|
||||
// plays the whole rail from there through the offline-capable queue path.
|
||||
//
|
||||
// Collapses to nothing while the list is empty, so the placeholder then
|
||||
// reads exactly like the generic one.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { OfflineFavoritesState, OfflineFavoritesActions } from "../state.slint";
|
||||
import { SlimCard } from "../discover/SlimCard.slint";
|
||||
import { ListView } from "std-widgets.slint";
|
||||
|
||||
export component FavoritesOfflineRail inherits VerticalLayout {
|
||||
init => {
|
||||
OfflineFavoritesActions.load();
|
||||
}
|
||||
|
||||
property <int> count: OfflineFavoritesState.tracks.length;
|
||||
|
||||
if root.count > 0: Rectangle { height: 28px; }
|
||||
if root.count > 0: HorizontalLayout {
|
||||
alignment: center;
|
||||
VerticalLayout {
|
||||
spacing: 8px;
|
||||
Text {
|
||||
text: @tr("Available offline") + " (\{root.count})";
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
}
|
||||
ListView {
|
||||
width: 480px;
|
||||
// Up to five 60px rows visible; longer lists scroll inside.
|
||||
height: Math.min(root.count, 5) * 60px;
|
||||
for item in OfflineFavoritesState.tracks: SlimCard {
|
||||
item: item;
|
||||
clicked(id) => {
|
||||
OfflineFavoritesActions.play(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,5 +62,10 @@ export component OfflinePlaceholder inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional extra content under the placeholder copy. Empty on the
|
||||
// generic mounts; the Favorites mount injects the B9 "playable
|
||||
// favorites" rail here (see AppShell + FavoritesOfflineRail).
|
||||
@children
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import { LabelReleasesView } from "../label/LabelReleasesView.slint";
|
||||
import { LabelPageView } from "../label/LabelPageView.slint";
|
||||
import { ArtistsByLocationView } from "../location/ArtistsByLocationView.slint";
|
||||
import { FavoritesView } from "../favorites/FavoritesView.slint";
|
||||
import { FavoritesOfflineRail } from "../favorites/FavoritesOfflineRail.slint";
|
||||
import { MixView } from "../mix/MixView.slint";
|
||||
import { PlaylistView } from "../playlist/PlaylistView.slint";
|
||||
import { PlaylistManagerView } from "../playlist/PlaylistManagerView.slint";
|
||||
@@ -183,8 +184,13 @@ export component AppShell inherits Rectangle {
|
||||
|
||||
// Offline: every Qobuz-only view below mounts only while
|
||||
// online; the shared placeholder takes the pane otherwise
|
||||
// (ADR-010 — the `if` lives here at the mount site).
|
||||
if root.qobuz-view-blocked: OfflinePlaceholder { }
|
||||
// (ADR-010 — the `if` lives here at the mount site). The
|
||||
// Favorites mount alone injects the B9 "playable favorites"
|
||||
// rail into the placeholder's children slot.
|
||||
if root.qobuz-view-blocked && NavState.view != ContentView.favorites: OfflinePlaceholder { }
|
||||
if root.qobuz-view-blocked && NavState.view == ContentView.favorites: OfflinePlaceholder {
|
||||
FavoritesOfflineRail { }
|
||||
}
|
||||
|
||||
if NavState.view == ContentView.home && !OfflineState.offline: HomeView {
|
||||
album-clicked(id) => {
|
||||
|
||||
@@ -3192,3 +3192,23 @@ export global OfflineModeActions {
|
||||
// persistence + #279 snapshot path as the Settings toggle.
|
||||
callback set-offline(bool);
|
||||
}
|
||||
|
||||
// B9 — offline Favorites "playable favorites" rail. While OFFLINE the
|
||||
// Favorites view is replaced by the OfflinePlaceholder; this rail (mounted
|
||||
// as the placeholder's children on the Favorites mount only) lists the
|
||||
// favorite tracks that are still playable — offline-cache READY copies and
|
||||
// library qobuz_download rows — so the view isn't a dead end.
|
||||
export global OfflineFavoritesState {
|
||||
// The playable favorite tracks, display order = play order (the Rust
|
||||
// side keeps a matching queue; a row click plays the rail from there).
|
||||
in property <[SlimItem]> tracks: [];
|
||||
}
|
||||
|
||||
export global OfflineFavoritesActions {
|
||||
// (Re)build the rail from the local stores — fired by the rail's init
|
||||
// on every mount of the Favorites offline placeholder.
|
||||
callback load();
|
||||
// Play the rail starting at the clicked track id (offline-capable
|
||||
// queue path; the cached copies play through the offline tier).
|
||||
callback play(string);
|
||||
}
|
||||
Reference in new issue
Block a user