diff --git a/rust/src/player/output.rs b/rust/src/player/output.rs index 0f6e91a..096325b 100644 --- a/rust/src/player/output.rs +++ b/rust/src/player/output.rs @@ -10,7 +10,9 @@ use std::sync::{ }; use symphonia::core::audio::AudioBufferRef; -const RING_BUFFER_SIZE: usize = 32 * 1024; +// Bigger output buffer gives the decoder/network pipeline more headroom, +// reducing audible underruns on transient CPU/network stalls. +const RING_BUFFER_SIZE: usize = 256 * 1024; pub struct AudioOutput { _ring: SpscRb, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b282d1e..3fc7ae8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -205,6 +205,7 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) #endif connect(m_library, &List::Library::favAlbumsRequested, this, [this] { + m_showFavAlbumsOnLoad = true; m_backend->getFavAlbums(); statusBar()->showMessage(tr("Loading favorite albums…")); }); @@ -259,6 +260,19 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) this, &MainWindow::onSearchAlbumSelected); connect(m_content, &MainContent::artistRequested, this, &MainWindow::onSearchArtistSelected); + connect(m_content, &MainContent::albumFavoriteToggled, + this, [this](const QString &albumId, bool favorite) { + if (favorite) { + m_backend->addFavAlbum(albumId); + m_favAlbumIds.insert(albumId); + statusBar()->showMessage(tr("Added album to favorites"), 3000); + } else { + m_backend->removeFavAlbum(albumId); + m_favAlbumIds.remove(albumId); + statusBar()->showMessage(tr("Removed album from favorites"), 3000); + } + m_content->setFavAlbumIds(m_favAlbumIds); + }); connect(m_content, &MainContent::playlistRequested, this, [this](qint64 playlistId) { m_backend->getPlaylist(playlistId); @@ -337,6 +351,8 @@ void MainWindow::tryRestoreSession() m_backend->getUser(); // userLoaded will call m_library->refresh() else m_library->refresh(); + // Preload fav albums so the album page fav button is accurate immediately. + m_backend->getFavAlbums(); // Preload fav artists so the artist page fav button works immediately m_backend->getFavArtists(); const QString name = AppSettings::instance().displayName(); @@ -388,6 +404,8 @@ void MainWindow::onLoginSuccess(const QString &token, const QJsonObject &user) statusBar()->showMessage(tr("Signed in as %1").arg( displayName.isEmpty() ? email : displayName)); m_library->refresh(); + m_backend->getFavAlbums(); + m_backend->getFavArtists(); } void MainWindow::onLoginError(const QString &error) @@ -449,9 +467,26 @@ void MainWindow::onFavTracksLoaded(const QJsonObject &result) void MainWindow::onFavAlbumsLoaded(const QJsonObject &result) { - m_content->showFavAlbums(result); - statusBar()->showMessage( - tr("%1 favorite albums").arg(result["total"].toInt()), 4000); + // Always cache fav album IDs (needed by the album page fav button) + m_favAlbumIds.clear(); + const QJsonArray items = result["items"].toArray(); + for (const QJsonValue &v : items) { + const QJsonObject album = v.toObject(); + QString id = album["id"].toString(); + if (id.isEmpty() && album["id"].isDouble()) + id = QString::number(static_cast(album["id"].toDouble())); + if (!id.isEmpty()) + m_favAlbumIds.insert(id); + } + m_content->setFavAlbumIds(m_favAlbumIds); + + // Only navigate to the fav albums page if the user explicitly requested it + if (m_showFavAlbumsOnLoad) { + m_showFavAlbumsOnLoad = false; + m_content->showFavAlbums(result); + statusBar()->showMessage( + tr("%1 favorite albums").arg(result["total"].toInt()), 4000); + } } void MainWindow::onFavArtistsLoaded(const QJsonObject &result) diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 4554f4a..81b5b77 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -55,7 +55,9 @@ private: QobuzBackend *m_backend = nullptr; PlayQueue *m_queue = nullptr; QVector> m_userPlaylists; + QSet m_favAlbumIds; QSet m_favArtistIds; + bool m_showFavAlbumsOnLoad = false; bool m_showFavArtistsOnLoad = false; MainToolBar *m_toolBar = nullptr; MainContent *m_content = nullptr; diff --git a/src/view/genrebrowser.cpp b/src/view/genrebrowser.cpp index 8087dc6..4f79f89 100644 --- a/src/view/genrebrowser.cpp +++ b/src/view/genrebrowser.cpp @@ -29,12 +29,20 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, PlayQueue *queue, QWid topLayout->setContentsMargins(8, 6, 8, 6); topLayout->setSpacing(6); + QFont topFont = topBar->font(); + if (topFont.pointSize() > 0) + topFont.setPointSize(topFont.pointSize() + 1); + static constexpr int controlHeight = 30; + m_browseLabel = new QLabel(tr("Show:"), this); + m_browseLabel->setFont(topFont); topLayout->addWidget(m_browseLabel); m_kindCombo = new QComboBox(this); m_kindCombo->addItem(tr("Albums"), QStringLiteral("albums")); m_kindCombo->addItem(tr("Playlists"), QStringLiteral("playlists")); + m_kindCombo->setFont(topFont); m_kindCombo->setMinimumWidth(110); + m_kindCombo->setFixedHeight(controlHeight); topLayout->addWidget(m_kindCombo); m_gapAfterKind = new QWidget(this); @@ -42,9 +50,12 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, PlayQueue *queue, QWid topLayout->addWidget(m_gapAfterKind); m_genreLabel = new QLabel(tr("Genre:"), this); + m_genreLabel->setFont(topFont); topLayout->addWidget(m_genreLabel); m_genreCombo = new QComboBox(this); + m_genreCombo->setFont(topFont); m_genreCombo->setMinimumWidth(180); + m_genreCombo->setFixedHeight(controlHeight); topLayout->addWidget(m_genreCombo); m_gapAfterGenre = new QWidget(this); @@ -52,29 +63,39 @@ GenreBrowserView::GenreBrowserView(QobuzBackend *backend, PlayQueue *queue, QWid topLayout->addWidget(m_gapAfterGenre); m_typeLabel = new QLabel(tr("Type:"), this); + m_typeLabel->setFont(topFont); topLayout->addWidget(m_typeLabel); m_typeCombo = new QComboBox(this); + m_typeCombo->setFont(topFont); m_typeCombo->setMinimumWidth(180); + m_typeCombo->setFixedHeight(controlHeight); topLayout->addWidget(m_typeCombo); m_playlistSearchLabel = new QLabel(tr("Search:"), this); + m_playlistSearchLabel->setFont(topFont); m_playlistSearchLabel->setVisible(false); topLayout->addWidget(m_playlistSearchLabel); m_playlistSearchBox = new QLineEdit(this); + m_playlistSearchBox->setFont(topFont); m_playlistSearchBox->setPlaceholderText(tr("Search playlists...")); m_playlistSearchBox->setClearButtonEnabled(true); m_playlistSearchBox->setVisible(false); m_playlistSearchBox->setMinimumWidth(220); m_playlistSearchBox->setMaximumWidth(320); + m_playlistSearchBox->setFixedHeight(controlHeight); topLayout->addWidget(m_playlistSearchBox); m_playlistSearchBtn = new QPushButton(tr("Go"), this); + m_playlistSearchBtn->setFont(topFont); m_playlistSearchBtn->setVisible(false); + m_playlistSearchBtn->setFixedHeight(controlHeight); topLayout->addWidget(m_playlistSearchBtn); m_deepShuffleBtn = new QPushButton(tr("⇄ Deep Shuffle"), this); + m_deepShuffleBtn->setFont(topFont); m_deepShuffleBtn->setVisible(false); + m_deepShuffleBtn->setFixedHeight(controlHeight); topLayout->addWidget(m_deepShuffleBtn); topLayout->addStretch(); diff --git a/src/view/maincontent.cpp b/src/view/maincontent.cpp index f449358..c364baf 100644 --- a/src/view/maincontent.cpp +++ b/src/view/maincontent.cpp @@ -48,6 +48,13 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren return; emit playlistFollowToggled(id, !m_header->playlistFollowed()); }); + QObject::connect(m_header->favButton(), &QPushButton::clicked, + [this] { + const QString albumId = m_header->albumId(); + if (albumId.isEmpty()) + return; + emit albumFavoriteToggled(albumId, !m_header->albumFaved()); + }); m_albumList = new AlbumListView(this); m_artistList = new ArtistListView(this); @@ -77,7 +84,10 @@ void MainContent::showWelcome() { m_stack->setCurrentIndex(0); } void MainContent::showAlbum(const QJsonObject &album) { - m_header->setAlbum(album); + QString albumId = album["id"].toString(); + if (albumId.isEmpty() && album["id"].isDouble()) + albumId = QString::number(static_cast(album["id"].toDouble())); + m_header->setAlbum(album, m_favAlbumIds.contains(albumId)); m_tracks->loadAlbum(album); m_stack->setCurrentIndex(1); } @@ -131,6 +141,14 @@ void MainContent::setFavArtistIds(const QSet &ids) m_artistView->setFavArtistIds(ids); } +void MainContent::setFavAlbumIds(const QSet &ids) +{ + m_favAlbumIds = ids; + const QString shownAlbumId = m_header->albumId(); + if (!shownAlbumId.isEmpty()) + m_header->setAlbumFaved(m_favAlbumIds.contains(shownAlbumId)); +} + void MainContent::onDeepShuffleTracks(const QJsonArray &tracks) { if (m_genreBrowser->tryHandleDeepShuffleTracks(tracks)) diff --git a/src/view/maincontent.hpp b/src/view/maincontent.hpp index f1d8dab..b60337d 100644 --- a/src/view/maincontent.hpp +++ b/src/view/maincontent.hpp @@ -14,6 +14,7 @@ #include #include #include +#include class MainContent : public QWidget { @@ -33,6 +34,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 setFavAlbumIds(const QSet &ids); void setFavArtistIds(const QSet &ids); void onDeepShuffleTracks(const QJsonArray &tracks); void showGenreBrowser(); @@ -44,6 +46,7 @@ public: signals: void albumRequested(const QString &albumId); void artistRequested(qint64 artistId); + void albumFavoriteToggled(const QString &albumId, bool favorite); void playlistRequested(qint64 playlistId); void playlistFollowToggled(qint64 playlistId, bool follow); void playTrackRequested(qint64 trackId); @@ -58,4 +61,5 @@ private: ArtistListView *m_artistList = nullptr; ArtistView *m_artistView = nullptr; GenreBrowserView *m_genreBrowser = nullptr; + QSet m_favAlbumIds; }; diff --git a/src/view/trackcontextheader.hpp b/src/view/trackcontextheader.hpp index 2894e39..2650c1c 100644 --- a/src/view/trackcontextheader.hpp +++ b/src/view/trackcontextheader.hpp @@ -93,6 +93,13 @@ public: "QPushButton:pressed { background: #333; }")); btnRow->addWidget(m_shuffleBtn); + m_favBtn = new QPushButton(tr("♡ Favourite"), info); + m_favBtn->setStyleSheet(btnBase + + QStringLiteral("QPushButton { background: #2a2a2a; color: #ccc; border: 1px solid #555; }" + "QPushButton:pressed { background: #333; }")); + m_favBtn->hide(); + btnRow->addWidget(m_favBtn); + m_followBtn = new QPushButton(tr("Follow"), info); m_followBtn->setStyleSheet(btnBase + QStringLiteral("QPushButton { background: #2a2a2a; color: #ddd; border: 1px solid #666; }" @@ -120,24 +127,35 @@ public: QPushButton *playButton() { return m_playBtn; } QPushButton *shuffleButton() { return m_shuffleBtn; } + QPushButton *favButton() { return m_favBtn; } QPushButton *followButton() { return m_followBtn; } QPushButton *subtitleButton() { return m_subtitle; } + QString albumId() const { return m_albumId; } + bool albumFaved() const { return m_albumFaved; } qint64 artistId() const { return m_artistId; } qint64 playlistId() const { return m_playlistId; } bool playlistFollowed() const { return m_playlistFollowed; } bool playlistOwned() const { return m_playlistOwned; } - void setAlbum(const QJsonObject &album) + void setAlbum(const QJsonObject &album, bool isFaved) { const QString base = album["title"].toString(); const QString ver = album["version"].toString().trimmed(); m_title->setText(ver.isEmpty() ? base : base + QStringLiteral(" (") + ver + QLatin1Char(')')); + m_albumId = album["id"].toString(); + if (m_albumId.isEmpty() && album["id"].isDouble()) + m_albumId = QString::number(static_cast(album["id"].toDouble())); m_artistId = static_cast(album["artist"].toObject()["id"].toDouble()); m_subtitle->setText(album["artist"].toObject()["name"].toString()); m_subtitle->setEnabled(m_artistId > 0); m_subtitle->setCursor(m_artistId > 0 ? Qt::PointingHandCursor : Qt::ArrowCursor); m_meta->setText(buildAlbumMeta(album)); + + setAlbumFaved(isFaved); + m_favBtn->setEnabled(!m_albumId.isEmpty()); + m_favBtn->show(); + m_followBtn->hide(); m_playlistId = 0; m_playlistFollowed = false; @@ -160,6 +178,10 @@ public: m_subtitle->setCursor(Qt::ArrowCursor); m_meta->setText(buildPlaylistMeta(playlist)); + m_albumId.clear(); + m_albumFaved = false; + m_favBtn->hide(); + if (m_playlistOwned) { m_followBtn->setText(tr("Owned")); m_followBtn->setEnabled(false); @@ -191,6 +213,24 @@ public: m_followBtn->setText(m_playlistFollowed ? tr("Unfollow") : tr("Follow")); } + void setAlbumFaved(bool faved) + { + m_albumFaved = faved; + if (faved) { + m_favBtn->setText(tr("♥ Favourited")); + m_favBtn->setStyleSheet(QStringLiteral( + "QPushButton { padding: 5px 16px; border-radius: 4px; font-weight: bold;" + " background: #2a2a2a; color: #FFB232; border: 1px solid #FFB232; }" + "QPushButton:pressed { background: #333; }")); + } else { + m_favBtn->setText(tr("♡ Favourite")); + m_favBtn->setStyleSheet(QStringLiteral( + "QPushButton { padding: 5px 16px; border-radius: 4px; font-weight: bold;" + " background: #2a2a2a; color: #ccc; border: 1px solid #555; }" + "QPushButton:pressed { background: #333; }")); + } + } + private: void fetchArt(const QJsonObject &img) { @@ -256,9 +296,12 @@ private: QLabel *m_meta = nullptr; QPushButton *m_playBtn = nullptr; QPushButton *m_shuffleBtn = nullptr; + QPushButton *m_favBtn = nullptr; QPushButton *m_followBtn = nullptr; QNetworkAccessManager *m_nam = nullptr; QString m_currentArtUrl; + QString m_albumId; + bool m_albumFaved = false; qint64 m_artistId = 0; qint64 m_playlistId = 0; bool m_playlistFollowed = false;