feat: artist sections, fav indicator, art scaling fix, volume popup fix

- Artist profile: collapsible Albums / EPs & Singles / Other sections
  keyed on release_type; fetches up to 200 albums per artist
- Favorites: starred icon on favorited tracks, context menu shows
  Add or Remove (not both); IDs cached when fav tracks are loaded
- Shuffle button: one-time shuffle via shuffleNow() without touching
  global shuffle flag, so double-click still plays in order
- Now-playing art: replaced setFixedHeight hack with ArtWidget that
  overrides hasHeightForWidth() — scales smoothly up and down, no min-size
- Volume popup: replaced QMenu (laggy, broken drag) with Qt::Popup QFrame;
  appears below button; fixed size locked at 100% label width

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 17:56:47 +01:00
parent 75429faffe
commit 56473cae6f
15 changed files with 370 additions and 109 deletions

153
src/view/artistview.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include "artistview.hpp"
#include "albumlistview.hpp"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QFont>
#include <QJsonValue>
// ---------------------------------------------------------------------------
// 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; }"
));
updateToggleText(0);
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);
}
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(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_bioLabel = new QLabel(this);
m_bioLabel->setWordWrap(true);
m_bioLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
m_bioLabel->setMaximumHeight(80);
outerLayout->addWidget(m_bioLabel);
// 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);
m_secAlbums = new ArtistSection(tr("Albums"), content);
m_secEps = new ArtistSection(tr("EPs & Singles"), content);
m_secOther = new ArtistSection(tr("Other"), content);
sectLayout->addWidget(m_secAlbums);
sectLayout->addWidget(m_secEps);
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_secOther, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
}
void ArtistView::setArtist(const QJsonObject &artist)
{
m_nameLabel->setText(artist["name"].toString());
const QString summary = artist["biography"].toObject()["summary"].toString();
m_bioLabel->setText(summary);
m_bioLabel->setVisible(!summary.isEmpty());
const QJsonArray allAlbums = artist["albums"].toObject()["items"].toArray();
QJsonArray albums, eps, other;
for (const QJsonValue &v : allAlbums) {
const QJsonObject a = v.toObject();
const QString rt = a["release_type"].toString();
if (rt == QStringLiteral("album"))
albums.append(a);
else if (rt == QStringLiteral("epSingle"))
eps.append(a);
else
other.append(a);
}
m_secAlbums->setAlbums(albums);
m_secEps->setAlbums(eps);
m_secOther->setAlbums(other);
m_secAlbums->setVisible(!m_secAlbums->isEmpty());
m_secEps->setVisible(!m_secEps->isEmpty());
m_secOther->setVisible(!m_secOther->isEmpty());
}