feat(slint): Qobuz mix detail views (DailyQ / WeeklyQ / FavQ / TopQ)
The For You Qobuz Mixes tiles now open functional mix detail pages.
- New MixView: header (mix name + description + Play all) over the
mix's track list (reusing SearchTrackRow). Clicking a track plays
the whole mix from there; Play all starts at the top.
- crate::mix resolves each mix to a track list from available
backend: DailyQ/WeeklyQ -> /dynamic/suggest seeded from the local
play-history track ids; FavQ -> favorite tracks shuffled (xorshift,
no rng dep); TopQ -> tracks aggregated from the user's playlists.
The loaded list is cached so play-all / play-from-track build the
queue without re-fetching.
- Backend: ported /dynamic/suggest into qbz-qobuz
(QobuzClient::get_dynamic_suggest — POST, items parsed leniently
into Vec<Track>) + QbzCore::get_dynamic_suggest.
- ContentView.mix + NavEntry::Mix { kind } so each mix is a history
entry; the tile click records + navigates. New MixTrack artwork
target. playback::play_tracks generalizes the radio queue builder
with a start index (radio reuses it at 0).
Tauri's exact generation (listened-track analysis payloads, playlist
play-stats ranking) is approximated; same surfaces + playback, sourced
from the backend the MVP has.
cc
This commit is contained in:
11 files changed
+502
-12
No files matched your search
@@ -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, HomeActions, ForYouState, GenreFilterState, GenreFilterActions, 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";
|
||||
export { HomeState, HomeActions, ForYouState, MixState, GenreFilterState, GenreFilterActions, 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`.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Qobuz mix detail view (DailyQ / WeeklyQ / FavQ / TopQ). Header with
|
||||
// the mix name + description + a Play all button, over the mix's
|
||||
// track list (reusing the shared SearchTrackRow).
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { MixState } from "../state.slint";
|
||||
import { SearchTrackRow } from "../search/SearchTrackRow.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
|
||||
export component MixView inherits Rectangle {
|
||||
callback play-track(string);
|
||||
callback play-all();
|
||||
|
||||
background: Theme.surface-main;
|
||||
|
||||
Flickable {
|
||||
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;
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: start;
|
||||
NavButtons { }
|
||||
}
|
||||
|
||||
Rectangle { height: 22px; }
|
||||
|
||||
// --- Header ------------------------------------------------
|
||||
HorizontalLayout {
|
||||
spacing: 20px;
|
||||
alignment: start;
|
||||
Rectangle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
background: @linear-gradient(135deg, #1e3a8a 0%, #6366f1 50%, #c084fc 100%);
|
||||
Text {
|
||||
text: MixState.title;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
}
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 8px;
|
||||
Text {
|
||||
text: MixState.title;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.title;
|
||||
font-weight: Typography.bold;
|
||||
}
|
||||
Text {
|
||||
text: MixState.subtitle;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
if MixState.tracks.length > 0: Rectangle {
|
||||
width: play-row.preferred-width + 28px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background: play-ta.has-hover ? #ffffffd6 : Theme.accent;
|
||||
play-row := HorizontalLayout {
|
||||
x: (parent.width - self.preferred-width) / 2;
|
||||
spacing: 8px;
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/play-fill.svg");
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
tint: #ffffff;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: @tr("Play all");
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
play-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.play-all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 28px; }
|
||||
|
||||
if MixState.loading: HorizontalLayout {
|
||||
alignment: center;
|
||||
LoadingSpinner { size: 36px; }
|
||||
}
|
||||
if !MixState.loading && MixState.tracks.length == 0: Text {
|
||||
text: @tr("No tracks for this mix.");
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
}
|
||||
|
||||
for track[i] in MixState.tracks: SearchTrackRow {
|
||||
track: track;
|
||||
index: i;
|
||||
show-artwork: true;
|
||||
media-action(kind, id, action) => {
|
||||
root.play-track(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { MusicianPageView } from "../musician/MusicianPageView.slint";
|
||||
import { LabelReleasesView } from "../label/LabelReleasesView.slint";
|
||||
import { ArtistsByLocationView } from "../location/ArtistsByLocationView.slint";
|
||||
import { FavoritesView } from "../favorites/FavoritesView.slint";
|
||||
import { MixView } from "../mix/MixView.slint";
|
||||
import { SettingsView } from "../settings/SettingsView.slint";
|
||||
import { SearchResultsView } from "../search/SearchResultsView.slint";
|
||||
|
||||
@@ -128,6 +129,14 @@ export component AppShell inherits Rectangle {
|
||||
root.open-artist(id);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.mix: MixView {
|
||||
play-track(id) => {
|
||||
root.media-action("mix-track", id, "play");
|
||||
}
|
||||
play-all => {
|
||||
root.media-action("mix", "", "play-all");
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.settings: SettingsView {
|
||||
settings-bool(k, v) => {
|
||||
root.settings-bool(k, v);
|
||||
|
||||
@@ -185,7 +185,7 @@ export global AlbumActions {
|
||||
}
|
||||
|
||||
// Which content view the shell shows.
|
||||
export enum ContentView { home, album, artist, settings, search, musician, label, location, favorites }
|
||||
export enum ContentView { home, album, artist, settings, search, musician, label, location, favorites, mix }
|
||||
|
||||
// Settings screen state. Audio and Playback preferences are loaded from
|
||||
// the persistence stores by the Rust settings controller and pushed here;
|
||||
@@ -791,6 +791,16 @@ export global FavoritesActions {
|
||||
callback track-action(string /* track id */, string /* action */);
|
||||
}
|
||||
|
||||
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
|
||||
// from the For You Qobuz Mixes tiles. Populated by crate::mix.
|
||||
export global MixState {
|
||||
in property <string> kind; // daily | weekly | fav | top
|
||||
in property <string> title;
|
||||
in property <string> subtitle;
|
||||
in property <[SearchTrackItem]> tracks: [];
|
||||
in property <bool> loading: false;
|
||||
}
|
||||
|
||||
// Search results screen state. The Rust search controller populates the
|
||||
// four category lists, their totals, and the most-popular hero entry.
|
||||
export global SearchState {
|
||||
|
||||
Reference in new issue
Block a user