2 Commits

Author SHA1 Message Date
joren
ecaee4d907 fix: sorting never changes the queue
Removed the sortApplied→syncQueueToModel connection. Sorting the track
list only reorders the view; the queue is only set when a track is
actually played (double-click or Play Now), at which point the current
sorted order and clicked row are used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 01:01:18 +01:00
joren
d5dedacc36 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>
2026-03-24 00:59:02 +01:00
13 changed files with 258 additions and 30 deletions

View File

@@ -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?;

View File

@@ -117,6 +117,7 @@ pub struct ArtistDto {
pub albums_count: Option<i32>,
pub image: Option<ImageDto>,
pub biography: Option<BiographyDto>,
pub albums: Option<SearchResultItems<AlbumDto>>,
}
#[derive(Debug, Deserialize, Clone, Serialize)]

View File

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

View File

@@ -35,10 +35,6 @@ Tracks::Tracks(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
connect(this, &QTreeView::customContextMenuRequested,
this, &Tracks::onContextMenu);
// Re-sync queue order whenever the model is re-sorted (user clicked header
// or setTracks re-applied an existing sort).
connect(m_model, &TrackListModel::sortApplied,
this, &Tracks::syncQueueToModel);
}
void Tracks::loadTracks(const QJsonArray &tracks)
@@ -65,14 +61,9 @@ void Tracks::loadSearchTracks(const QJsonArray &tracks)
void Tracks::setPlayingTrackId(qint64 id)
{
m_playingId = id;
m_model->setPlayingId(id);
}
void Tracks::syncQueueToModel()
{
m_queue->reorderContext(m_model->currentTracksJson(), m_playingId);
}
void Tracks::onDoubleClicked(const QModelIndex &index)
{

View File

@@ -32,10 +32,8 @@ namespace List
TrackListModel *m_model = nullptr;
QobuzBackend *m_backend = nullptr;
PlayQueue *m_queue = nullptr;
qint64 m_playingId = 0;
void onDoubleClicked(const QModelIndex &index);
void onContextMenu(const QPoint &pos);
void syncQueueToModel();
};
}

View File

@@ -65,6 +65,7 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
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::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);
@@ -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)

View File

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

View 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);
};

View 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
View 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;
};

View File

@@ -20,11 +20,21 @@ MainContent::MainContent(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
m_welcome->setAlignment(Qt::AlignCenter);
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_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);
}

View File

@@ -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;
AlbumListView *m_albumList = nullptr;
ArtistListView *m_artistList = nullptr;
ArtistView *m_artistView = nullptr;
};

View File

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