feat: album/playlist header and playlist ownership filtering
- Add TrackContextHeader widget: shows album art (fetched via NAM), title, subtitle (artist/description), and metadata (year · tracks · duration) above the track list when an album or playlist is opened - Hide header for favorite tracks and search results - Store user ID in AppSettings on login - Only show "Delete playlist" for playlists the user owns - "Add to playlist" submenu only lists owned playlists - "Remove from this playlist" only appears when viewing an owned playlist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
162
src/view/trackcontextheader.hpp
Normal file
162
src/view/trackcontextheader.hpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QFont>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QUrl>
|
||||
|
||||
/// Header strip shown above the track list when an album or playlist is open.
|
||||
/// Displays album art, title, subtitle, and metadata.
|
||||
class TrackContextHeader : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit TrackContextHeader(QWidget *parent = nullptr)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setFixedHeight(140);
|
||||
|
||||
auto *hlay = new QHBoxLayout(this);
|
||||
hlay->setContentsMargins(12, 8, 12, 8);
|
||||
hlay->setSpacing(14);
|
||||
|
||||
m_art = new QLabel(this);
|
||||
m_art->setFixedSize(120, 120);
|
||||
m_art->setScaledContents(true);
|
||||
m_art->setAlignment(Qt::AlignCenter);
|
||||
m_art->setStyleSheet(QStringLiteral("background: #1a1a1a; border-radius: 4px;"));
|
||||
hlay->addWidget(m_art, 0, Qt::AlignVCenter);
|
||||
|
||||
auto *info = new QWidget(this);
|
||||
auto *vlay = new QVBoxLayout(info);
|
||||
vlay->setContentsMargins(0, 0, 0, 0);
|
||||
vlay->setSpacing(4);
|
||||
|
||||
m_title = new QLabel(info);
|
||||
QFont tf = m_title->font();
|
||||
tf.setPointSize(tf.pointSize() + 5);
|
||||
tf.setBold(true);
|
||||
m_title->setFont(tf);
|
||||
m_title->setWordWrap(true);
|
||||
vlay->addWidget(m_title);
|
||||
|
||||
m_subtitle = new QLabel(info);
|
||||
QFont sf = m_subtitle->font();
|
||||
sf.setPointSize(sf.pointSize() + 1);
|
||||
m_subtitle->setFont(sf);
|
||||
m_subtitle->setWordWrap(true);
|
||||
vlay->addWidget(m_subtitle);
|
||||
|
||||
m_meta = new QLabel(info);
|
||||
QPalette mp = m_meta->palette();
|
||||
mp.setColor(QPalette::WindowText, QColor(0xaa, 0xaa, 0xaa));
|
||||
m_meta->setPalette(mp);
|
||||
vlay->addWidget(m_meta);
|
||||
|
||||
vlay->addStretch();
|
||||
hlay->addWidget(info, 1);
|
||||
|
||||
m_nam = new QNetworkAccessManager(this);
|
||||
QObject::connect(m_nam, &QNetworkAccessManager::finished,
|
||||
[this](QNetworkReply *reply) {
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
return;
|
||||
QPixmap pix;
|
||||
if (pix.loadFromData(reply->readAll()))
|
||||
m_art->setPixmap(pix);
|
||||
});
|
||||
}
|
||||
|
||||
void setAlbum(const QJsonObject &album)
|
||||
{
|
||||
m_title->setText(album["title"].toString());
|
||||
m_subtitle->setText(album["artist"].toObject()["name"].toString());
|
||||
m_meta->setText(buildAlbumMeta(album));
|
||||
fetchArt(album["image"].toObject());
|
||||
show();
|
||||
}
|
||||
|
||||
void setPlaylist(const QJsonObject &playlist)
|
||||
{
|
||||
m_title->setText(playlist["name"].toString());
|
||||
const QString desc = playlist["description"].toString();
|
||||
const QString owner = playlist["owner"].toObject()["name"].toString();
|
||||
m_subtitle->setText(desc.isEmpty() ? owner : desc);
|
||||
m_meta->setText(buildPlaylistMeta(playlist));
|
||||
|
||||
const QJsonArray imgs = playlist["image_rectangle"].toArray();
|
||||
if (!imgs.isEmpty())
|
||||
fetchUrl(imgs.first().toString());
|
||||
else
|
||||
m_art->setPixmap(QPixmap());
|
||||
|
||||
show();
|
||||
}
|
||||
|
||||
private:
|
||||
void fetchArt(const QJsonObject &img)
|
||||
{
|
||||
QString url = img["large"].toString();
|
||||
if (url.isEmpty()) url = img["small"].toString();
|
||||
fetchUrl(url);
|
||||
}
|
||||
|
||||
void fetchUrl(const QString &url)
|
||||
{
|
||||
if (url.isEmpty()) {
|
||||
m_art->setPixmap(QPixmap());
|
||||
return;
|
||||
}
|
||||
if (url == m_currentArtUrl)
|
||||
return;
|
||||
m_currentArtUrl = url;
|
||||
m_nam->get(QNetworkRequest(QUrl(url)));
|
||||
}
|
||||
|
||||
static QString formatDuration(int totalSecs)
|
||||
{
|
||||
const int h = totalSecs / 3600;
|
||||
const int m = (totalSecs % 3600) / 60;
|
||||
if (h > 0)
|
||||
return QStringLiteral("%1h %2m").arg(h).arg(m);
|
||||
return QStringLiteral("%1 min").arg(m);
|
||||
}
|
||||
|
||||
static QString buildAlbumMeta(const QJsonObject &album)
|
||||
{
|
||||
QStringList parts;
|
||||
const QString year = album["release_date_original"].toString().left(4);
|
||||
if (!year.isEmpty()) parts << year;
|
||||
const int tracks = album["tracks_count"].toInt();
|
||||
if (tracks > 0) parts << QStringLiteral("%1 tracks").arg(tracks);
|
||||
const int dur = static_cast<int>(album["duration"].toDouble());
|
||||
if (dur > 0) parts << formatDuration(dur);
|
||||
return parts.join(QStringLiteral(" · "));
|
||||
}
|
||||
|
||||
static QString buildPlaylistMeta(const QJsonObject &playlist)
|
||||
{
|
||||
QStringList parts;
|
||||
const int tracks = playlist["tracks_count"].toInt();
|
||||
if (tracks > 0) parts << QStringLiteral("%1 tracks").arg(tracks);
|
||||
const int dur = static_cast<int>(playlist["duration"].toDouble());
|
||||
if (dur > 0) parts << formatDuration(dur);
|
||||
return parts.join(QStringLiteral(" · "));
|
||||
}
|
||||
|
||||
QLabel *m_art = nullptr;
|
||||
QLabel *m_title = nullptr;
|
||||
QLabel *m_subtitle = nullptr;
|
||||
QLabel *m_meta = nullptr;
|
||||
QNetworkAccessManager *m_nam = nullptr;
|
||||
QString m_currentArtUrl;
|
||||
};
|
||||
Reference in New Issue
Block a user