feat: playlist art fix + Play/Shuffle buttons in context header
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -183,7 +183,12 @@ pub struct PlaylistDto {
|
|||||||
pub duration: Option<i64>,
|
pub duration: Option<i64>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub owner: Option<PlaylistOwnerDto>,
|
pub owner: Option<PlaylistOwnerDto>,
|
||||||
pub image_rectangle: Option<Vec<String>>,
|
/// 4-cover mosaic at 300 px (preferred)
|
||||||
|
pub images300: Option<Vec<String>>,
|
||||||
|
/// 4-cover mosaic at 150 px (fallback)
|
||||||
|
pub images150: Option<Vec<String>>,
|
||||||
|
/// 4-cover mosaic at 50 px (last resort)
|
||||||
|
pub images: Option<Vec<String>>,
|
||||||
pub tracks: Option<TracksWrapper>,
|
pub tracks: Option<TracksWrapper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ void Tracks::setPlayingTrackId(qint64 id)
|
|||||||
m_model->setPlayingId(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<qint64>(m_queue->current()["id"].toDouble());
|
||||||
|
if (firstId > 0)
|
||||||
|
emit playTrackRequested(firstId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void Tracks::onDoubleClicked(const QModelIndex &index)
|
void Tracks::onDoubleClicked(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace List
|
|||||||
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
|
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
|
||||||
void setPlayingTrackId(qint64 id);
|
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).
|
/// Set which playlist is currently displayed (0 = none).
|
||||||
/// isOwned controls whether "Remove from this playlist" is shown.
|
/// isOwned controls whether "Remove from this playlist" is shown.
|
||||||
void setPlaylistContext(qint64 playlistId, bool isOwned = false);
|
void setPlaylistContext(qint64 playlistId, bool isOwned = false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "maincontent.hpp"
|
#include "maincontent.hpp"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
@@ -31,6 +32,11 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
tracksLayout->addWidget(m_header);
|
tracksLayout->addWidget(m_header);
|
||||||
tracksLayout->addWidget(m_tracks, 1);
|
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_albumList = new AlbumListView(this);
|
||||||
m_artistList = new ArtistListView(this);
|
m_artistList = new ArtistListView(this);
|
||||||
m_artistView = new ArtistView(this);
|
m_artistView = new ArtistView(this);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
@@ -15,14 +16,14 @@
|
|||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
/// Header strip shown above the track list when an album or playlist is open.
|
/// 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
|
class TrackContextHeader : public QWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit TrackContextHeader(QWidget *parent = nullptr)
|
explicit TrackContextHeader(QWidget *parent = nullptr)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
setFixedHeight(140);
|
setFixedHeight(148);
|
||||||
|
|
||||||
auto *hlay = new QHBoxLayout(this);
|
auto *hlay = new QHBoxLayout(this);
|
||||||
hlay->setContentsMargins(12, 8, 12, 8);
|
hlay->setContentsMargins(12, 8, 12, 8);
|
||||||
@@ -61,7 +62,35 @@ public:
|
|||||||
m_meta->setPalette(mp);
|
m_meta->setPalette(mp);
|
||||||
vlay->addWidget(m_meta);
|
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);
|
hlay->addWidget(info, 1);
|
||||||
|
|
||||||
m_nam = new QNetworkAccessManager(this);
|
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)
|
void setAlbum(const QJsonObject &album)
|
||||||
{
|
{
|
||||||
m_title->setText(album["title"].toString());
|
m_title->setText(album["title"].toString());
|
||||||
@@ -93,9 +125,14 @@ public:
|
|||||||
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
||||||
m_meta->setText(buildPlaylistMeta(playlist));
|
m_meta->setText(buildPlaylistMeta(playlist));
|
||||||
|
|
||||||
const QJsonArray imgs = playlist["image_rectangle"].toArray();
|
// Try images300 → images150 → images (API returns mosaic arrays, not image_rectangle)
|
||||||
if (!imgs.isEmpty())
|
const QJsonArray imgs300 = playlist["images300"].toArray();
|
||||||
fetchUrl(imgs.first().toString());
|
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
|
else
|
||||||
m_art->setPixmap(QPixmap());
|
m_art->setPixmap(QPixmap());
|
||||||
|
|
||||||
@@ -140,7 +177,6 @@ private:
|
|||||||
if (tracks > 0) parts << QStringLiteral("%1 tracks").arg(tracks);
|
if (tracks > 0) parts << QStringLiteral("%1 tracks").arg(tracks);
|
||||||
const int dur = static_cast<int>(album["duration"].toDouble());
|
const int dur = static_cast<int>(album["duration"].toDouble());
|
||||||
if (dur > 0) parts << formatDuration(dur);
|
if (dur > 0) parts << formatDuration(dur);
|
||||||
// Resolution: e.g. "24-bit / 96 kHz"
|
|
||||||
const int bits = album["maximum_bit_depth"].toInt();
|
const int bits = album["maximum_bit_depth"].toInt();
|
||||||
const double rate = album["maximum_sampling_rate"].toDouble();
|
const double rate = album["maximum_sampling_rate"].toDouble();
|
||||||
if (bits > 0 && rate > 0) {
|
if (bits > 0 && rate > 0) {
|
||||||
@@ -162,10 +198,12 @@ private:
|
|||||||
return parts.join(QStringLiteral(" · "));
|
return parts.join(QStringLiteral(" · "));
|
||||||
}
|
}
|
||||||
|
|
||||||
QLabel *m_art = nullptr;
|
QLabel *m_art = nullptr;
|
||||||
QLabel *m_title = nullptr;
|
QLabel *m_title = nullptr;
|
||||||
QLabel *m_subtitle = nullptr;
|
QLabel *m_subtitle = nullptr;
|
||||||
QLabel *m_meta = nullptr;
|
QLabel *m_meta = nullptr;
|
||||||
QNetworkAccessManager *m_nam = nullptr;
|
QPushButton *m_playBtn = nullptr;
|
||||||
|
QPushButton *m_shuffleBtn = nullptr;
|
||||||
|
QNetworkAccessManager *m_nam = nullptr;
|
||||||
QString m_currentArtUrl;
|
QString m_currentArtUrl;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user