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:
@@ -35,6 +35,8 @@ enum QobuzEvent {
|
||||
EV_PLAYLIST_DELETED = 21,
|
||||
EV_PLAYLIST_TRACK_ADDED = 22,
|
||||
EV_USER_OK = 23,
|
||||
EV_ARTIST_RELEASES_OK = 24,
|
||||
EV_DEEP_SHUFFLE_OK = 25,
|
||||
};
|
||||
|
||||
// Callback signature
|
||||
@@ -84,9 +86,12 @@ uint32_t qobuz_backend_viz_read(QobuzBackendOpaque *backend, float *buf, uint32_
|
||||
uint32_t qobuz_backend_viz_sample_rate(const QobuzBackendOpaque *backend);
|
||||
uint32_t qobuz_backend_viz_channels(const QobuzBackendOpaque *backend);
|
||||
|
||||
// Artist releases (full paginated list per release type)
|
||||
// Artist releases (auto-paginates to fetch all)
|
||||
void qobuz_backend_get_artist_releases(QobuzBackendOpaque *backend, int64_t artist_id, const char *release_type, uint32_t limit, uint32_t offset);
|
||||
|
||||
// Deep shuffle: fetch tracks from multiple albums (album_ids_json is a JSON array of strings)
|
||||
void qobuz_backend_get_albums_tracks(QobuzBackendOpaque *backend, const char *album_ids_json);
|
||||
|
||||
// Playlist management
|
||||
void qobuz_backend_create_playlist(QobuzBackendOpaque *backend, const char *name);
|
||||
void qobuz_backend_delete_playlist(QobuzBackendOpaque *backend, int64_t playlist_id);
|
||||
|
||||
@@ -69,6 +69,7 @@ pub const EV_TRACK_URL_OK: c_int = 17;
|
||||
pub const EV_TRACK_URL_ERR: c_int = 18;
|
||||
pub const EV_GENERIC_ERR: c_int = 19;
|
||||
pub const EV_ARTIST_RELEASES_OK: c_int = 24;
|
||||
pub const EV_DEEP_SHUFFLE_OK: c_int = 25;
|
||||
|
||||
// ---------- Callback ----------
|
||||
|
||||
@@ -263,7 +264,7 @@ pub unsafe extern "C" fn qobuz_backend_get_artist_releases(
|
||||
artist_id: i64,
|
||||
release_type: *const c_char,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
_offset: u32,
|
||||
) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
@@ -271,19 +272,90 @@ pub unsafe extern "C" fn qobuz_backend_get_artist_releases(
|
||||
let rtype = CStr::from_ptr(release_type).to_string_lossy().into_owned();
|
||||
|
||||
spawn(inner, async move {
|
||||
// Auto-paginate: fetch all pages until has_more is false.
|
||||
let mut all_items: Vec<serde_json::Value> = Vec::new();
|
||||
let mut offset: u32 = 0;
|
||||
loop {
|
||||
let result = client.lock().await
|
||||
.get_artist_releases_list(artist_id, &rtype, limit, offset)
|
||||
.await;
|
||||
let (ev, json) = match result {
|
||||
match result {
|
||||
Ok(r) => {
|
||||
// Wrap with the release_type so Qt can route to the right section
|
||||
let mut obj = r.as_object().cloned().unwrap_or_default();
|
||||
obj.insert("release_type".to_string(), serde_json::Value::String(rtype));
|
||||
(EV_ARTIST_RELEASES_OK, serde_json::to_string(&obj).unwrap_or_default())
|
||||
let obj = r.as_object().cloned().unwrap_or_default();
|
||||
if let Some(items) = obj.get("items").and_then(|v| v.as_array()) {
|
||||
all_items.extend(items.iter().cloned());
|
||||
}
|
||||
let has_more = obj.get("has_more").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
if !has_more {
|
||||
break;
|
||||
}
|
||||
offset += limit;
|
||||
}
|
||||
Err(e) => {
|
||||
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = serde_json::json!({
|
||||
"release_type": rtype,
|
||||
"items": all_items,
|
||||
"has_more": false,
|
||||
"offset": 0
|
||||
});
|
||||
call_cb(cb, ud, EV_ARTIST_RELEASES_OK, &serde_json::to_string(&result).unwrap_or_default());
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Deep shuffle (fetch tracks from multiple albums) ----------
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn qobuz_backend_get_albums_tracks(
|
||||
ptr: *mut Backend,
|
||||
album_ids_json: *const c_char,
|
||||
) {
|
||||
let inner = &(*ptr).0;
|
||||
let client = inner.client.clone();
|
||||
let cb = inner.cb; let ud = inner.ud;
|
||||
let ids_str = CStr::from_ptr(album_ids_json).to_string_lossy().into_owned();
|
||||
|
||||
let album_ids: Vec<String> = match serde_json::from_str(&ids_str) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
|
||||
return;
|
||||
}
|
||||
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
|
||||
};
|
||||
call_cb(cb, ud, ev, &json);
|
||||
|
||||
spawn(inner, async move {
|
||||
let mut all_tracks: Vec<serde_json::Value> = Vec::new();
|
||||
for id in &album_ids {
|
||||
let result = client.lock().await.get_album(id).await;
|
||||
if let Ok(album) = result {
|
||||
if let Some(tracks) = album.tracks.as_ref().and_then(|t| t.items.as_ref()) {
|
||||
for t in tracks {
|
||||
// Serialize track and inject album info for playback context
|
||||
if let Ok(mut tv) = serde_json::to_value(t) {
|
||||
if let Some(obj) = tv.as_object_mut() {
|
||||
// Ensure album context is present on each track
|
||||
if obj.get("album").is_none() || obj["album"].is_null() {
|
||||
obj.insert("album".to_string(), serde_json::json!({
|
||||
"id": album.id,
|
||||
"title": album.title,
|
||||
"artist": album.artist,
|
||||
"image": album.image,
|
||||
}));
|
||||
}
|
||||
}
|
||||
all_tracks.push(tv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip albums that fail — don't abort the whole operation
|
||||
}
|
||||
let result = serde_json::json!({ "tracks": all_tracks });
|
||||
call_cb(cb, ud, EV_DEEP_SHUFFLE_OK, &serde_json::to_string(&result).unwrap_or_default());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,13 @@ void QobuzBackend::getArtistReleases(qint64 artistId, const QString &releaseType
|
||||
releaseType.toUtf8().constData(), limit, offset);
|
||||
}
|
||||
|
||||
void QobuzBackend::getAlbumsTracks(const QStringList &albumIds)
|
||||
{
|
||||
const QJsonArray arr = QJsonArray::fromStringList(albumIds);
|
||||
const QByteArray json = QJsonDocument(arr).toJson(QJsonDocument::Compact);
|
||||
qobuz_backend_get_albums_tracks(m_backend, json.constData());
|
||||
}
|
||||
|
||||
void QobuzBackend::getPlaylist(qint64 playlistId, quint32 offset, quint32 limit)
|
||||
{
|
||||
qobuz_backend_get_playlist(m_backend, playlistId, offset, limit);
|
||||
@@ -266,6 +273,9 @@ void QobuzBackend::onEvent(int eventType, const QString &json)
|
||||
obj["offset"].toInt()
|
||||
);
|
||||
break;
|
||||
case 25: // EV_DEEP_SHUFFLE_OK
|
||||
emit deepShuffleTracksLoaded(obj["tracks"].toArray());
|
||||
break;
|
||||
case EV_ARTIST_ERR:
|
||||
emit error(obj["error"].toString());
|
||||
break;
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
void getAlbum(const QString &albumId);
|
||||
void getArtist(qint64 artistId);
|
||||
void getArtistReleases(qint64 artistId, const QString &releaseType, quint32 limit = 50, quint32 offset = 0);
|
||||
void getAlbumsTracks(const QStringList &albumIds);
|
||||
void getPlaylist(qint64 playlistId, quint32 offset = 0, quint32 limit = 500);
|
||||
|
||||
// --- favorites ---
|
||||
@@ -88,6 +89,7 @@ signals:
|
||||
void albumLoaded(const QJsonObject &album);
|
||||
void artistLoaded(const QJsonObject &artist);
|
||||
void artistReleasesLoaded(const QString &releaseType, const QJsonArray &items, bool hasMore, int offset);
|
||||
void deepShuffleTracksLoaded(const QJsonArray &tracks);
|
||||
void playlistLoaded(const QJsonObject &playlist);
|
||||
void playlistCreated(const QJsonObject &playlist);
|
||||
void playlistDeleted(const QJsonObject &result);
|
||||
|
||||
@@ -40,7 +40,8 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
m_libraryDock->setObjectName(QStringLiteral("libraryDock"));
|
||||
m_libraryDock->setFeatures(QDockWidget::DockWidgetMovable);
|
||||
m_libraryDock->setWidget(m_library);
|
||||
m_libraryDock->setMinimumWidth(200);
|
||||
m_libraryDock->setMinimumWidth(180);
|
||||
m_library->setFixedWidth(220);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_libraryDock);
|
||||
|
||||
// ---- Now-playing context dock (left, below library) ----
|
||||
@@ -86,6 +87,8 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
connect(m_backend, &QobuzBackend::artistLoaded, this, &MainWindow::onArtistLoaded);
|
||||
connect(m_backend, &QobuzBackend::artistReleasesLoaded,
|
||||
m_content, &MainContent::updateArtistReleases);
|
||||
connect(m_backend, &QobuzBackend::deepShuffleTracksLoaded,
|
||||
m_content, &MainContent::onDeepShuffleTracks);
|
||||
connect(m_backend, &QobuzBackend::playlistLoaded, this, &MainWindow::onPlaylistLoaded);
|
||||
connect(m_backend, &QobuzBackend::playlistCreated, this, &MainWindow::onPlaylistCreated);
|
||||
connect(m_backend, &QobuzBackend::playlistDeleted, this, [this](const QJsonObject &) {
|
||||
@@ -170,22 +173,6 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
connect(m_toolBar, &MainToolBar::queueToggled,
|
||||
this, [this](bool v) { m_queuePanel->setVisible(v); });
|
||||
|
||||
connect(m_toolBar->backAction(), &QAction::triggered, this, [this] {
|
||||
if (m_navIndex <= 0) return;
|
||||
--m_navIndex;
|
||||
m_navFromHistory = true;
|
||||
navigateTo(m_navHistory[m_navIndex]);
|
||||
m_navFromHistory = false;
|
||||
updateNavButtons();
|
||||
});
|
||||
connect(m_toolBar->forwardAction(), &QAction::triggered, this, [this] {
|
||||
if (m_navIndex >= m_navHistory.size() - 1) return;
|
||||
++m_navIndex;
|
||||
m_navFromHistory = true;
|
||||
navigateTo(m_navHistory[m_navIndex]);
|
||||
m_navFromHistory = false;
|
||||
updateNavButtons();
|
||||
});
|
||||
connect(m_toolBar, &MainToolBar::albumRequested, this, &MainWindow::onSearchAlbumSelected);
|
||||
connect(m_toolBar, &MainToolBar::artistRequested, this, &MainWindow::onSearchArtistSelected);
|
||||
|
||||
@@ -394,16 +381,12 @@ void MainWindow::onPlayTrackRequested(qint64 trackId)
|
||||
|
||||
void MainWindow::onSearchAlbumSelected(const QString &albumId)
|
||||
{
|
||||
NavPage p; p.type = NavPage::Album; p.albumId = albumId;
|
||||
pushNav(p.type, p.albumId);
|
||||
m_backend->getAlbum(albumId);
|
||||
statusBar()->showMessage(tr("Loading album…"));
|
||||
}
|
||||
|
||||
void MainWindow::onSearchArtistSelected(qint64 artistId)
|
||||
{
|
||||
NavPage p; p.type = NavPage::Artist; p.artistId = artistId;
|
||||
pushNav(p.type, {}, p.artistId);
|
||||
m_backend->getArtist(artistId);
|
||||
statusBar()->showMessage(tr("Loading artist…"));
|
||||
}
|
||||
@@ -429,38 +412,3 @@ void MainWindow::onUserPlaylistsChanged(const QVector<QPair<qint64, QString>> &p
|
||||
m_content->tracksList()->setUserPlaylists(playlists);
|
||||
}
|
||||
|
||||
void MainWindow::pushNav(NavPage::Type type, const QString &albumId, qint64 artistId)
|
||||
{
|
||||
if (m_navFromHistory) return;
|
||||
// Truncate any forward history
|
||||
while (m_navHistory.size() > m_navIndex + 1)
|
||||
m_navHistory.removeLast();
|
||||
NavPage p;
|
||||
p.type = type;
|
||||
p.albumId = albumId;
|
||||
p.artistId = artistId;
|
||||
m_navHistory.push_back(p);
|
||||
m_navIndex = m_navHistory.size() - 1;
|
||||
updateNavButtons();
|
||||
}
|
||||
|
||||
void MainWindow::navigateTo(const NavPage &page)
|
||||
{
|
||||
switch (page.type) {
|
||||
case NavPage::Album:
|
||||
m_backend->getAlbum(page.albumId);
|
||||
statusBar()->showMessage(tr("Loading album…"));
|
||||
break;
|
||||
case NavPage::Artist:
|
||||
m_backend->getArtist(page.artistId);
|
||||
statusBar()->showMessage(tr("Loading artist…"));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateNavButtons()
|
||||
{
|
||||
m_toolBar->backAction()->setEnabled(m_navIndex > 0);
|
||||
m_toolBar->forwardAction()->setEnabled(m_navIndex < m_navHistory.size() - 1);
|
||||
}
|
||||
|
||||
@@ -64,19 +64,6 @@ private:
|
||||
QDockWidget *m_libraryDock = nullptr;
|
||||
LastFmScrobbler *m_scrobbler = nullptr;
|
||||
|
||||
// Navigation history (browser-style Back / Forward)
|
||||
struct NavPage {
|
||||
enum Type { None, Album, Artist } type = None;
|
||||
QString albumId;
|
||||
qint64 artistId = 0;
|
||||
};
|
||||
QVector<NavPage> m_navHistory;
|
||||
int m_navIndex = -1;
|
||||
bool m_navFromHistory = false;
|
||||
|
||||
void setupMenuBar();
|
||||
void tryRestoreSession();
|
||||
void pushNav(NavPage::Type type, const QString &albumId = {}, qint64 artistId = 0);
|
||||
void navigateTo(const NavPage &page);
|
||||
void updateNavButtons();
|
||||
};
|
||||
|
||||
@@ -44,6 +44,20 @@ public:
|
||||
addAlbums(albums);
|
||||
}
|
||||
|
||||
/// Configure for artist page: hide Artist column, set fixed column widths
|
||||
/// that match the Popular Tracks list for perfect vertical alignment.
|
||||
void setArtistPageMode()
|
||||
{
|
||||
setColumnHidden(2, true); // Artist — redundant on artist page
|
||||
header()->setSectionResizeMode(0, QHeaderView::Fixed);
|
||||
header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
header()->setSectionResizeMode(3, QHeaderView::Fixed);
|
||||
header()->setSectionResizeMode(4, QHeaderView::Fixed);
|
||||
header()->resizeSection(0, 40);
|
||||
header()->resizeSection(3, 120);
|
||||
header()->resizeSection(4, 70);
|
||||
}
|
||||
|
||||
void addAlbums(const QJsonArray &albums)
|
||||
{
|
||||
QFont hiResFont;
|
||||
|
||||
@@ -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
|
||||
// Play top tracks
|
||||
connect(m_playBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(false); });
|
||||
connect(m_shuffleBtn, &QPushButton::clicked, m_topTracks, [this] { m_topTracks->playAll(true); });
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -23,22 +23,19 @@ class ArtistSection : public QWidget
|
||||
public:
|
||||
explicit ArtistSection(const QString &title, const QString &releaseType, QWidget *parent = nullptr);
|
||||
|
||||
void setAlbums(const QJsonArray &albums, bool hasMore = false);
|
||||
void appendAlbums(const QJsonArray &albums, bool hasMore = false);
|
||||
void setAlbums(const QJsonArray &albums);
|
||||
bool isEmpty() const;
|
||||
QStringList albumIds() const;
|
||||
void setArtistPageMode();
|
||||
|
||||
signals:
|
||||
void albumSelected(const QString &albumId);
|
||||
void loadMoreRequested(const QString &releaseType, int nextOffset);
|
||||
|
||||
private:
|
||||
QString m_baseTitle;
|
||||
QString m_releaseType;
|
||||
QPushButton *m_toggle = nullptr;
|
||||
AlbumListView *m_list = nullptr;
|
||||
QPushButton *m_loadMoreBtn = nullptr;
|
||||
bool m_hasMore = false;
|
||||
int m_loadedCount = 0;
|
||||
|
||||
void updateToggleText();
|
||||
};
|
||||
@@ -55,6 +52,7 @@ public:
|
||||
void setReleases(const QString &releaseType, const QJsonArray &items,
|
||||
bool hasMore = false, int offset = 0);
|
||||
void setFavArtistIds(const QSet<qint64> &ids);
|
||||
void onDeepShuffleTracks(const QJsonArray &tracks);
|
||||
|
||||
signals:
|
||||
void albumSelected(const QString &albumId);
|
||||
@@ -62,6 +60,7 @@ signals:
|
||||
|
||||
private:
|
||||
QobuzBackend *m_backend = nullptr;
|
||||
PlayQueue *m_queue = nullptr;
|
||||
qint64 m_artistId = 0;
|
||||
|
||||
// Header widgets
|
||||
@@ -88,5 +87,6 @@ private:
|
||||
ArtistSection *m_secCompilations = nullptr;
|
||||
ArtistSection *m_secOther = nullptr;
|
||||
|
||||
QStringList allAlbumIds() const;
|
||||
void setFaved(bool faved);
|
||||
};
|
||||
|
||||
@@ -117,3 +117,8 @@ void MainContent::setFavArtistIds(const QSet<qint64> &ids)
|
||||
{
|
||||
m_artistView->setFavArtistIds(ids);
|
||||
}
|
||||
|
||||
void MainContent::onDeepShuffleTracks(const QJsonArray &tracks)
|
||||
{
|
||||
m_artistView->onDeepShuffleTracks(tracks);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ public:
|
||||
void showArtist(const QJsonObject &artist);
|
||||
void updateArtistReleases(const QString &releaseType, const QJsonArray &items, bool hasMore, int offset);
|
||||
void setFavArtistIds(const QSet<qint64> &ids);
|
||||
void onDeepShuffleTracks(const QJsonArray &tracks);
|
||||
|
||||
ArtistView *artistView() const { return m_artistView; }
|
||||
|
||||
signals:
|
||||
void albumRequested(const QString &albumId);
|
||||
|
||||
@@ -16,13 +16,6 @@ MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
||||
setContextMenuPolicy(Qt::PreventContextMenu);
|
||||
setIconSize(QSize(22, 22));
|
||||
|
||||
// ---- Back / Forward navigation ----
|
||||
m_back = addAction(QIcon::fromTheme(QStringLiteral("go-previous")), tr("Back"));
|
||||
m_back->setEnabled(false);
|
||||
m_fwd = addAction(QIcon::fromTheme(QStringLiteral("go-next")), tr("Forward"));
|
||||
m_fwd->setEnabled(false);
|
||||
addSeparator();
|
||||
|
||||
m_nam = new QNetworkAccessManager(this);
|
||||
connect(m_nam, &QNetworkAccessManager::finished, this, &MainToolBar::onAlbumArtReady);
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ public:
|
||||
void setCurrentTrack(const QJsonObject &track);
|
||||
void updateProgress(quint64 position, quint64 duration);
|
||||
|
||||
QAction *backAction() const { return m_back; }
|
||||
QAction *forwardAction() const { return m_fwd; }
|
||||
|
||||
signals:
|
||||
void searchToggled(bool visible);
|
||||
void queueToggled(bool visible);
|
||||
@@ -57,8 +54,6 @@ private:
|
||||
QobuzBackend *m_backend = nullptr;
|
||||
PlayQueue *m_queue = nullptr;
|
||||
|
||||
QAction *m_back = nullptr;
|
||||
QAction *m_fwd = nullptr;
|
||||
QLabel *m_artLabel = nullptr;
|
||||
QLabel *m_trackLabel = nullptr;
|
||||
QAction *m_previous = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user