feat(slint): favorites tracks Play all + Shuffle header actions
Retain the loaded favorite tracks as a play-ready Vec<Track> and add Play all + Shuffle CircleAction buttons by the title (Tracks tab only), wired to playback::play_tracks. Mirrors the Mix/Playlist transport (those expose exactly these two; no batch play-next path exists yet). cc
This commit is contained in:
4 files changed
+110
-8
No files matched your search
@@ -8,7 +8,7 @@
|
||||
//! relevant branch into typed qbz-models items and maps them to the
|
||||
//! Slint row/card structs.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
use qbz_app::shell::AppRuntime;
|
||||
use qbz_core::FrontendAdapter;
|
||||
@@ -32,6 +32,10 @@ pub const PAGE_SIZE: u32 = 500;
|
||||
/// FAVORITES_PAGE_SIZE * FAVORITES_MAX_PAGES ceiling).
|
||||
const MAX_ITEMS: usize = 10_000;
|
||||
|
||||
/// The loaded favorite tracks as a play-ready queue source (Play all /
|
||||
/// Shuffle). Set on the UI thread by `apply_favorites`.
|
||||
static FAV_CURRENT: LazyLock<Mutex<Vec<Track>>> = LazyLock::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
/// Which favorites tab to load.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FavTab {
|
||||
@@ -85,7 +89,7 @@ struct FavLabel {
|
||||
}
|
||||
|
||||
pub enum FavData {
|
||||
Tracks { items: Vec<TrackCard>, total: usize },
|
||||
Tracks { items: Vec<TrackCard>, play: Vec<Track>, total: usize },
|
||||
Albums { items: Vec<AlbumCard>, total: usize },
|
||||
Artists { items: Vec<ArtistCard>, total: usize },
|
||||
Playlists { items: Vec<PlaylistCard>, total: usize },
|
||||
@@ -191,8 +195,10 @@ where
|
||||
Ok(match tab {
|
||||
FavTab::Tracks => {
|
||||
let tracks: Vec<Track> = serde_json::from_value(items).unwrap_or_default();
|
||||
let play = tracks.clone();
|
||||
FavData::Tracks {
|
||||
items: tracks.into_iter().map(map_track).collect(),
|
||||
play,
|
||||
total,
|
||||
}
|
||||
}
|
||||
@@ -308,7 +314,10 @@ fn map_label(label: FavLabel) -> LabelCard {
|
||||
pub fn apply_favorites(window: &AppWindow, data: FavData) {
|
||||
let state = window.global::<FavoritesState>();
|
||||
match data {
|
||||
FavData::Tracks { items, total } => {
|
||||
FavData::Tracks { items, play, total } => {
|
||||
if let Ok(mut current) = FAV_CURRENT.lock() {
|
||||
*current = play;
|
||||
}
|
||||
let rows: Vec<TrackItem> = items
|
||||
.into_iter()
|
||||
.map(|t| TrackItem {
|
||||
@@ -414,6 +423,30 @@ pub fn derive_tracks(window: &AppWindow) {
|
||||
state.set_tracks_visible(ModelRc::new(VecModel::from(filtered)));
|
||||
}
|
||||
|
||||
/// The loaded favorite tracks as a play-ready queue (Play all).
|
||||
pub fn play_tracks() -> Vec<Track> {
|
||||
FAV_CURRENT.lock().map(|c| c.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The favorite tracks in a fresh random order (Shuffle). Mirrors
|
||||
/// playlist::shuffled_tracks (time-seeded xorshift Fisher-Yates).
|
||||
pub fn shuffled_tracks() -> Vec<Track> {
|
||||
let mut tracks = play_tracks();
|
||||
let mut seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(1)
|
||||
| 1;
|
||||
for i in (1..tracks.len()).rev() {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
let j = (seed % (i as u64 + 1)) as usize;
|
||||
tracks.swap(i, j);
|
||||
}
|
||||
tracks
|
||||
}
|
||||
|
||||
pub fn reset_loading(window: &AppWindow) {
|
||||
window.global::<FavoritesState>().set_loading(true);
|
||||
}
|
||||
|
||||
@@ -4272,6 +4272,40 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
// Play all favorite tracks as a fresh queue.
|
||||
let runtime = app_runtime.clone();
|
||||
let weak = window.as_weak();
|
||||
let handle = tokio_rt.handle().clone();
|
||||
window
|
||||
.global::<FavoritesActions>()
|
||||
.on_play_all_tracks(move || {
|
||||
playback::play_tracks(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
handle.clone(),
|
||||
favorites::play_tracks(),
|
||||
0,
|
||||
);
|
||||
});
|
||||
}
|
||||
{
|
||||
// Shuffle-play the favorite tracks.
|
||||
let runtime = app_runtime.clone();
|
||||
let weak = window.as_weak();
|
||||
let handle = tokio_rt.handle().clone();
|
||||
window
|
||||
.global::<FavoritesActions>()
|
||||
.on_shuffle_tracks(move || {
|
||||
playback::play_tracks(
|
||||
runtime.clone(),
|
||||
weak.clone(),
|
||||
handle.clone(),
|
||||
favorites::shuffled_tracks(),
|
||||
0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Artwork right-click menu wiring — Open in browser / Save as /
|
||||
// Add custom / Remove custom. Mirrors the v2_library_* + native
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
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";
|
||||
@@ -275,11 +276,43 @@ export component FavoritesView inherits Rectangle {
|
||||
|
||||
// Page title — same size as the album name in AlbumView
|
||||
// (Typography.section), not the larger Typography.title.
|
||||
Text {
|
||||
text: @tr("Favorites");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.section;
|
||||
font-weight: Typography.bold;
|
||||
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; }
|
||||
|
||||
@@ -1477,6 +1477,8 @@ export global FavoritesActions {
|
||||
callback play-track(string /* track id */);
|
||||
callback track-action(string /* track id */, string /* action */);
|
||||
callback search-tracks(string /* query */);
|
||||
callback play-all-tracks();
|
||||
callback shuffle-tracks();
|
||||
}
|
||||
|
||||
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
|
||||
|
||||
Reference in new issue
Block a user