#include "artistview.hpp" #include "albumlistview.hpp" #include #include #include #include #include #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 // --------------------------------------------------------------------------- static const QString kBtnBase = QStringLiteral( "QPushButton { padding: 5px 16px; border-radius: 4px; font-weight: bold; }" ); ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) : QWidget(parent) { auto *outerLayout = new QVBoxLayout(this); outerLayout->setContentsMargins(8, 8, 8, 8); outerLayout->setSpacing(6); // --- 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(); f.setPointSize(f.pointSize() + 4); f.setBold(true); m_nameLabel->setFont(f); headerInfoLayout->addWidget(m_nameLabel); m_bioEdit = new QTextEdit(headerInfo); m_bioEdit->setReadOnly(true); m_bioEdit->setFrameShape(QFrame::NoFrame); m_bioEdit->setMaximumHeight(80); m_bioEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_bioEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 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 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); 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(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); } // 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 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()); }