From d5dedacc363558af0bf35c3172eb95cae7c9b87b Mon Sep 17 00:00:00 2001 From: joren Date: Tue, 24 Mar 2026 00:59:02 +0100 Subject: [PATCH] feat: album list, artist list, and artist detail views - Fav albums: now shows a sortable list (title/artist/year/tracks); double-click opens the album - Fav artists: now shows a sortable list; double-click opens the artist - Artist detail page: name, biography summary, and their album list - Rust ArtistDto gains albums field; get_artist fixed to extra=albums only - Volume popup minimum width set so "100%" label is never clipped Co-Authored-By: Claude Sonnet 4.6 --- rust/src/api/client.rs | 2 +- rust/src/api/models.rs | 1 + src/CMakeLists.txt | 3 ++ src/mainwindow.cpp | 30 ++++++++++++----- src/mainwindow.hpp | 1 + src/view/albumlistview.hpp | 61 +++++++++++++++++++++++++++++++++++ src/view/artistlistview.hpp | 53 ++++++++++++++++++++++++++++++ src/view/artistview.hpp | 64 +++++++++++++++++++++++++++++++++++++ src/view/maincontent.cpp | 34 ++++++++++++++++++-- src/view/maincontent.hpp | 21 +++++++++--- src/widget/volumebutton.hpp | 1 + 11 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 src/view/albumlistview.hpp create mode 100644 src/view/artistlistview.hpp create mode 100644 src/view/artistview.hpp diff --git a/rust/src/api/client.rs b/rust/src/api/client.rs index 0dc7f05..e2defed 100644 --- a/rust/src/api/client.rs +++ b/rust/src/api/client.rs @@ -248,7 +248,7 @@ impl QobuzClient { .get_request("artist/get") .query(&[ ("artist_id", artist_id.to_string()), - ("extra", "albums,biography".to_string()), + ("extra", "albums".to_string()), ]) .send() .await?; diff --git a/rust/src/api/models.rs b/rust/src/api/models.rs index 5049ce8..a2c9f55 100644 --- a/rust/src/api/models.rs +++ b/rust/src/api/models.rs @@ -117,6 +117,7 @@ pub struct ArtistDto { pub albums_count: Option, pub image: Option, pub biography: Option, + pub albums: Option>, } #[derive(Debug, Deserialize, Clone, Serialize)] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dc87068..f3d1063 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,9 @@ target_sources(qobuz-qt PRIVATE view/context/view.cpp view/queuepanel.hpp view/queuepanel.cpp + view/albumlistview.hpp + view/artistlistview.hpp + view/artistview.hpp view/sidepanel/view.hpp view/sidepanel/view.cpp diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6d8c9dc..7e8c610 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -63,10 +63,11 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) // ---- Backend signals ---- connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess); connect(m_backend, &QobuzBackend::loginError, this, &MainWindow::onLoginError); - connect(m_backend, &QobuzBackend::favTracksLoaded, this, &MainWindow::onFavTracksLoaded); - connect(m_backend, &QobuzBackend::favAlbumsLoaded, this, &MainWindow::onFavAlbumsLoaded); - connect(m_backend, &QobuzBackend::albumLoaded, this, &MainWindow::onAlbumLoaded); - connect(m_backend, &QobuzBackend::artistLoaded, this, &MainWindow::onArtistLoaded); + connect(m_backend, &QobuzBackend::favTracksLoaded, this, &MainWindow::onFavTracksLoaded); + connect(m_backend, &QobuzBackend::favAlbumsLoaded, this, &MainWindow::onFavAlbumsLoaded); + connect(m_backend, &QobuzBackend::favArtistsLoaded, this, &MainWindow::onFavArtistsLoaded); + connect(m_backend, &QobuzBackend::albumLoaded, this, &MainWindow::onAlbumLoaded); + connect(m_backend, &QobuzBackend::artistLoaded, this, &MainWindow::onArtistLoaded); connect(m_backend, &QobuzBackend::playlistLoaded, this, &MainWindow::onPlaylistLoaded); connect(m_backend, &QobuzBackend::trackChanged, this, &MainWindow::onTrackChanged); connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) { @@ -104,6 +105,12 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) connect(m_sidePanel, &SidePanel::View::trackPlayRequested, this, &MainWindow::onPlayTrackRequested); + // ---- Album / artist navigation from content views ---- + connect(m_content, &MainContent::albumRequested, + this, &MainWindow::onSearchAlbumSelected); + connect(m_content, &MainContent::artistRequested, + this, &MainWindow::onSearchArtistSelected); + // ---- Toolbar toggles ---- connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled); connect(m_toolBar, &MainToolBar::queueToggled, @@ -224,9 +231,16 @@ void MainWindow::onFavTracksLoaded(const QJsonObject &result) void MainWindow::onFavAlbumsLoaded(const QJsonObject &result) { - const int total = result["total"].toInt(); - statusBar()->showMessage(tr("%1 favorite albums").arg(total), 4000); - // TODO: album grid view + m_content->showFavAlbums(result); + statusBar()->showMessage( + tr("%1 favorite albums").arg(result["total"].toInt()), 4000); +} + +void MainWindow::onFavArtistsLoaded(const QJsonObject &result) +{ + m_content->showFavArtists(result); + statusBar()->showMessage( + tr("%1 favorite artists").arg(result["total"].toInt()), 4000); } void MainWindow::onAlbumLoaded(const QJsonObject &album) @@ -238,9 +252,9 @@ void MainWindow::onAlbumLoaded(const QJsonObject &album) void MainWindow::onArtistLoaded(const QJsonObject &artist) { + m_content->showArtist(artist); statusBar()->showMessage( tr("Artist: %1").arg(artist["name"].toString()), 4000); - // TODO: artist view } void MainWindow::onPlaylistLoaded(const QJsonObject &playlist) diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 583deda..243596c 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -27,6 +27,7 @@ private slots: void onFavTracksLoaded(const QJsonObject &result); void onFavAlbumsLoaded(const QJsonObject &result); + void onFavArtistsLoaded(const QJsonObject &result); void onAlbumLoaded(const QJsonObject &album); void onArtistLoaded(const QJsonObject &artist); void onPlaylistLoaded(const QJsonObject &playlist); diff --git a/src/view/albumlistview.hpp b/src/view/albumlistview.hpp new file mode 100644 index 0000000..2ddc8a0 --- /dev/null +++ b/src/view/albumlistview.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include + +/// A simple list of albums (used for fav albums and artist detail pages). +/// Double-clicking an item emits albumSelected(albumId). +class AlbumListView : public QTreeWidget +{ + Q_OBJECT + +public: + explicit AlbumListView(QWidget *parent = nullptr) : QTreeWidget(parent) + { + setColumnCount(4); + setHeaderLabels({tr("Title"), tr("Artist"), tr("Year"), tr("Tracks")}); + setRootIsDecorated(false); + setAlternatingRowColors(true); + setSelectionBehavior(QAbstractItemView::SelectRows); + setSortingEnabled(true); + + header()->setStretchLastSection(false); + header()->setSectionResizeMode(0, QHeaderView::Stretch); + header()->setSectionResizeMode(1, QHeaderView::Stretch); + header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + + connect(this, &QTreeWidget::itemDoubleClicked, + this, [this](QTreeWidgetItem *item, int) { + const QString id = item->data(0, Qt::UserRole).toString(); + if (!id.isEmpty()) emit albumSelected(id); + }); + } + + void setAlbums(const QJsonArray &albums) + { + clear(); + for (const auto &v : albums) { + const QJsonObject a = v.toObject(); + const QString id = a["id"].toString(); + const QString title = a["title"].toString(); + const QString artist = a["artist"].toObject()["name"].toString(); + const QString date = a["release_date_original"].toString(); + const QString year = date.left(4); + const int tracks = a["tracks_count"].toInt(); + + auto *item = new QTreeWidgetItem(this); + item->setText(0, title); + item->setText(1, artist); + item->setText(2, year); + item->setText(3, tracks > 0 ? QString::number(tracks) : QString()); + item->setData(0, Qt::UserRole, id); + } + } + +signals: + void albumSelected(const QString &albumId); +}; diff --git a/src/view/artistlistview.hpp b/src/view/artistlistview.hpp new file mode 100644 index 0000000..d5bbb36 --- /dev/null +++ b/src/view/artistlistview.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +/// A simple list of artists. +/// Double-clicking an item emits artistSelected(artistId). +class ArtistListView : public QTreeWidget +{ + Q_OBJECT + +public: + explicit ArtistListView(QWidget *parent = nullptr) : QTreeWidget(parent) + { + setColumnCount(2); + setHeaderLabels({tr("Artist"), tr("Albums")}); + setRootIsDecorated(false); + setAlternatingRowColors(true); + setSelectionBehavior(QAbstractItemView::SelectRows); + + header()->setStretchLastSection(false); + header()->setSectionResizeMode(0, QHeaderView::Stretch); + header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + connect(this, &QTreeWidget::itemDoubleClicked, + this, [this](QTreeWidgetItem *item, int) { + const qint64 id = item->data(0, Qt::UserRole).toLongLong(); + if (id > 0) emit artistSelected(id); + }); + } + + void setArtists(const QJsonArray &artists) + { + clear(); + for (const auto &v : artists) { + const QJsonObject a = v.toObject(); + const qint64 id = static_cast(a["id"].toDouble()); + const QString name = a["name"].toString(); + const int albums = a["albums_count"].toInt(); + + auto *item = new QTreeWidgetItem(this); + item->setText(0, name); + item->setText(1, albums > 0 ? QString::number(albums) : QString()); + item->setData(0, Qt::UserRole, id); + } + } + +signals: + void artistSelected(qint64 artistId); +}; diff --git a/src/view/artistview.hpp b/src/view/artistview.hpp new file mode 100644 index 0000000..8df1589 --- /dev/null +++ b/src/view/artistview.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "albumlistview.hpp" + +#include +#include +#include +#include +#include +#include + +/// Artist detail page: name, biography summary, and their album list. +class ArtistView : public QWidget +{ + Q_OBJECT + +public: + explicit ArtistView(QWidget *parent = nullptr) : QWidget(parent) + { + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(8, 8, 8, 8); + layout->setSpacing(6); + + m_nameLabel = new QLabel(this); + QFont f = m_nameLabel->font(); + f.setPointSize(f.pointSize() + 4); + f.setBold(true); + m_nameLabel->setFont(f); + + m_bioLabel = new QLabel(this); + m_bioLabel->setWordWrap(true); + m_bioLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); + m_bioLabel->setMaximumHeight(80); + + m_albums = new AlbumListView(this); + + layout->addWidget(m_nameLabel); + layout->addWidget(m_bioLabel); + layout->addWidget(m_albums, 1); + + connect(m_albums, &AlbumListView::albumSelected, + this, &ArtistView::albumSelected); + } + + void setArtist(const QJsonObject &artist) + { + m_nameLabel->setText(artist["name"].toString()); + + const QString summary = artist["biography"].toObject()["summary"].toString(); + m_bioLabel->setText(summary); + m_bioLabel->setVisible(!summary.isEmpty()); + + const QJsonArray albums = artist["albums"].toObject()["items"].toArray(); + m_albums->setAlbums(albums); + } + +signals: + void albumSelected(const QString &albumId); + +private: + QLabel *m_nameLabel = nullptr; + QLabel *m_bioLabel = nullptr; + AlbumListView *m_albums = nullptr; +}; diff --git a/src/view/maincontent.cpp b/src/view/maincontent.cpp index 7c6a54b..765d45a 100644 --- a/src/view/maincontent.cpp +++ b/src/view/maincontent.cpp @@ -19,12 +19,22 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren this); m_welcome->setAlignment(Qt::AlignCenter); - m_tracks = new List::Tracks(m_backend, queue, this); + m_tracks = new List::Tracks(m_backend, queue, this); + m_albumList = new AlbumListView(this); + m_artistList = new ArtistListView(this); + m_artistView = new ArtistView(this); - m_stack->addWidget(m_welcome); // 0 - m_stack->addWidget(m_tracks); // 1 + m_stack->addWidget(m_welcome); // 0 + m_stack->addWidget(m_tracks); // 1 + m_stack->addWidget(m_albumList); // 2 + m_stack->addWidget(m_artistList); // 3 + m_stack->addWidget(m_artistView); // 4 m_stack->setCurrentIndex(0); + + connect(m_albumList, &AlbumListView::albumSelected, this, &MainContent::albumRequested); + connect(m_artistList, &ArtistListView::artistSelected, this, &MainContent::artistRequested); + connect(m_artistView, &ArtistView::albumSelected, this, &MainContent::albumRequested); } void MainContent::showWelcome() { m_stack->setCurrentIndex(0); } @@ -52,3 +62,21 @@ void MainContent::showSearchTracks(const QJsonArray &tracks) m_tracks->loadSearchTracks(tracks); m_stack->setCurrentIndex(1); } + +void MainContent::showFavAlbums(const QJsonObject &result) +{ + m_albumList->setAlbums(result["items"].toArray()); + m_stack->setCurrentIndex(2); +} + +void MainContent::showFavArtists(const QJsonObject &result) +{ + m_artistList->setArtists(result["items"].toArray()); + m_stack->setCurrentIndex(3); +} + +void MainContent::showArtist(const QJsonObject &artist) +{ + m_artistView->setArtist(artist); + m_stack->setCurrentIndex(4); +} diff --git a/src/view/maincontent.hpp b/src/view/maincontent.hpp index fc9490b..487c045 100644 --- a/src/view/maincontent.hpp +++ b/src/view/maincontent.hpp @@ -3,6 +3,9 @@ #include "../list/tracks.hpp" #include "../backend/qobuzbackend.hpp" #include "../playqueue.hpp" +#include "albumlistview.hpp" +#include "artistlistview.hpp" +#include "artistview.hpp" #include #include @@ -24,10 +27,20 @@ public: void showPlaylist(const QJsonObject &playlist); void showFavTracks(const QJsonObject &result); void showSearchTracks(const QJsonArray &tracks); + void showFavAlbums(const QJsonObject &result); + void showFavArtists(const QJsonObject &result); + void showArtist(const QJsonObject &artist); + +signals: + void albumRequested(const QString &albumId); + void artistRequested(qint64 artistId); private: - QobuzBackend *m_backend = nullptr; - QStackedWidget *m_stack = nullptr; - QLabel *m_welcome = nullptr; - List::Tracks *m_tracks = nullptr; + QobuzBackend *m_backend = nullptr; + QStackedWidget *m_stack = nullptr; + QLabel *m_welcome = nullptr; + List::Tracks *m_tracks = nullptr; + AlbumListView *m_albumList = nullptr; + ArtistListView *m_artistList = nullptr; + ArtistView *m_artistView = nullptr; }; diff --git a/src/widget/volumebutton.hpp b/src/widget/volumebutton.hpp index 2354190..3a4bef5 100644 --- a/src/widget/volumebutton.hpp +++ b/src/widget/volumebutton.hpp @@ -22,6 +22,7 @@ public: auto *menu = new QMenu(this); auto *widget = new QWidget(menu); + widget->setMinimumWidth(72); auto *layout = new QVBoxLayout(widget); layout->setContentsMargins(6, 6, 6, 6);