#include "artistview.hpp" #include "albumlistview.hpp" #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // 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()); }