diff --git a/.gitignore b/.gitignore index bbf3176..1ac0663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ build-*/ target/ +src/visualizer/ .cache/ *.user *.autosave diff --git a/rust/src/player/mod.rs b/rust/src/player/mod.rs index 99f7c48..1dc67a4 100644 --- a/rust/src/player/mod.rs +++ b/rust/src/player/mod.rs @@ -215,6 +215,9 @@ fn player_loop(rx: std::sync::mpsc::Receiver, status: PlayerStatu Err(e) => { eprintln!("playback error: {e}"); *status.state.lock().unwrap() = PlayerState::Error(e.to_string()); + // Signal track end so the queue advances to the next track + // instead of stalling on an unplayable track. + status.track_finished.store(true, Ordering::SeqCst); } } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c7d81cf..269fda8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -121,6 +121,7 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) statusBar()->showMessage(tr("Loading favorite albums…")); }); connect(m_library, &List::Library::favArtistsRequested, this, [this] { + m_showFavArtistsOnLoad = true; m_backend->getFavArtists(); statusBar()->showMessage(tr("Loading favorite artists…")); }); @@ -233,6 +234,8 @@ void MainWindow::tryRestoreSession() m_backend->getUser(); // userLoaded will call m_library->refresh() else m_library->refresh(); + // Preload fav artists so the artist page fav button works immediately + m_backend->getFavArtists(); const QString name = AppSettings::instance().displayName(); statusBar()->showMessage(tr("Signed in as %1").arg( name.isEmpty() ? AppSettings::instance().userEmail() : name)); @@ -339,9 +342,22 @@ void MainWindow::onFavAlbumsLoaded(const QJsonObject &result) void MainWindow::onFavArtistsLoaded(const QJsonObject &result) { - m_content->showFavArtists(result); - statusBar()->showMessage( - tr("%1 favorite artists").arg(result["total"].toInt()), 4000); + // Always cache fav artist IDs (needed by the artist page fav button) + m_favArtistIds.clear(); + const QJsonArray items = result["items"].toArray(); + for (const QJsonValue &v : items) { + const qint64 id = static_cast(v.toObject()["id"].toDouble()); + if (id > 0) m_favArtistIds.insert(id); + } + m_content->setFavArtistIds(m_favArtistIds); + + // Only navigate to the fav artists page if the user explicitly requested it + if (m_showFavArtistsOnLoad) { + m_showFavArtistsOnLoad = false; + m_content->showFavArtists(result); + statusBar()->showMessage( + tr("%1 favorite artists").arg(result["total"].toInt()), 4000); + } } void MainWindow::onAlbumLoaded(const QJsonObject &album) diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index c4bf972..2b825a8 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -53,6 +53,8 @@ private: QobuzBackend *m_backend = nullptr; PlayQueue *m_queue = nullptr; QVector> m_userPlaylists; + QSet m_favArtistIds; + bool m_showFavArtistsOnLoad = false; MainToolBar *m_toolBar = nullptr; MainContent *m_content = nullptr; List::Library *m_library = nullptr; diff --git a/src/view/artistview.cpp b/src/view/artistview.cpp index 2967476..7efefed 100644 --- a/src/view/artistview.cpp +++ b/src/view/artistview.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include // Shared button style (mirrors TrackContextHeader) @@ -18,11 +17,12 @@ static const QString kBtnBase = QStringLiteral( "QPushButton { padding: 5px 16px; border-radius: 4px; font-weight: bold; }" ); -// Section-toggle style: text-only, left-aligned, bottom separator +// Section-toggle style: flat QPushButton, truly left-aligned static const QString kToggleStyle = QStringLiteral( - "QToolButton { text-align: left; font-weight: bold; padding: 4px 6px;" - " border: none; border-bottom: 1px solid #333; }" - "QToolButton:hover { background: #1e1e1e; }" + "QPushButton { text-align: left; font-weight: bold; font-size: 13px;" + " padding: 6px 8px; border: none; border-bottom: 1px solid #333;" + " background: transparent; }" + "QPushButton:hover { background: #1e1e1e; }" ); // --------------------------------------------------------------------------- @@ -38,10 +38,10 @@ ArtistSection::ArtistSection(const QString &title, const QString &releaseType, Q layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - m_toggle = new QToolButton(this); + m_toggle = new QPushButton(this); m_toggle->setCheckable(true); m_toggle->setChecked(true); - m_toggle->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_toggle->setFlat(true); m_toggle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_toggle->setStyleSheet(kToggleStyle); layout->addWidget(m_toggle); @@ -49,16 +49,26 @@ ArtistSection::ArtistSection(const QString &title, const QString &releaseType, Q 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); + // "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); - // Infinite scroll: emit loadMoreRequested when scrolled to bottom - connect(m_list->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int val) { - const int max = m_list->verticalScrollBar()->maximum(); - if (m_hasMore && max > 0 && val >= max) { - m_hasMore = false; // prevent double-fire until next page arrives - emit loadMoreRequested(m_releaseType, m_loadedCount); - } + 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(); @@ -69,6 +79,9 @@ void ArtistSection::setAlbums(const QJsonArray &albums, bool hasMore) 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(); } @@ -77,6 +90,9 @@ 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(); } @@ -92,10 +108,6 @@ void ArtistSection::updateToggleText() ? QStringLiteral("%1%2 (%3)").arg(arrow, m_baseTitle).arg(m_loadedCount) : arrow + m_baseTitle; m_toggle->setText(text); - - disconnect(m_toggle, &QToolButton::toggled, nullptr, nullptr); - connect(m_toggle, &QToolButton::toggled, m_list, &AlbumListView::setVisible); - connect(m_toggle, &QToolButton::toggled, this, [this](bool) { updateToggleText(); }); } // --------------------------------------------------------------------------- @@ -194,16 +206,16 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) sectLayout->setContentsMargins(0, 0, 0, 0); sectLayout->setSpacing(0); - // Popular Tracks — same collapsible header style as ArtistSection + // Popular Tracks section — same toggle style as release sections m_topTracksSection = new QWidget(content); auto *ttLayout = new QVBoxLayout(m_topTracksSection); ttLayout->setContentsMargins(0, 0, 0, 0); ttLayout->setSpacing(0); - m_topTracksToggle = new QToolButton(m_topTracksSection); + m_topTracksToggle = new QPushButton(m_topTracksSection); m_topTracksToggle->setCheckable(true); m_topTracksToggle->setChecked(true); - m_topTracksToggle->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_topTracksToggle->setFlat(true); m_topTracksToggle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_topTracksToggle->setStyleSheet(kToggleStyle); ttLayout->addWidget(m_topTracksToggle); @@ -212,7 +224,7 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) m_topTracks->setMaximumHeight(320); ttLayout->addWidget(m_topTracks); - connect(m_topTracksToggle, &QToolButton::toggled, m_topTracks, &List::Tracks::setVisible); + connect(m_topTracksToggle, &QPushButton::toggled, m_topTracks, &QWidget::setVisible); connect(m_topTracks, &List::Tracks::playTrackRequested, this, &ArtistView::playTrackRequested); sectLayout->addWidget(m_topTracksSection); @@ -242,10 +254,13 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) connect(m_favBtn, &QPushButton::clicked, this, [this] { if (m_artistId <= 0) return; m_isFaved = !m_isFaved; - if (m_isFaved) + if (m_isFaved) { m_backend->addFavArtist(m_artistId); - else + m_favArtistIds.insert(m_artistId); + } else { m_backend->removeFavArtist(m_artistId); + m_favArtistIds.remove(m_artistId); + } setFaved(m_isFaved); }); @@ -256,7 +271,7 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) connect(m_secCompilations, &ArtistSection::albumSelected, this, &ArtistView::albumSelected); connect(m_secOther, &ArtistSection::albumSelected, this, &ArtistView::albumSelected); - // Load-more connections: when a section scrolls to bottom, request next page + // Load-more connections auto connectLoadMore = [this](ArtistSection *sec) { connect(sec, &ArtistSection::loadMoreRequested, this, [this](const QString &releaseType, int nextOffset) { @@ -274,8 +289,7 @@ ArtistView::ArtistView(QobuzBackend *backend, PlayQueue *queue, QWidget *parent) void ArtistView::setArtist(const QJsonObject &artist) { m_artistId = static_cast(artist["id"].toDouble()); - m_isFaved = false; - setFaved(false); + setFaved(m_favArtistIds.contains(m_artistId)); m_nameLabel->setText(artist["name"].toObject()["display"].toString()); @@ -298,7 +312,6 @@ void ArtistView::setArtist(const QJsonObject &artist) } // Artist portrait: images.portrait.hash + format → CDN URL - // URL format: https://static.qobuz.com/images/artists/covers/large/{hash}.{format} const QJsonObject portrait = artist["images"].toObject()["portrait"].toObject(); const QString hash = portrait["hash"].toString(); const QString format = portrait["format"].toString(); @@ -307,7 +320,6 @@ void ArtistView::setArtist(const QJsonObject &artist) artUrl = QStringLiteral("https://static.qobuz.com/images/artists/covers/large/%1.%2") .arg(hash, format.isEmpty() ? QStringLiteral("jpg") : format); } else { - // Fallback: try direct image field const QJsonObject img = artist["image"].toObject(); artUrl = img["large"].toString(); if (artUrl.isEmpty()) artUrl = img["small"].toString(); @@ -325,9 +337,9 @@ void ArtistView::setArtist(const QJsonObject &artist) m_topTracks->loadTracks(topTracks); const int ttCount = topTracks.size(); - disconnect(m_topTracksToggle, &QToolButton::toggled, nullptr, nullptr); - connect(m_topTracksToggle, &QToolButton::toggled, m_topTracks, &List::Tracks::setVisible); - connect(m_topTracksToggle, &QToolButton::toggled, this, [this, ttCount](bool open) { + disconnect(m_topTracksToggle, &QPushButton::toggled, nullptr, nullptr); + connect(m_topTracksToggle, &QPushButton::toggled, m_topTracks, &QWidget::setVisible); + connect(m_topTracksToggle, &QPushButton::toggled, this, [this, ttCount](bool open) { const QString a = open ? QStringLiteral("▼ ") : QStringLiteral("▶ "); m_topTracksToggle->setText(ttCount > 0 ? QStringLiteral("%1Popular Tracks (%2)").arg(a).arg(ttCount) @@ -340,7 +352,7 @@ void ArtistView::setArtist(const QJsonObject &artist) : QStringLiteral("▼ Popular Tracks")); m_topTracksSection->setVisible(!topTracks.isEmpty()); - // Clear release sections — populated asynchronously via setReleases + // Clear release sections for (ArtistSection *sec : {m_secAlbums, m_secEps, m_secLive, m_secCompilations, m_secOther}) { sec->setAlbums({}); sec->setVisible(false); @@ -365,6 +377,14 @@ void ArtistView::setReleases(const QString &releaseType, const QJsonArray &items sec->setVisible(!sec->isEmpty()); } +void ArtistView::setFavArtistIds(const QSet &ids) +{ + m_favArtistIds = ids; + // Update current state if we're showing an artist + if (m_artistId > 0) + setFaved(ids.contains(m_artistId)); +} + void ArtistView::setFaved(bool faved) { m_isFaved = faved; diff --git a/src/view/artistview.hpp b/src/view/artistview.hpp index 87f0fc6..440f309 100644 --- a/src/view/artistview.hpp +++ b/src/view/artistview.hpp @@ -8,12 +8,11 @@ #include #include #include -#include #include #include -#include #include #include +#include class AlbumListView; @@ -22,7 +21,6 @@ class ArtistSection : public QWidget { Q_OBJECT public: - /// releaseType is the API string ("album", "epSingle", "live", "compilation"). explicit ArtistSection(const QString &title, const QString &releaseType, QWidget *parent = nullptr); void setAlbums(const QJsonArray &albums, bool hasMore = false); @@ -31,23 +29,21 @@ public: signals: void albumSelected(const QString &albumId); - /// Emitted when the user scrolls to the bottom and has_more is true. void loadMoreRequested(const QString &releaseType, int nextOffset); private: QString m_baseTitle; QString m_releaseType; - QToolButton *m_toggle = nullptr; - AlbumListView *m_list = nullptr; - bool m_hasMore = false; - int m_loadedCount = 0; + QPushButton *m_toggle = nullptr; + AlbumListView *m_list = nullptr; + QPushButton *m_loadMoreBtn = nullptr; + bool m_hasMore = false; + int m_loadedCount = 0; void updateToggleText(); }; -/// Artist detail page: portrait header (mirrors TrackContextHeader), -/// then collapsible sections for popular tracks, albums, EPs, live, -/// compilations, and other releases. +/// Artist detail page. class ArtistView : public QWidget { Q_OBJECT @@ -58,6 +54,7 @@ public: void setArtist(const QJsonObject &artist); void setReleases(const QString &releaseType, const QJsonArray &items, bool hasMore = false, int offset = 0); + void setFavArtistIds(const QSet &ids); signals: void albumSelected(const QString &albumId); @@ -77,11 +74,12 @@ private: QNetworkAccessManager *m_nam = nullptr; QString m_currentArtUrl; bool m_isFaved = false; + QSet m_favArtistIds; // Popular tracks section - QWidget *m_topTracksSection = nullptr; - QToolButton *m_topTracksToggle = nullptr; - List::Tracks *m_topTracks = nullptr; + QWidget *m_topTracksSection = nullptr; + QPushButton *m_topTracksToggle = nullptr; + List::Tracks *m_topTracks = nullptr; // Release sections ArtistSection *m_secAlbums = nullptr; diff --git a/src/view/maincontent.cpp b/src/view/maincontent.cpp index d9a3d42..b2d047c 100644 --- a/src/view/maincontent.cpp +++ b/src/view/maincontent.cpp @@ -112,3 +112,8 @@ void MainContent::updateArtistReleases(const QString &releaseType, const QJsonAr { m_artistView->setReleases(releaseType, items, hasMore, offset); } + +void MainContent::setFavArtistIds(const QSet &ids) +{ + m_artistView->setFavArtistIds(ids); +} diff --git a/src/view/maincontent.hpp b/src/view/maincontent.hpp index 3a4403b..3fae2e5 100644 --- a/src/view/maincontent.hpp +++ b/src/view/maincontent.hpp @@ -32,6 +32,7 @@ public: void showFavArtists(const QJsonObject &result); void showArtist(const QJsonObject &artist); void updateArtistReleases(const QString &releaseType, const QJsonArray &items, bool hasMore, int offset); + void setFavArtistIds(const QSet &ids); signals: void albumRequested(const QString &albumId);