feat: artist sections, fav indicator, art scaling fix, volume popup fix
- Artist profile: collapsible Albums / EPs & Singles / Other sections keyed on release_type; fetches up to 200 albums per artist - Favorites: starred icon on favorited tracks, context menu shows Add or Remove (not both); IDs cached when fav tracks are loaded - Shuffle button: one-time shuffle via shuffleNow() without touching global shuffle flag, so double-click still plays in order - Now-playing art: replaced setFixedHeight hack with ArtWidget that overrides hasHeightForWidth() — scales smoothly up and down, no min-size - Volume popup: replaced QMenu (laggy, broken drag) with Qt::Popup QFrame; appears below button; fixed size locked at 100% label width Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -258,6 +258,7 @@ impl QobuzClient {
|
||||
.query(&[
|
||||
("artist_id", artist_id.to_string()),
|
||||
("extra", "albums".to_string()),
|
||||
("albums_limit", "200".to_string()),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -98,6 +98,7 @@ pub struct AlbumDto {
|
||||
pub maximum_sampling_rate: Option<f64>,
|
||||
pub hires_streamable: Option<bool>,
|
||||
pub streamable: Option<bool>,
|
||||
pub release_type: Option<String>,
|
||||
pub tracks: Option<TracksWrapper>,
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ target_sources(qobuz-qt PRIVATE
|
||||
view/albumlistview.hpp
|
||||
view/artistlistview.hpp
|
||||
view/artistview.hpp
|
||||
view/artistview.cpp
|
||||
view/sidepanel/view.hpp
|
||||
view/sidepanel/view.cpp
|
||||
|
||||
|
||||
@@ -84,12 +84,30 @@ void Tracks::setPlayingTrackId(qint64 id)
|
||||
m_model->setPlayingId(id);
|
||||
}
|
||||
|
||||
void Tracks::setFavTrackIds(const QSet<qint64> &ids)
|
||||
{
|
||||
m_model->setFavIds(ids);
|
||||
}
|
||||
|
||||
void Tracks::addFavTrackId(qint64 id)
|
||||
{
|
||||
m_model->addFavId(id);
|
||||
}
|
||||
|
||||
void Tracks::removeFavTrackId(qint64 id)
|
||||
{
|
||||
m_model->removeFavId(id);
|
||||
}
|
||||
|
||||
void Tracks::playAll(bool shuffle)
|
||||
{
|
||||
const QJsonArray tracks = m_model->currentTracksJson();
|
||||
if (tracks.isEmpty()) return;
|
||||
m_queue->setShuffle(shuffle);
|
||||
m_queue->setContext(tracks, 0);
|
||||
// Shuffle once without touching the global shuffle flag — so a subsequent
|
||||
// double-click on a track plays in normal order (unless global shuffle is on).
|
||||
if (shuffle && !m_queue->shuffleEnabled())
|
||||
m_queue->shuffleNow();
|
||||
const qint64 firstId = static_cast<qint64>(m_queue->current()["id"].toDouble());
|
||||
if (firstId > 0)
|
||||
emit playTrackRequested(firstId);
|
||||
@@ -119,8 +137,21 @@ void Tracks::onContextMenu(const QPoint &pos)
|
||||
auto *playNext = menu.addAction(QIcon(":/res/icons/media-skip-forward.svg"), tr("Play next"));
|
||||
auto *addQueue = menu.addAction(QIcon(":/res/icons/media-playlist-append.svg"), tr("Add to queue"));
|
||||
menu.addSeparator();
|
||||
auto *addFav = menu.addAction(QIcon(":/res/icons/starred-symbolic.svg"), tr("Add to favorites"));
|
||||
auto *remFav = menu.addAction(QIcon(":/res/icons/non-starred-symbolic.svg"), tr("Remove from favorites"));
|
||||
|
||||
const bool isFav = m_model->isFav(id);
|
||||
if (isFav) {
|
||||
auto *remFav = menu.addAction(QIcon(":/res/icons/non-starred-symbolic.svg"), tr("Remove from favorites"));
|
||||
connect(remFav, &QAction::triggered, this, [this, id] {
|
||||
m_backend->removeFavTrack(id);
|
||||
m_model->removeFavId(id);
|
||||
});
|
||||
} else {
|
||||
auto *addFav = menu.addAction(QIcon(":/res/icons/starred-symbolic.svg"), tr("Add to favorites"));
|
||||
connect(addFav, &QAction::triggered, this, [this, id] {
|
||||
m_backend->addFavTrack(id);
|
||||
m_model->addFavId(id);
|
||||
});
|
||||
}
|
||||
|
||||
const int row = index.row();
|
||||
connect(playNow, &QAction::triggered, this, [this, id, row] {
|
||||
@@ -133,12 +164,6 @@ void Tracks::onContextMenu(const QPoint &pos)
|
||||
connect(addQueue, &QAction::triggered, this, [this, trackJson] {
|
||||
m_queue->addToQueue(trackJson);
|
||||
});
|
||||
connect(addFav, &QAction::triggered, this, [this, id] {
|
||||
m_backend->addFavTrack(id);
|
||||
});
|
||||
connect(remFav, &QAction::triggered, this, [this, id] {
|
||||
m_backend->removeFavTrack(id);
|
||||
});
|
||||
|
||||
// Open album
|
||||
const QString albumId = m_model->trackAt(index.row()).albumId;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QVector>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QSet>
|
||||
|
||||
namespace List
|
||||
{
|
||||
@@ -28,6 +29,11 @@ namespace List
|
||||
/// Called when the backend fires EV_TRACK_CHANGED so the playing row is highlighted.
|
||||
void setPlayingTrackId(qint64 id);
|
||||
|
||||
/// Populate favorite track IDs so the star indicator and context menu reflect fav status.
|
||||
void setFavTrackIds(const QSet<qint64> &ids);
|
||||
void addFavTrackId(qint64 id);
|
||||
void removeFavTrackId(qint64 id);
|
||||
|
||||
/// Start playing all tracks in the current view from the beginning.
|
||||
/// If shuffle is true, enables shuffle mode before starting.
|
||||
void playAll(bool shuffle = false);
|
||||
|
||||
@@ -283,6 +283,15 @@ void MainWindow::onTrackChanged(const QJsonObject &track)
|
||||
|
||||
void MainWindow::onFavTracksLoaded(const QJsonObject &result)
|
||||
{
|
||||
// Cache fav IDs so the star indicator and context menu stay in sync
|
||||
QSet<qint64> ids;
|
||||
const QJsonArray items = result["items"].toArray();
|
||||
for (const QJsonValue &v : items) {
|
||||
const qint64 id = static_cast<qint64>(v.toObject()["id"].toDouble());
|
||||
if (id > 0) ids.insert(id);
|
||||
}
|
||||
m_content->tracksList()->setFavTrackIds(ids);
|
||||
|
||||
m_content->showFavTracks(result);
|
||||
statusBar()->showMessage(
|
||||
tr("%1 favorite tracks").arg(result["total"].toInt()), 4000);
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
#include <QMainWindow>
|
||||
#include <QDockWidget>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVector>
|
||||
#include <QSet>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
|
||||
|
||||
@@ -77,6 +77,36 @@ void TrackListModel::removeTrack(int row)
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void TrackListModel::setFavIds(const QSet<qint64> &ids)
|
||||
{
|
||||
m_favIds = ids;
|
||||
if (!m_tracks.isEmpty())
|
||||
emit dataChanged(index(0, ColTitle), index(rowCount() - 1, ColTitle),
|
||||
{Qt::DecorationRole});
|
||||
}
|
||||
|
||||
void TrackListModel::addFavId(qint64 id)
|
||||
{
|
||||
m_favIds.insert(id);
|
||||
for (int r = 0; r < m_tracks.size(); ++r) {
|
||||
if (m_tracks[r].id == id) {
|
||||
const auto idx = index(r, ColTitle);
|
||||
emit dataChanged(idx, idx, {Qt::DecorationRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrackListModel::removeFavId(qint64 id)
|
||||
{
|
||||
m_favIds.remove(id);
|
||||
for (int r = 0; r < m_tracks.size(); ++r) {
|
||||
if (m_tracks[r].id == id) {
|
||||
const auto idx = index(r, ColTitle);
|
||||
emit dataChanged(idx, idx, {Qt::DecorationRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrackListModel::setPlayingId(qint64 id)
|
||||
{
|
||||
m_playingId = id;
|
||||
@@ -128,6 +158,10 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
|
||||
return QIcon(QStringLiteral(":/res/icons/media-track-show-active.svg"));
|
||||
}
|
||||
|
||||
if (role == Qt::DecorationRole && index.column() == ColTitle && m_favIds.contains(t.id)) {
|
||||
return QIcon(QStringLiteral(":/res/icons/starred-symbolic.svg"));
|
||||
}
|
||||
|
||||
if (role == TrackIdRole) return t.id;
|
||||
if (role == TrackJsonRole) return t.raw;
|
||||
if (role == HiResRole) return t.hiRes;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QVector>
|
||||
#include <QSet>
|
||||
#include <QFont>
|
||||
|
||||
struct TrackItem {
|
||||
@@ -52,6 +53,11 @@ public:
|
||||
void setPlayingId(qint64 id);
|
||||
qint64 playingId() const { return m_playingId; }
|
||||
|
||||
void setFavIds(const QSet<qint64> &ids);
|
||||
void addFavId(qint64 id);
|
||||
void removeFavId(qint64 id);
|
||||
bool isFav(qint64 id) const { return m_favIds.contains(id); }
|
||||
|
||||
/// Optimistically remove a row (e.g. after deleting from playlist).
|
||||
void removeTrack(int row);
|
||||
|
||||
@@ -81,6 +87,7 @@ signals:
|
||||
|
||||
private:
|
||||
QVector<TrackItem> m_tracks;
|
||||
QSet<qint64> m_favIds;
|
||||
qint64 m_playingId = 0;
|
||||
int m_sortColumn = -1;
|
||||
Qt::SortOrder m_sortOrder = Qt::AscendingOrder;
|
||||
|
||||
@@ -107,6 +107,14 @@ public:
|
||||
emit queueChanged();
|
||||
}
|
||||
|
||||
/// Shuffle the current queue once without changing the global shuffle flag.
|
||||
void shuffleNow()
|
||||
{
|
||||
if (m_queue.isEmpty()) return;
|
||||
shuffleQueue(m_index);
|
||||
emit queueChanged();
|
||||
}
|
||||
|
||||
// ---- Play-next prepend queue (like "Add to queue" ----
|
||||
|
||||
void addToQueue(const QJsonObject &track)
|
||||
|
||||
153
src/view/artistview.cpp
Normal file
153
src/view/artistview.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "artistview.hpp"
|
||||
#include "albumlistview.hpp"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QFont>
|
||||
#include <QJsonValue>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ArtistSection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ArtistSection::ArtistSection(const QString &title, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_baseTitle(title)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_toggle = new QToolButton(this);
|
||||
m_toggle->setCheckable(true);
|
||||
m_toggle->setChecked(true);
|
||||
m_toggle->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
m_toggle->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
m_toggle->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { text-align: left; font-weight: bold; padding: 4px 6px;"
|
||||
" border: none; border-bottom: 1px solid #333; }"
|
||||
"QToolButton:hover { background: #1e1e1e; }"
|
||||
));
|
||||
updateToggleText(0);
|
||||
layout->addWidget(m_toggle);
|
||||
|
||||
m_list = new AlbumListView(this);
|
||||
layout->addWidget(m_list);
|
||||
|
||||
connect(m_toggle, &QToolButton::toggled, m_list, &AlbumListView::setVisible);
|
||||
connect(m_list, &AlbumListView::albumSelected, this, &ArtistSection::albumSelected);
|
||||
}
|
||||
|
||||
void ArtistSection::setAlbums(const QJsonArray &albums)
|
||||
{
|
||||
m_list->setAlbums(albums);
|
||||
updateToggleText(albums.size());
|
||||
}
|
||||
|
||||
bool ArtistSection::isEmpty() const
|
||||
{
|
||||
return m_list->topLevelItemCount() == 0;
|
||||
}
|
||||
|
||||
void ArtistSection::updateToggleText(int count)
|
||||
{
|
||||
const QString arrow = m_toggle->isChecked() ? QStringLiteral("▼ ") : QStringLiteral("▶ ");
|
||||
const QString text = count > 0
|
||||
? QStringLiteral("%1%2 (%3)").arg(arrow, m_baseTitle).arg(count)
|
||||
: arrow + m_baseTitle;
|
||||
m_toggle->setText(text);
|
||||
|
||||
// Keep arrow in sync when toggled
|
||||
disconnect(m_toggle, &QToolButton::toggled, nullptr, nullptr);
|
||||
connect(m_toggle, &QToolButton::toggled, m_list, &AlbumListView::setVisible);
|
||||
connect(m_toggle, &QToolButton::toggled, this, [this, count](bool open) {
|
||||
const QString a = open ? QStringLiteral("▼ ") : QStringLiteral("▶ ");
|
||||
const QString t = count > 0
|
||||
? QStringLiteral("%1%2 (%3)").arg(a, m_baseTitle).arg(count)
|
||||
: a + m_baseTitle;
|
||||
m_toggle->setText(t);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ArtistView
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ArtistView::ArtistView(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto *outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(8, 8, 8, 8);
|
||||
outerLayout->setSpacing(6);
|
||||
|
||||
m_nameLabel = new QLabel(this);
|
||||
QFont f = m_nameLabel->font();
|
||||
f.setPointSize(f.pointSize() + 4);
|
||||
f.setBold(true);
|
||||
m_nameLabel->setFont(f);
|
||||
outerLayout->addWidget(m_nameLabel);
|
||||
|
||||
m_bioLabel = new QLabel(this);
|
||||
m_bioLabel->setWordWrap(true);
|
||||
m_bioLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
m_bioLabel->setMaximumHeight(80);
|
||||
outerLayout->addWidget(m_bioLabel);
|
||||
|
||||
// Scrollable sections area
|
||||
auto *scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto *content = new QWidget(scroll);
|
||||
auto *sectLayout = new QVBoxLayout(content);
|
||||
sectLayout->setContentsMargins(0, 0, 0, 0);
|
||||
sectLayout->setSpacing(8);
|
||||
|
||||
m_secAlbums = new ArtistSection(tr("Albums"), content);
|
||||
m_secEps = new ArtistSection(tr("EPs & Singles"), content);
|
||||
m_secOther = new ArtistSection(tr("Other"), content);
|
||||
|
||||
sectLayout->addWidget(m_secAlbums);
|
||||
sectLayout->addWidget(m_secEps);
|
||||
sectLayout->addWidget(m_secOther);
|
||||
sectLayout->addStretch();
|
||||
|
||||
scroll->setWidget(content);
|
||||
outerLayout->addWidget(scroll, 1);
|
||||
|
||||
connect(m_secAlbums, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
|
||||
connect(m_secEps, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
|
||||
connect(m_secOther, &ArtistSection::albumSelected, this, &ArtistView::albumSelected);
|
||||
}
|
||||
|
||||
void ArtistView::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 allAlbums = artist["albums"].toObject()["items"].toArray();
|
||||
|
||||
QJsonArray albums, eps, other;
|
||||
for (const QJsonValue &v : allAlbums) {
|
||||
const QJsonObject a = v.toObject();
|
||||
const QString rt = a["release_type"].toString();
|
||||
if (rt == QStringLiteral("album"))
|
||||
albums.append(a);
|
||||
else if (rt == QStringLiteral("epSingle"))
|
||||
eps.append(a);
|
||||
else
|
||||
other.append(a);
|
||||
}
|
||||
|
||||
m_secAlbums->setAlbums(albums);
|
||||
m_secEps->setAlbums(eps);
|
||||
m_secOther->setAlbums(other);
|
||||
|
||||
m_secAlbums->setVisible(!m_secAlbums->isEmpty());
|
||||
m_secEps->setVisible(!m_secEps->isEmpty());
|
||||
m_secOther->setVisible(!m_secOther->isEmpty());
|
||||
}
|
||||
@@ -3,62 +3,52 @@
|
||||
#include "albumlistview.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QFont>
|
||||
#include <QToolButton>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
/// Artist detail page: name, biography summary, and their album list.
|
||||
class ArtistView : public QWidget
|
||||
class AlbumListView;
|
||||
|
||||
/// One collapsible section (e.g. "Albums", "EPs & Singles") inside ArtistView.
|
||||
class ArtistSection : 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);
|
||||
explicit ArtistSection(const QString &title, QWidget *parent = nullptr);
|
||||
|
||||
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);
|
||||
}
|
||||
void setAlbums(const QJsonArray &albums);
|
||||
bool isEmpty() const;
|
||||
|
||||
signals:
|
||||
void albumSelected(const QString &albumId);
|
||||
|
||||
private:
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
QLabel *m_bioLabel = nullptr;
|
||||
AlbumListView *m_albums = nullptr;
|
||||
QString m_baseTitle;
|
||||
QToolButton *m_toggle = nullptr;
|
||||
AlbumListView *m_list = nullptr;
|
||||
|
||||
void updateToggleText(int count);
|
||||
};
|
||||
|
||||
/// Artist detail page: name, biography, and albums split into collapsible sections
|
||||
/// (Albums / EPs & Singles / Other) keyed on the release_type field.
|
||||
class ArtistView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArtistView(QWidget *parent = nullptr);
|
||||
|
||||
void setArtist(const QJsonObject &artist);
|
||||
|
||||
signals:
|
||||
void albumSelected(const QString &albumId);
|
||||
|
||||
private:
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
QLabel *m_bioLabel = nullptr;
|
||||
ArtistSection *m_secAlbums = nullptr;
|
||||
ArtistSection *m_secEps = nullptr;
|
||||
ArtistSection *m_secOther = nullptr;
|
||||
};
|
||||
|
||||
@@ -22,11 +22,7 @@ View::View(QobuzBackend *backend, QWidget *parent)
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_albumArt = new QLabel(container);
|
||||
m_albumArt->setAlignment(Qt::AlignCenter);
|
||||
m_albumArt->setStyleSheet(QStringLiteral(
|
||||
"background: #1a1a1a; border-radius: 4px;"));
|
||||
m_albumArt->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
m_albumArt = new ArtWidget(container);
|
||||
layout->addWidget(m_albumArt);
|
||||
|
||||
m_title = new QLabel(tr("Not playing"), container);
|
||||
@@ -45,7 +41,6 @@ View::View(QobuzBackend *backend, QWidget *parent)
|
||||
|
||||
layout->addStretch();
|
||||
setWidget(container);
|
||||
setMinimumWidth(160);
|
||||
|
||||
connect(m_backend, &QobuzBackend::trackChanged, this, &View::onTrackChanged);
|
||||
}
|
||||
@@ -60,7 +55,6 @@ void View::onTrackChanged(const QJsonObject &track)
|
||||
m_title->setText(title.isEmpty() ? tr("Not playing") : title);
|
||||
m_artist->setText(artist);
|
||||
|
||||
// Prefer "large" image, fall back to "small"
|
||||
const QJsonObject img = track["album"].toObject()["image"].toObject();
|
||||
QString artUrl = img["large"].toString();
|
||||
if (artUrl.isEmpty())
|
||||
@@ -77,26 +71,9 @@ void View::onArtReady(QNetworkReply *reply)
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
return;
|
||||
if (m_artPixmap.loadFromData(reply->readAll()))
|
||||
scaleArtToWidth();
|
||||
}
|
||||
|
||||
void View::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QDockWidget::resizeEvent(event);
|
||||
if (m_artPixmap.isNull()) return;
|
||||
// Use the new dock width from the event so we don't lag behind the layout
|
||||
const int side = qMax(32, event->size().width() - 16);
|
||||
m_albumArt->setFixedHeight(side);
|
||||
m_albumArt->setPixmap(m_artPixmap.scaled(side, side, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
void View::scaleArtToWidth()
|
||||
{
|
||||
if (m_artPixmap.isNull()) return;
|
||||
const int side = qMax(32, width() - 16);
|
||||
m_albumArt->setFixedHeight(side);
|
||||
m_albumArt->setPixmap(m_artPixmap.scaled(side, side, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
QPixmap pix;
|
||||
if (pix.loadFromData(reply->readAll()))
|
||||
m_albumArt->setPixmap(pix);
|
||||
}
|
||||
|
||||
} // namespace Context
|
||||
|
||||
@@ -3,15 +3,49 @@
|
||||
#include "../../backend/qobuzbackend.hpp"
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QResizeEvent>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace Context
|
||||
{
|
||||
/// Square art widget: always as wide as its parent allows, height follows width.
|
||||
class ArtWidget : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit ArtWidget(QWidget *parent = nullptr) : QWidget(parent)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
}
|
||||
|
||||
void setPixmap(const QPixmap &px) { m_pix = px; update(); }
|
||||
bool hasHeightForWidth() const override { return true; }
|
||||
int heightForWidth(int w) const override { return w; }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter p(this);
|
||||
if (m_pix.isNull()) {
|
||||
p.fillRect(rect(), QColor(0x1a, 0x1a, 0x1a));
|
||||
return;
|
||||
}
|
||||
const QPixmap scaled = m_pix.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
p.fillRect(rect(), QColor(0x1a, 0x1a, 0x1a));
|
||||
p.drawPixmap((width() - scaled.width()) / 2,
|
||||
(height() - scaled.height()) / 2,
|
||||
scaled);
|
||||
}
|
||||
|
||||
private:
|
||||
QPixmap m_pix;
|
||||
};
|
||||
|
||||
class View : public QDockWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -23,18 +57,12 @@ namespace Context
|
||||
void onTrackChanged(const QJsonObject &track);
|
||||
void onArtReady(QNetworkReply *reply);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
void scaleArtToWidth();
|
||||
|
||||
QobuzBackend *m_backend = nullptr;
|
||||
QLabel *m_albumArt = nullptr;
|
||||
ArtWidget *m_albumArt = nullptr;
|
||||
QLabel *m_title = nullptr;
|
||||
QLabel *m_artist = nullptr;
|
||||
QNetworkAccessManager *m_nam = nullptr;
|
||||
QString m_currentArtUrl;
|
||||
QPixmap m_artPixmap;
|
||||
};
|
||||
} // namespace Context
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
#include "../util/icon.hpp"
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QWidgetAction>
|
||||
#include <QMenu>
|
||||
#include <QLabel>
|
||||
#include <QFrame>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
/// A toolbar button that shows a volume slider popup when clicked.
|
||||
class VolumeButton : public QToolButton
|
||||
@@ -17,33 +16,36 @@ class VolumeButton : public QToolButton
|
||||
public:
|
||||
explicit VolumeButton(QWidget *parent = nullptr) : QToolButton(parent)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setIcon(Icon::volumeHigh());
|
||||
|
||||
auto *menu = new QMenu(this);
|
||||
auto *widget = new QWidget(menu);
|
||||
widget->setMinimumWidth(72);
|
||||
auto *layout = new QVBoxLayout(widget);
|
||||
layout->setContentsMargins(6, 6, 6, 6);
|
||||
// Qt::Popup closes automatically when the user clicks outside.
|
||||
m_popup = new QFrame(this, Qt::Popup);
|
||||
m_popup->setFrameShape(QFrame::StyledPanel);
|
||||
m_popup->setFrameShadow(QFrame::Raised);
|
||||
|
||||
m_label = new QLabel("80%", widget);
|
||||
auto *layout = new QVBoxLayout(m_popup);
|
||||
layout->setContentsMargins(10, 10, 10, 10);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_label = new QLabel(QStringLiteral("80%"), m_popup);
|
||||
m_label->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_label);
|
||||
|
||||
m_slider = new ClickableSlider(Qt::Vertical, widget);
|
||||
m_slider = new ClickableSlider(Qt::Vertical, m_popup);
|
||||
m_slider->setRange(0, 100);
|
||||
m_slider->setValue(80);
|
||||
m_slider->setFixedHeight(120);
|
||||
layout->addWidget(m_slider, 0, Qt::AlignHCenter);
|
||||
|
||||
layout->addWidget(m_label);
|
||||
layout->addWidget(m_slider);
|
||||
|
||||
auto *action = new QWidgetAction(menu);
|
||||
action->setDefaultWidget(widget);
|
||||
menu->addAction(action);
|
||||
setMenu(menu);
|
||||
// Size the popup at its maximum (label = "100%") and lock it
|
||||
m_label->setText(QStringLiteral("100%"));
|
||||
m_popup->adjustSize();
|
||||
m_popup->setFixedSize(m_popup->sizeHint());
|
||||
m_label->setText(QStringLiteral("80%"));
|
||||
|
||||
connect(this, &QToolButton::clicked, this, &VolumeButton::togglePopup);
|
||||
connect(m_slider, &QSlider::valueChanged, this, [this](int v) {
|
||||
m_label->setText(QString::number(v) + "%");
|
||||
m_label->setText(QString::number(v) + QStringLiteral("%"));
|
||||
updateIcon(v);
|
||||
emit volumeChanged(v);
|
||||
});
|
||||
@@ -56,14 +58,31 @@ public:
|
||||
m_slider->blockSignals(true);
|
||||
m_slider->setValue(v);
|
||||
m_slider->blockSignals(false);
|
||||
m_label->setText(QString::number(v) + "%");
|
||||
m_label->setText(QString::number(v) + QStringLiteral("%"));
|
||||
updateIcon(v);
|
||||
}
|
||||
|
||||
signals:
|
||||
void volumeChanged(int volume);
|
||||
|
||||
private slots:
|
||||
void togglePopup()
|
||||
{
|
||||
if (m_popup->isVisible()) {
|
||||
m_popup->hide();
|
||||
return;
|
||||
}
|
||||
// Centre popup horizontally over button, place below it
|
||||
const QPoint global = mapToGlobal(
|
||||
QPoint(width() / 2 - m_popup->width() / 2,
|
||||
height() + 4));
|
||||
m_popup->move(global);
|
||||
m_popup->show();
|
||||
m_popup->raise();
|
||||
}
|
||||
|
||||
private:
|
||||
QFrame *m_popup = nullptr;
|
||||
ClickableSlider *m_slider = nullptr;
|
||||
QLabel *m_label = nullptr;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user