feat: add album favorites in header and unify browse controls
Some checks failed
Build for Windows / build-windows (push) Has been cancelled

Improve album workflow with in-header favorite toggle, keep favorite state synced from backend, and normalize browse top-bar sizing while increasing audio output buffer headroom to reduce glitches.
This commit is contained in:
joren
2026-03-31 01:49:53 +02:00
parent cdac82dbef
commit 1ad3ba4e69
7 changed files with 131 additions and 6 deletions

View File

@@ -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<f32>,

View File

@@ -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<qint64>(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)

View File

@@ -55,7 +55,9 @@ private:
QobuzBackend *m_backend = nullptr;
PlayQueue *m_queue = nullptr;
QVector<QPair<qint64, QString>> m_userPlaylists;
QSet<QString> m_favAlbumIds;
QSet<qint64> m_favArtistIds;
bool m_showFavAlbumsOnLoad = false;
bool m_showFavArtistsOnLoad = false;
MainToolBar *m_toolBar = nullptr;
MainContent *m_content = nullptr;

View File

@@ -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();

View File

@@ -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<qint64>(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<qint64> &ids)
m_artistView->setFavArtistIds(ids);
}
void MainContent::setFavAlbumIds(const QSet<QString> &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))

View File

@@ -14,6 +14,7 @@
#include <QStackedWidget>
#include <QJsonObject>
#include <QJsonArray>
#include <QSet>
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<QString> &ids);
void setFavArtistIds(const QSet<qint64> &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<QString> m_favAlbumIds;
};

View File

@@ -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<qint64>(album["id"].toDouble()));
m_artistId = static_cast<qint64>(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;