- 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>
225 lines
8.5 KiB
C++
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("&"), QStringLiteral("&"));
|
|
plain.replace(QStringLiteral("<"), QStringLiteral("<"));
|
|
plain.replace(QStringLiteral(">"), QStringLiteral(">"));
|
|
plain.replace(QStringLiteral("""), QStringLiteral("\""));
|
|
plain.replace(QStringLiteral("'"), QStringLiteral("'"));
|
|
plain.replace(QStringLiteral(" "), 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());
|
|
}
|