From 75429faffef8cc16ece35fc66b6fd4492891fd3b Mon Sep 17 00:00:00 2001 From: joren Date: Tue, 24 Mar 2026 17:20:00 +0100 Subject: [PATCH] feat: playlist art fix + Play/Shuffle buttons in context header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix playlist art: API returns images/images150/images300 arrays, not image_rectangle; update Rust model and Qt header to use images300 → images150 → images with fallback - Add Play (▶) and Shuffle (⇄) buttons to the album/playlist header; Play starts from track 1, Shuffle enables shuffle mode and plays from a randomised position Co-Authored-By: Claude Sonnet 4.6 --- rust/src/api/models.rs | 7 +++- src/list/tracks.cpp | 11 ++++++ src/list/tracks.hpp | 4 +++ src/view/maincontent.cpp | 6 ++++ src/view/trackcontextheader.hpp | 62 ++++++++++++++++++++++++++------- 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/rust/src/api/models.rs b/rust/src/api/models.rs index 2d8e2c4..33e0bd5 100644 --- a/rust/src/api/models.rs +++ b/rust/src/api/models.rs @@ -183,7 +183,12 @@ pub struct PlaylistDto { pub duration: Option, pub description: Option, pub owner: Option, - pub image_rectangle: Option>, + /// 4-cover mosaic at 300 px (preferred) + pub images300: Option>, + /// 4-cover mosaic at 150 px (fallback) + pub images150: Option>, + /// 4-cover mosaic at 50 px (last resort) + pub images: Option>, pub tracks: Option, } diff --git a/src/list/tracks.cpp b/src/list/tracks.cpp index 27e9926..0f49387 100644 --- a/src/list/tracks.cpp +++ b/src/list/tracks.cpp @@ -84,6 +84,17 @@ void Tracks::setPlayingTrackId(qint64 id) m_model->setPlayingId(id); } +void Tracks::playAll(bool shuffle) +{ + const QJsonArray tracks = m_model->currentTracksJson(); + if (tracks.isEmpty()) return; + m_queue->setShuffle(shuffle); + m_queue->setContext(tracks, 0); + const qint64 firstId = static_cast(m_queue->current()["id"].toDouble()); + if (firstId > 0) + emit playTrackRequested(firstId); +} + void Tracks::onDoubleClicked(const QModelIndex &index) { diff --git a/src/list/tracks.hpp b/src/list/tracks.hpp index 5df2f9a..86f5095 100644 --- a/src/list/tracks.hpp +++ b/src/list/tracks.hpp @@ -28,6 +28,10 @@ namespace List /// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted. void setPlayingTrackId(qint64 id); + /// Start playing all tracks in the current view from the beginning. + /// If shuffle is true, enables shuffle mode before starting. + void playAll(bool shuffle = false); + /// Set which playlist is currently displayed (0 = none). /// isOwned controls whether "Remove from this playlist" is shown. void setPlaylistContext(qint64 playlistId, bool isOwned = false); diff --git a/src/view/maincontent.cpp b/src/view/maincontent.cpp index d26170f..11131cf 100644 --- a/src/view/maincontent.cpp +++ b/src/view/maincontent.cpp @@ -1,6 +1,7 @@ #include "maincontent.hpp" #include +#include MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) : QWidget(parent) @@ -31,6 +32,11 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren tracksLayout->addWidget(m_header); tracksLayout->addWidget(m_tracks, 1); + QObject::connect(m_header->playButton(), &QPushButton::clicked, + [this] { m_tracks->playAll(false); }); + QObject::connect(m_header->shuffleButton(), &QPushButton::clicked, + [this] { m_tracks->playAll(true); }); + m_albumList = new AlbumListView(this); m_artistList = new ArtistListView(this); m_artistView = new ArtistView(this); diff --git a/src/view/trackcontextheader.hpp b/src/view/trackcontextheader.hpp index 440854d..54ae319 100644 --- a/src/view/trackcontextheader.hpp +++ b/src/view/trackcontextheader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -15,14 +16,14 @@ #include /// Header strip shown above the track list when an album or playlist is open. -/// Displays album art, title, subtitle, and metadata. +/// Displays album art, title, subtitle, metadata, and Play/Shuffle buttons. class TrackContextHeader : public QWidget { public: explicit TrackContextHeader(QWidget *parent = nullptr) : QWidget(parent) { - setFixedHeight(140); + setFixedHeight(148); auto *hlay = new QHBoxLayout(this); hlay->setContentsMargins(12, 8, 12, 8); @@ -61,7 +62,35 @@ public: m_meta->setPalette(mp); vlay->addWidget(m_meta); - vlay->addStretch(); + // Play / Shuffle buttons + auto *btnRow = new QHBoxLayout; + btnRow->setSpacing(8); + btnRow->setContentsMargins(0, 4, 0, 0); + + static const QString btnBase = QStringLiteral( + "QPushButton {" + " padding: 5px 16px;" + " border-radius: 4px;" + " font-weight: bold;" + "}" + "QPushButton:hover { opacity: 0.85; }" + ); + + m_playBtn = new QPushButton(tr("▶ Play"), info); + m_playBtn->setStyleSheet(btnBase + + QStringLiteral("QPushButton { background: #FFB232; color: #000; }" + "QPushButton:pressed { background: #e09e28; }")); + btnRow->addWidget(m_playBtn); + + m_shuffleBtn = new QPushButton(tr("⇄ Shuffle"), info); + m_shuffleBtn->setStyleSheet(btnBase + + QStringLiteral("QPushButton { background: #2a2a2a; color: #FFB232; border: 1px solid #FFB232; }" + "QPushButton:pressed { background: #333; }")); + btnRow->addWidget(m_shuffleBtn); + + btnRow->addStretch(); + vlay->addLayout(btnRow); + hlay->addWidget(info, 1); m_nam = new QNetworkAccessManager(this); @@ -76,6 +105,9 @@ public: }); } + QPushButton *playButton() { return m_playBtn; } + QPushButton *shuffleButton() { return m_shuffleBtn; } + void setAlbum(const QJsonObject &album) { m_title->setText(album["title"].toString()); @@ -93,9 +125,14 @@ public: m_subtitle->setText(desc.isEmpty() ? owner : desc); m_meta->setText(buildPlaylistMeta(playlist)); - const QJsonArray imgs = playlist["image_rectangle"].toArray(); - if (!imgs.isEmpty()) - fetchUrl(imgs.first().toString()); + // Try images300 → images150 → images (API returns mosaic arrays, not image_rectangle) + const QJsonArray imgs300 = playlist["images300"].toArray(); + const QJsonArray imgs150 = playlist["images150"].toArray(); + const QJsonArray imgs = playlist["images"].toArray(); + const QJsonArray &best = !imgs300.isEmpty() ? imgs300 + : !imgs150.isEmpty() ? imgs150 : imgs; + if (!best.isEmpty()) + fetchUrl(best.first().toString()); else m_art->setPixmap(QPixmap()); @@ -140,7 +177,6 @@ private: if (tracks > 0) parts << QStringLiteral("%1 tracks").arg(tracks); const int dur = static_cast(album["duration"].toDouble()); if (dur > 0) parts << formatDuration(dur); - // Resolution: e.g. "24-bit / 96 kHz" const int bits = album["maximum_bit_depth"].toInt(); const double rate = album["maximum_sampling_rate"].toDouble(); if (bits > 0 && rate > 0) { @@ -162,10 +198,12 @@ private: return parts.join(QStringLiteral(" · ")); } - QLabel *m_art = nullptr; - QLabel *m_title = nullptr; - QLabel *m_subtitle = nullptr; - QLabel *m_meta = nullptr; - QNetworkAccessManager *m_nam = nullptr; + QLabel *m_art = nullptr; + QLabel *m_title = nullptr; + QLabel *m_subtitle = nullptr; + QLabel *m_meta = nullptr; + QPushButton *m_playBtn = nullptr; + QPushButton *m_shuffleBtn = nullptr; + QNetworkAccessManager *m_nam = nullptr; QString m_currentArtUrl; };