refactor: UI polish — lock sidebar, remove nav buttons, uniform artist tables, deep shuffle

- Lock sidebar width (setFixedWidth) so it doesn't jump between views
- Remove back/forward navigation buttons and all NavPage history code
- Uniform column layout on artist page: hide Artist column from both
  Popular Tracks and release sections, set matching fixed column widths
  so columns align vertically across all sections
- Deep shuffle: new Rust FFI endpoint fetches tracks from all albums
  in parallel, combines them, and returns via EV_DEEP_SHUFFLE_OK
- Auto-paginate artist releases in Rust (loop until has_more=false)
  so all releases load at once sorted newest-first

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-25 23:42:00 +01:00
parent 333a620be2
commit a21d0c8a33
13 changed files with 217 additions and 161 deletions

View File

@@ -1,9 +1,11 @@
#include "artistview.hpp"
#include "albumlistview.hpp"
#include "../model/tracklistmodel.hpp"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QHeaderView>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
@@ -49,50 +51,18 @@ ArtistSection::ArtistSection(const QString &title, const QString &releaseType, Q
m_list = new AlbumListView(this);
layout->addWidget(m_list);
// "Load more" button (shown when has_more is true)
m_loadMoreBtn = new QPushButton(tr("Load more…"), this);
m_loadMoreBtn->setStyleSheet(QStringLiteral(
"QPushButton { text-align: left; color: #FFB232; background: transparent;"
" border: none; padding: 6px 8px; }"
"QPushButton:hover { background: #1e1e1e; }"));
m_loadMoreBtn->setCursor(Qt::PointingHandCursor);
m_loadMoreBtn->setVisible(false);
layout->addWidget(m_loadMoreBtn);
connect(m_toggle, &QPushButton::toggled, this, [this](bool checked) {
m_list->setVisible(checked);
m_loadMoreBtn->setVisible(checked && m_hasMore);
updateToggleText();
});
connect(m_list, &AlbumListView::albumSelected, this, &ArtistSection::albumSelected);
connect(m_loadMoreBtn, &QPushButton::clicked, this, [this] {
m_loadMoreBtn->setEnabled(false);
m_loadMoreBtn->setText(tr("Loading…"));
emit loadMoreRequested(m_releaseType, m_loadedCount);
});
updateToggleText();
}
void ArtistSection::setAlbums(const QJsonArray &albums, bool hasMore)
void ArtistSection::setAlbums(const QJsonArray &albums)
{
m_list->setAlbums(albums);
m_loadedCount = albums.size();
m_hasMore = hasMore;
m_loadMoreBtn->setVisible(hasMore && m_toggle->isChecked());
m_loadMoreBtn->setEnabled(true);
m_loadMoreBtn->setText(tr("Load more…"));
updateToggleText();
}
void ArtistSection::appendAlbums(const QJsonArray &albums, bool hasMore)
{
m_list->addAlbums(albums);
m_loadedCount += albums.size();
m_hasMore = hasMore;
m_loadMoreBtn->setVisible(hasMore && m_toggle->isChecked());
m_loadMoreBtn->setEnabled(true);
m_loadMoreBtn->setText(tr("Load more…"));
updateToggleText();
}
@@ -101,11 +71,28 @@ bool ArtistSection::isEmpty() const
return m_list->topLevelItemCount() == 0;
}
QStringList ArtistSection::albumIds() const
{
QStringList ids;
for (int i = 0; i < m_list->topLevelItemCount(); ++i) {
const QString id = m_list->topLevelItem(i)->data(1, Qt::UserRole).toString();
if (!id.isEmpty())
ids.append(id);
}
return ids;
}
void ArtistSection::setArtistPageMode()
{
m_list->setArtistPageMode();
}
void ArtistSection::updateToggleText()
{
const int count = m_list->topLevelItemCount();
const QString arrow = m_toggle->isChecked() ? QStringLiteral("") : QStringLiteral("");
const QString text = m_loadedCount > 0
? QStringLiteral("%1%2 (%3)").arg(arrow, m_baseTitle).arg(m_loadedCount)
const QString text = count > 0
? QStringLiteral("%1%2 (%3)").arg(arrow, m_baseTitle).arg(count)
: arrow + m_baseTitle;
m_toggle->setText(text);
}
@@ -117,6 +104,7 @@ void ArtistSection::updateToggleText()
ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
: QWidget(parent)
, m_backend(backend)
, m_queue(queue)
{
auto *outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(0, 0, 0, 0);
@@ -165,7 +153,7 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
QStringLiteral("QPushButton { background: #FFB232; color: #000; }"
"QPushButton:pressed { background: #e09e28; }"));
m_shuffleBtn = new QPushButton(tr("⇄ Shuffle"), info);
m_shuffleBtn = new QPushButton(tr("⇄ Shuffle All"), info);
m_shuffleBtn->setStyleSheet(kBtnBase +
QStringLiteral("QPushButton { background: #2a2a2a; color: #FFB232; border: 1px solid #FFB232; }"
"QPushButton:pressed { background: #333; }"));
@@ -222,6 +210,14 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
m_topTracks = new List::Tracks(backend, queue, m_topTracksSection);
m_topTracks->setMaximumHeight(320);
// Artist page column layout: hide Artist & Album, match album-section widths
m_topTracks->setColumnHidden(TrackListModel::ColArtist, true);
m_topTracks->setColumnHidden(TrackListModel::ColAlbum, true);
m_topTracks->header()->setSectionResizeMode(TrackListModel::ColNumber, QHeaderView::Fixed);
m_topTracks->header()->setSectionResizeMode(TrackListModel::ColTitle, QHeaderView::Stretch);
m_topTracks->header()->setSectionResizeMode(TrackListModel::ColDuration, QHeaderView::Fixed);
m_topTracks->header()->resizeSection(TrackListModel::ColNumber, 40);
m_topTracks->header()->resizeSection(TrackListModel::ColDuration, 70);
ttLayout->addWidget(m_topTracks);
connect(m_topTracksToggle, &QPushButton::toggled, m_topTracks, &QWidget::setVisible);
@@ -236,6 +232,10 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
m_secCompilations = new ArtistSection(tr("Compilations"), QStringLiteral("compilation"), content);
m_secOther = new ArtistSection(tr("Other"), QStringLiteral("other"), content);
// Uniform column layout: hide Artist column, match fixed widths across all sections
for (ArtistSection *sec : {m_secAlbums, m_secEps, m_secLive, m_secCompilations, m_secOther})
sec->setArtistPageMode();
sectLayout->addWidget(m_secAlbums);
sectLayout->addWidget(m_secEps);
sectLayout->addWidget(m_secLive);
@@ -246,9 +246,21 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
scroll->setWidget(content);
outerLayout->addWidget(scroll, 1);
// Playback connections
connect(m_playBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(false); });
connect(m_shuffleBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(true); });
// Play top tracks
connect(m_playBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(false); });
// Deep shuffle: fetch all album tracks, combine, shuffle, play
connect(m_shuffleBtn, &QPushButton::clicked, this, [this] {
const QStringList ids = allAlbumIds();
if (ids.isEmpty()) {
// Fallback: just shuffle popular tracks
m_topTracks->playAll(true);
return;
}
m_shuffleBtn->setEnabled(false);
m_shuffleBtn->setText(tr("Loading…"));
m_backend->getAlbumsTracks(ids);
});
// Favourite button
connect(m_favBtn, &QPushButton::clicked, this, [this] {
@@ -270,20 +282,6 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
connect(m_secLive, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secCompilations, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
connect(m_secOther, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
// Load-more connections
auto connectLoadMore = [this](ArtistSection *sec) {
connect(sec, &ArtistSection::loadMoreRequested, this,
[this](const QString &releaseType, int nextOffset) {
if (m_artistId > 0)
m_backend->getArtistReleases(m_artistId, releaseType, 50, static_cast<quint32>(nextOffset));
});
};
connectLoadMore(m_secAlbums);
connectLoadMore(m_secEps);
connectLoadMore(m_secLive);
connectLoadMore(m_secCompilations);
connectLoadMore(m_secOther);
}
void ArtistView::setArtist(const QJsonObject &artist)
@@ -352,6 +350,10 @@ void ArtistView::setArtist(const QJsonObject &artist)
: QStringLiteral("▼ Popular Tracks"));
m_topTracksSection->setVisible(!topTracks.isEmpty());
// Reset shuffle button state
m_shuffleBtn->setEnabled(true);
m_shuffleBtn->setText(tr("⇄ Shuffle All"));
// Clear release sections
for (ArtistSection *sec : {m_secAlbums, m_secEps, m_secLive, m_secCompilations, m_secOther}) {
sec->setAlbums({});
@@ -360,7 +362,7 @@ void ArtistView::setArtist(const QJsonObject &artist)
}
void ArtistView::setReleases(const QString &releaseType, const QJsonArray &items,
bool hasMore, int offset)
bool /*hasMore*/, int /*offset*/)
{
ArtistSection *sec = nullptr;
if (releaseType == QStringLiteral("album")) sec = m_secAlbums;
@@ -369,22 +371,42 @@ void ArtistView::setReleases(const QString &releaseType, const QJsonArray &items
else if (releaseType == QStringLiteral("compilation")) sec = m_secCompilations;
else sec = m_secOther;
if (offset == 0)
sec->setAlbums(items, hasMore);
else
sec->appendAlbums(items, hasMore);
// Rust auto-paginates, so we always get the full list at once
sec->setAlbums(items);
sec->setVisible(!sec->isEmpty());
}
void ArtistView::setFavArtistIds(const QSet<qint64> &ids)
{
m_favArtistIds = ids;
// Update current state if we're showing an artist
if (m_artistId > 0)
setFaved(ids.contains(m_artistId));
}
void ArtistView::onDeepShuffleTracks(const QJsonArray &tracks)
{
m_shuffleBtn->setEnabled(true);
m_shuffleBtn->setText(tr("⇄ Shuffle All"));
if (tracks.isEmpty()) return;
m_queue->setContext(tracks, 0);
m_queue->shuffleNow();
const QJsonObject first = m_queue->current();
const qint64 id = static_cast<qint64>(first["id"].toDouble());
if (id > 0)
emit playTrackRequested(id);
}
QStringList ArtistView::allAlbumIds() const
{
QStringList ids;
for (const ArtistSection *sec : {m_secAlbums, m_secEps, m_secLive, m_secCompilations, m_secOther})
ids.append(sec->albumIds());
return ids;
}
void ArtistView::setFaved(bool faved)
{
m_isFaved = faved;