Files
qobuz-qt/src/view/artistview.cpp
joren fb58c0ac8c fix: artist page top tracks and scrollable biography
- top_tracks is a flat array in the API response, not {items:[...]}
- Replace bio QLabel with scrollable QTextEdit (max 110px, scrolls if longer)
- Track model: handle artist.name as {display:...} object for top_tracks format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:02:26 +01:00

225 lines
8.5 KiB
C++

#include "artistview.hpp"
#include "albumlistview.hpp"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QTextEdit>
#include <QFont>
#include <QJsonValue>
#include <QRegularExpression>
// ---------------------------------------------------------------------------
// ArtistSection
// ---------------------------------------------------------------------------
ArtistSection::ArtistSection(const QString &title, QWidget *parent)
: QWidget(parent)
, m_baseTitle(title)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_toggle = new QToolButton(this);
m_toggle->setCheckable(true);
m_toggle->setChecked(true);
m_toggle->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_toggle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
m_toggle->setStyleSheet(QStringLiteral(
"QToolButton { text-align: left; font-weight: bold; padding: 4px 6px;"
" border: none; border-bottom: 1px solid #333; }"
"QToolButton:hover { background: #1e1e1e; }"
));
layout->addWidget(m_toggle);
m_list = new AlbumListView(this);
layout->addWidget(m_list);
connect(m_toggle, &QToolButton::toggled, m_list, &AlbumListView::setVisible);
connect(m_list, &AlbumListView::albumSelected, this, &ArtistSection::albumSelected);
updateToggleText(0);
}
void ArtistSection::setAlbums(const QJsonArray &albums)
{
m_list->setAlbums(albums);
updateToggleText(albums.size());
}
bool ArtistSection::isEmpty() const
{
return m_list->topLevelItemCount() == 0;
}
void ArtistSection::updateToggleText(int count)
{
const QString arrow = m_toggle->isChecked() ? QStringLiteral("") : QStringLiteral("");
const QString text = count > 0
? QStringLiteral("%1%2 (%3)").arg(arrow, m_baseTitle).arg(count)
: arrow + m_baseTitle;
m_toggle->setText(text);
// Keep arrow in sync when toggled
disconnect(m_toggle, &QToolButton::toggled, nullptr, nullptr);
connect(m_toggle, &QToolButton::toggled, m_list, &AlbumListView::setVisible);
connect(m_toggle, &QToolButton::toggled, this, [this, count](bool open) {
const QString a = open ? QStringLiteral("") : QStringLiteral("");
const QString t = count > 0
? QStringLiteral("%1%2 (%3)").arg(a, m_baseTitle).arg(count)
: a + m_baseTitle;
m_toggle->setText(t);
});
}
// ---------------------------------------------------------------------------
// ArtistView
// ---------------------------------------------------------------------------
ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
: QWidget(parent)
{
auto *outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(8, 8, 8, 8);
outerLayout->setSpacing(6);
m_nameLabel = new QLabel(this);
QFont f = m_nameLabel->font();
f.setPointSize(f.pointSize() + 4);
f.setBold(true);
m_nameLabel->setFont(f);
outerLayout->addWidget(m_nameLabel);
m_bioEdit = new QTextEdit(this);
m_bioEdit->setReadOnly(true);
m_bioEdit->setFrameShape(QFrame::NoFrame);
m_bioEdit->setMaximumHeight(110);
m_bioEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_bioEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
outerLayout->addWidget(m_bioEdit);
// Scrollable sections area
auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
auto *content = new QWidget(scroll);
auto *sectLayout = new QVBoxLayout(content);
sectLayout->setContentsMargins(0, 0, 0, 0);
sectLayout->setSpacing(8);
// --- Top Tracks section ---
m_topTracksSection = new QWidget(content);
auto *ttLayout = new QVBoxLayout(m_topTracksSection);
ttLayout->setContentsMargins(0, 0, 0, 0);
ttLayout->setSpacing(0);
// Header row: label + play + shuffle
auto *ttHeader = new QWidget(m_topTracksSection);
auto *ttHeaderLayout = new QHBoxLayout(ttHeader);
ttHeaderLayout->setContentsMargins(6, 4, 6, 4);
ttHeaderLayout->setSpacing(6);
auto *ttLabel = new QLabel(tr("Popular Tracks"), ttHeader);
QFont ttFont = ttLabel->font();
ttFont.setBold(true);
ttLabel->setFont(ttFont);
ttHeaderLayout->addWidget(ttLabel, 1);
auto *playBtn = new QPushButton(tr("▶ Play"), ttHeader);
auto *shuffleBtn = new QPushButton(tr("⇀ Shuffle"), ttHeader);
ttHeaderLayout->addWidget(playBtn);
ttHeaderLayout->addWidget(shuffleBtn);
ttLayout->addWidget(ttHeader);
m_topTracks = new List::Tracks(backend, queue, m_topTracksSection);
// Limit visible height so it doesn't swamp the page
m_topTracks->setMaximumHeight(320);
ttLayout->addWidget(m_topTracks);
connect(playBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(false); });
connect(shuffleBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(true); });
connect(m_topTracks, &List::Tracks::playTrackRequested, this, &ArtistView::playTrackRequested);
sectLayout->addWidget(m_topTracksSection);
// --- Album sections ---
m_secAlbums = new ArtistSection(tr("Albums"), content);
m_secEps = new ArtistSection(tr("Singles & EPs"), content);
m_secLive = new ArtistSection(tr("Live"), content);
m_secCompilations = new ArtistSection(tr("Compilations"), content);
m_secOther = new ArtistSection(tr("Other"), content);
sectLayout->addWidget(m_secAlbums);
sectLayout->addWidget(m_secEps);
sectLayout->addWidget(m_secLive);
sectLayout->addWidget(m_secCompilations);
sectLayout->addWidget(m_secOther);
sectLayout->addStretch();
scroll->setWidget(content);
outerLayout->addWidget(scroll, 1);
connect(m_secAlbums, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secEps, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secLive, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secCompilations, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secOther, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
}
void ArtistView::setArtist(const QJsonObject &artist)
{
// artist/page: name is {"display": "..."}
m_nameLabel->setText(artist["name"].toObject()["display"].toString());
// biography.content is HTML — strip tags for safe plain-text display
const QString bioHtml = artist["biography"].toObject()["content"].toString();
if (!bioHtml.isEmpty()) {
QString plain = bioHtml;
plain.remove(QRegularExpression(QStringLiteral("<[^>]*>")));
plain.replace(QStringLiteral("&amp;"), QStringLiteral("&"));
plain.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
plain.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
plain.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
plain.replace(QStringLiteral("&#39;"), QStringLiteral("'"));
plain.replace(QStringLiteral("&nbsp;"), QStringLiteral(" "));
plain = plain.trimmed();
m_bioEdit->setPlainText(plain);
m_bioEdit->setVisible(!plain.isEmpty());
} else {
m_bioEdit->setVisible(false);
}
// top_tracks is a flat array in the artist/page response
const QJsonArray topTracks = artist["top_tracks"].toArray();
m_topTracks->loadTracks(topTracks);
m_topTracksSection->setVisible(!topTracks.isEmpty());
// Release sections are populated asynchronously via setReleases().
// Clear them now so stale data from a previous artist isn't shown.
m_secAlbums->setAlbums({});
m_secEps->setAlbums({});
m_secLive->setAlbums({});
m_secCompilations->setAlbums({});
m_secOther->setAlbums({});
m_secAlbums->setVisible(false);
m_secEps->setVisible(false);
m_secLive->setVisible(false);
m_secCompilations->setVisible(false);
m_secOther->setVisible(false);
}
void ArtistView::setReleases(const QString &releaseType, const QJsonArray &items)
{
ArtistSection *sec = nullptr;
if (releaseType == QStringLiteral("album")) sec = m_secAlbums;
else if (releaseType == QStringLiteral("epSingle")) sec = m_secEps;
else if (releaseType == QStringLiteral("live")) sec = m_secLive;
else if (releaseType == QStringLiteral("compilation")) sec = m_secCompilations;
else sec = m_secOther;
sec->setAlbums(items);
sec->setVisible(!sec->isEmpty());
}