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 <noreply@anthropic.com>
This commit is contained in:
@@ -248,7 +248,7 @@ impl QobuzClient {
|
|||||||
.get_request("artist/get")
|
.get_request("artist/get")
|
||||||
.query(&[
|
.query(&[
|
||||||
("artist_id", artist_id.to_string()),
|
("artist_id", artist_id.to_string()),
|
||||||
("extra", "albums,biography".to_string()),
|
("extra", "albums".to_string()),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ pub struct ArtistDto {
|
|||||||
pub albums_count: Option<i32>,
|
pub albums_count: Option<i32>,
|
||||||
pub image: Option<ImageDto>,
|
pub image: Option<ImageDto>,
|
||||||
pub biography: Option<BiographyDto>,
|
pub biography: Option<BiographyDto>,
|
||||||
|
pub albums: Option<SearchResultItems<AlbumDto>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ target_sources(qobuz-qt PRIVATE
|
|||||||
view/context/view.cpp
|
view/context/view.cpp
|
||||||
view/queuepanel.hpp
|
view/queuepanel.hpp
|
||||||
view/queuepanel.cpp
|
view/queuepanel.cpp
|
||||||
|
view/albumlistview.hpp
|
||||||
|
view/artistlistview.hpp
|
||||||
|
view/artistview.hpp
|
||||||
view/sidepanel/view.hpp
|
view/sidepanel/view.hpp
|
||||||
view/sidepanel/view.cpp
|
view/sidepanel/view.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,11 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
|||||||
// ---- Backend signals ----
|
// ---- Backend signals ----
|
||||||
connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess);
|
connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess);
|
||||||
connect(m_backend, &QobuzBackend::loginError, this, &MainWindow::onLoginError);
|
connect(m_backend, &QobuzBackend::loginError, this, &MainWindow::onLoginError);
|
||||||
connect(m_backend, &QobuzBackend::favTracksLoaded, this, &MainWindow::onFavTracksLoaded);
|
connect(m_backend, &QobuzBackend::favTracksLoaded, this, &MainWindow::onFavTracksLoaded);
|
||||||
connect(m_backend, &QobuzBackend::favAlbumsLoaded, this, &MainWindow::onFavAlbumsLoaded);
|
connect(m_backend, &QobuzBackend::favAlbumsLoaded, this, &MainWindow::onFavAlbumsLoaded);
|
||||||
connect(m_backend, &QobuzBackend::albumLoaded, this, &MainWindow::onAlbumLoaded);
|
connect(m_backend, &QobuzBackend::favArtistsLoaded, this, &MainWindow::onFavArtistsLoaded);
|
||||||
connect(m_backend, &QobuzBackend::artistLoaded, this, &MainWindow::onArtistLoaded);
|
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::playlistLoaded, this, &MainWindow::onPlaylistLoaded);
|
||||||
connect(m_backend, &QobuzBackend::trackChanged, this, &MainWindow::onTrackChanged);
|
connect(m_backend, &QobuzBackend::trackChanged, this, &MainWindow::onTrackChanged);
|
||||||
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
|
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,
|
connect(m_sidePanel, &SidePanel::View::trackPlayRequested,
|
||||||
this, &MainWindow::onPlayTrackRequested);
|
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 ----
|
// ---- Toolbar toggles ----
|
||||||
connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled);
|
connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled);
|
||||||
connect(m_toolBar, &MainToolBar::queueToggled,
|
connect(m_toolBar, &MainToolBar::queueToggled,
|
||||||
@@ -224,9 +231,16 @@ void MainWindow::onFavTracksLoaded(const QJsonObject &result)
|
|||||||
|
|
||||||
void MainWindow::onFavAlbumsLoaded(const QJsonObject &result)
|
void MainWindow::onFavAlbumsLoaded(const QJsonObject &result)
|
||||||
{
|
{
|
||||||
const int total = result["total"].toInt();
|
m_content->showFavAlbums(result);
|
||||||
statusBar()->showMessage(tr("%1 favorite albums").arg(total), 4000);
|
statusBar()->showMessage(
|
||||||
// TODO: album grid view
|
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)
|
void MainWindow::onAlbumLoaded(const QJsonObject &album)
|
||||||
@@ -238,9 +252,9 @@ void MainWindow::onAlbumLoaded(const QJsonObject &album)
|
|||||||
|
|
||||||
void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
||||||
{
|
{
|
||||||
|
m_content->showArtist(artist);
|
||||||
statusBar()->showMessage(
|
statusBar()->showMessage(
|
||||||
tr("Artist: %1").arg(artist["name"].toString()), 4000);
|
tr("Artist: %1").arg(artist["name"].toString()), 4000);
|
||||||
// TODO: artist view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ private slots:
|
|||||||
|
|
||||||
void onFavTracksLoaded(const QJsonObject &result);
|
void onFavTracksLoaded(const QJsonObject &result);
|
||||||
void onFavAlbumsLoaded(const QJsonObject &result);
|
void onFavAlbumsLoaded(const QJsonObject &result);
|
||||||
|
void onFavArtistsLoaded(const QJsonObject &result);
|
||||||
void onAlbumLoaded(const QJsonObject &album);
|
void onAlbumLoaded(const QJsonObject &album);
|
||||||
void onArtistLoaded(const QJsonObject &artist);
|
void onArtistLoaded(const QJsonObject &artist);
|
||||||
void onPlaylistLoaded(const QJsonObject &playlist);
|
void onPlaylistLoaded(const QJsonObject &playlist);
|
||||||
|
|||||||
61
src/view/albumlistview.hpp
Normal file
61
src/view/albumlistview.hpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
};
|
||||||
53
src/view/artistlistview.hpp
Normal file
53
src/view/artistlistview.hpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QTreeWidgetItem>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
/// 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<qint64>(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);
|
||||||
|
};
|
||||||
64
src/view/artistview.hpp
Normal file
64
src/view/artistview.hpp
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "albumlistview.hpp"
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
};
|
||||||
@@ -19,12 +19,22 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
this);
|
this);
|
||||||
m_welcome->setAlignment(Qt::AlignCenter);
|
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_welcome); // 0
|
||||||
m_stack->addWidget(m_tracks); // 1
|
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);
|
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); }
|
void MainContent::showWelcome() { m_stack->setCurrentIndex(0); }
|
||||||
@@ -52,3 +62,21 @@ void MainContent::showSearchTracks(const QJsonArray &tracks)
|
|||||||
m_tracks->loadSearchTracks(tracks);
|
m_tracks->loadSearchTracks(tracks);
|
||||||
m_stack->setCurrentIndex(1);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#include "../list/tracks.hpp"
|
#include "../list/tracks.hpp"
|
||||||
#include "../backend/qobuzbackend.hpp"
|
#include "../backend/qobuzbackend.hpp"
|
||||||
#include "../playqueue.hpp"
|
#include "../playqueue.hpp"
|
||||||
|
#include "albumlistview.hpp"
|
||||||
|
#include "artistlistview.hpp"
|
||||||
|
#include "artistview.hpp"
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
@@ -24,10 +27,20 @@ public:
|
|||||||
void showPlaylist(const QJsonObject &playlist);
|
void showPlaylist(const QJsonObject &playlist);
|
||||||
void showFavTracks(const QJsonObject &result);
|
void showFavTracks(const QJsonObject &result);
|
||||||
void showSearchTracks(const QJsonArray &tracks);
|
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:
|
private:
|
||||||
QobuzBackend *m_backend = nullptr;
|
QobuzBackend *m_backend = nullptr;
|
||||||
QStackedWidget *m_stack = nullptr;
|
QStackedWidget *m_stack = nullptr;
|
||||||
QLabel *m_welcome = nullptr;
|
QLabel *m_welcome = nullptr;
|
||||||
List::Tracks *m_tracks = nullptr;
|
List::Tracks *m_tracks = nullptr;
|
||||||
|
AlbumListView *m_albumList = nullptr;
|
||||||
|
ArtistListView *m_artistList = nullptr;
|
||||||
|
ArtistView *m_artistView = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public:
|
|||||||
|
|
||||||
auto *menu = new QMenu(this);
|
auto *menu = new QMenu(this);
|
||||||
auto *widget = new QWidget(menu);
|
auto *widget = new QWidget(menu);
|
||||||
|
widget->setMinimumWidth(72);
|
||||||
auto *layout = new QVBoxLayout(widget);
|
auto *layout = new QVBoxLayout(widget);
|
||||||
layout->setContentsMargins(6, 6, 6, 6);
|
layout->setContentsMargins(6, 6, 6, 6);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user