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:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 <QWidget>
|
||||
#include <QLabel>
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user