feat: artist portrait, race condition fix, and uniform button styling

- Load artist portrait from images.portrait.hash via QNetworkAccessManager
- Fix race condition: fire getArtistReleases after setArtist() clears sections,
  not before (from onArtistLoaded instead of onSearchArtistSelected)
- Apply uniform gold (#FFB232) play/shuffle button style matching album view
- Make biography scrollable (QTextEdit with max height + scroll on overflow)
- Extend track artist name parsing to handle top_tracks {display:...} format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-25 14:17:36 +01:00
parent fb58c0ac8c
commit 8310eceeb2
3 changed files with 80 additions and 12 deletions

View File

@@ -335,6 +335,12 @@ void MainWindow::onAlbumLoaded(const QJsonObject &album)
void MainWindow::onArtistLoaded(const QJsonObject &artist) void MainWindow::onArtistLoaded(const QJsonObject &artist)
{ {
m_content->showArtist(artist); m_content->showArtist(artist);
// Fire release requests only after the artist page is shown — avoids the
// race where a fast-responding release request arrives before setArtist()
// clears the sections, causing setArtist() to wipe out the data.
const qint64 artistId = static_cast<qint64>(artist["id"].toDouble());
for (const char *type : {"album", "epSingle", "live", "compilation"})
m_backend->getArtistReleases(artistId, QString::fromLatin1(type));
statusBar()->showMessage( statusBar()->showMessage(
tr("Artist: %1").arg(artist["name"].toObject()["display"].toString()), 4000); tr("Artist: %1").arg(artist["name"].toObject()["display"].toString()), 4000);
} }
@@ -360,9 +366,6 @@ void MainWindow::onSearchAlbumSelected(const QString &albumId)
void MainWindow::onSearchArtistSelected(qint64 artistId) void MainWindow::onSearchArtistSelected(qint64 artistId)
{ {
m_backend->getArtist(artistId); m_backend->getArtist(artistId);
// Fire release-type requests in parallel — each updates its section when it arrives
for (const char *type : {"album", "epSingle", "live", "compilation"})
m_backend->getArtistReleases(artistId, QString::fromLatin1(type));
statusBar()->showMessage(tr("Loading artist…")); statusBar()->showMessage(tr("Loading artist…"));
} }

View File

@@ -5,6 +5,11 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QScrollArea> #include <QScrollArea>
#include <QTextEdit> #include <QTextEdit>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPixmap>
#include <QUrl>
#include <QFont> #include <QFont>
#include <QJsonValue> #include <QJsonValue>
#include <QRegularExpression> #include <QRegularExpression>
@@ -76,6 +81,10 @@ void ArtistSection::updateToggleText(int count)
// ArtistView // ArtistView
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static const QString kBtnBase = QStringLiteral(
"QPushButton { padding: 5px 16px; border-radius: 4px; font-weight: bold; }"
);
ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
@@ -83,20 +92,49 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
outerLayout->setContentsMargins(8, 8, 8, 8); outerLayout->setContentsMargins(8, 8, 8, 8);
outerLayout->setSpacing(6); outerLayout->setSpacing(6);
m_nameLabel = new QLabel(this); // --- Artist header: portrait + name + bio ---
auto *headerRow = new QHBoxLayout;
headerRow->setSpacing(12);
m_artLabel = new QLabel(this);
m_artLabel->setFixedSize(100, 100);
m_artLabel->setScaledContents(true);
m_artLabel->setAlignment(Qt::AlignCenter);
m_artLabel->setStyleSheet(QStringLiteral("background: #1a1a1a; border-radius: 50px;"));
headerRow->addWidget(m_artLabel, 0, Qt::AlignTop);
auto *headerInfo = new QWidget(this);
auto *headerInfoLayout = new QVBoxLayout(headerInfo);
headerInfoLayout->setContentsMargins(0, 0, 0, 0);
headerInfoLayout->setSpacing(4);
m_nameLabel = new QLabel(headerInfo);
QFont f = m_nameLabel->font(); QFont f = m_nameLabel->font();
f.setPointSize(f.pointSize() + 4); f.setPointSize(f.pointSize() + 4);
f.setBold(true); f.setBold(true);
m_nameLabel->setFont(f); m_nameLabel->setFont(f);
outerLayout->addWidget(m_nameLabel); headerInfoLayout->addWidget(m_nameLabel);
m_bioEdit = new QTextEdit(this); m_bioEdit = new QTextEdit(headerInfo);
m_bioEdit->setReadOnly(true); m_bioEdit->setReadOnly(true);
m_bioEdit->setFrameShape(QFrame::NoFrame); m_bioEdit->setFrameShape(QFrame::NoFrame);
m_bioEdit->setMaximumHeight(110); m_bioEdit->setMaximumHeight(80);
m_bioEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_bioEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_bioEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_bioEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
outerLayout->addWidget(m_bioEdit); headerInfoLayout->addWidget(m_bioEdit);
headerRow->addWidget(headerInfo, 1);
outerLayout->addLayout(headerRow);
m_nam = new QNetworkAccessManager(this);
QObject::connect(m_nam, &QNetworkAccessManager::finished,
this, [this](QNetworkReply *reply) {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) return;
QPixmap pix;
if (pix.loadFromData(reply->readAll()))
m_artLabel->setPixmap(pix);
});
// Scrollable sections area // Scrollable sections area
auto *scroll = new QScrollArea(this); auto *scroll = new QScrollArea(this);
@@ -127,7 +165,15 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
ttHeaderLayout->addWidget(ttLabel, 1); ttHeaderLayout->addWidget(ttLabel, 1);
auto *playBtn = new QPushButton(tr("▶ Play"), ttHeader); auto *playBtn = new QPushButton(tr("▶ Play"), ttHeader);
auto *shuffleBtn = new QPushButton(tr("⇀ Shuffle"), ttHeader); playBtn->setStyleSheet(kBtnBase +
QStringLiteral("QPushButton { background: #FFB232; color: #000; }"
"QPushButton:pressed { background: #e09e28; }"));
auto *shuffleBtn = new QPushButton(tr("⇄ Shuffle"), ttHeader);
shuffleBtn->setStyleSheet(kBtnBase +
QStringLiteral("QPushButton { background: #2a2a2a; color: #FFB232; border: 1px solid #FFB232; }"
"QPushButton:pressed { background: #333; }"));
ttHeaderLayout->addWidget(playBtn); ttHeaderLayout->addWidget(playBtn);
ttHeaderLayout->addWidget(shuffleBtn); ttHeaderLayout->addWidget(shuffleBtn);
@@ -191,6 +237,21 @@ void ArtistView::setArtist(const QJsonObject &artist)
m_bioEdit->setVisible(false); m_bioEdit->setVisible(false);
} }
// Artist portrait: images.portrait.hash → CDN URL
const QString hash = artist["images"].toObject()["portrait"].toObject()["hash"].toString();
if (!hash.isEmpty() && hash.length() >= 4) {
const QString p1 = hash.right(2);
const QString p2 = hash.mid(hash.length() - 4, 2);
const QString url = QStringLiteral("https://static.qobuz.com/images/artists/%1/%2/%3_600.jpg")
.arg(p1, p2, hash);
if (url != m_currentArtUrl) {
m_currentArtUrl = url;
m_nam->get(QNetworkRequest(QUrl(url)));
}
} else {
m_artLabel->setPixmap(QPixmap());
}
// top_tracks is a flat array in the artist/page response // top_tracks is a flat array in the artist/page response
const QJsonArray topTracks = artist["top_tracks"].toArray(); const QJsonArray topTracks = artist["top_tracks"].toArray();
m_topTracks->loadTracks(topTracks); m_topTracks->loadTracks(topTracks);

View File

@@ -10,6 +10,7 @@
#include <QTextEdit> #include <QTextEdit>
#include <QToolButton> #include <QToolButton>
#include <QPushButton> #include <QPushButton>
#include <QNetworkAccessManager>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
@@ -54,8 +55,11 @@ signals:
void playTrackRequested(qint64 trackId); void playTrackRequested(qint64 trackId);
private: private:
QLabel *m_artLabel = nullptr;
QLabel *m_nameLabel = nullptr; QLabel *m_nameLabel = nullptr;
QTextEdit *m_bioEdit = nullptr; QTextEdit *m_bioEdit = nullptr;
QNetworkAccessManager *m_nam = nullptr;
QString m_currentArtUrl;
// Top tracks section // Top tracks section
QWidget *m_topTracksSection = nullptr; QWidget *m_topTracksSection = nullptr;