feat: context menus on search panel, track info dialog, separate shuffle buttons
- Add right-click context menus to search panel tracks (play, queue, favorites, playlist, go to album/artist, track info) and albums (open, favorite, go to artist) - Add "Track info..." dialog showing metadata (title, performer, composer, album, quality, hi-res status) — available everywhere: playlists, albums, favorites, search results - Split artist page shuffle into "Shuffle" (popular tracks) and "Shuffle All" (deep shuffle across all releases) - Remove magnifying glass emoji from welcome text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include "view.hpp"
|
||||
#include "../../util/trackinfo.hpp"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
@@ -6,18 +7,21 @@
|
||||
#include <QHeaderView>
|
||||
#include <QFont>
|
||||
#include <QJsonArray>
|
||||
#include <QMenu>
|
||||
|
||||
static constexpr int IdRole = Qt::UserRole + 1;
|
||||
static constexpr int TypeRole = Qt::UserRole + 2;
|
||||
static constexpr int JsonRole = Qt::UserRole + 3;
|
||||
|
||||
namespace SidePanel
|
||||
{
|
||||
|
||||
// ---- SearchTab ----
|
||||
|
||||
SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
SearchTab::SearchTab(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_backend(backend)
|
||||
, m_queue(queue)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
@@ -25,7 +29,7 @@ SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
// Search bar
|
||||
auto *barLayout = new QHBoxLayout;
|
||||
m_searchBox = new QLineEdit(this);
|
||||
m_searchBox->setPlaceholderText(tr("Search Qobuz…"));
|
||||
m_searchBox->setPlaceholderText(tr("Search Qobuz..."));
|
||||
m_searchBox->setClearButtonEnabled(true);
|
||||
auto *searchBtn = new QPushButton(tr("Go"), this);
|
||||
barLayout->addWidget(m_searchBox);
|
||||
@@ -38,6 +42,7 @@ SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
m_trackResults = new QTreeWidget(this);
|
||||
m_trackResults->setHeaderLabels({tr("Title"), tr("Artist"), tr("Album")});
|
||||
m_trackResults->setRootIsDecorated(false);
|
||||
m_trackResults->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
m_albumResults = new QTreeWidget(this);
|
||||
m_albumResults->setHeaderLabels({tr(""), tr("Album"), tr("Artist")});
|
||||
@@ -46,6 +51,7 @@ SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
m_albumResults->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
m_albumResults->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_albumResults->header()->setStretchLastSection(false);
|
||||
m_albumResults->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
m_artistResults = new QTreeWidget(this);
|
||||
m_artistResults->setHeaderLabels({tr("Artist")});
|
||||
@@ -64,6 +70,17 @@ SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
connect(m_trackResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
connect(m_albumResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
connect(m_artistResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
|
||||
// Context menus
|
||||
connect(m_trackResults, &QTreeWidget::customContextMenuRequested,
|
||||
this, &SearchTab::onTrackContextMenu);
|
||||
connect(m_albumResults, &QTreeWidget::customContextMenuRequested,
|
||||
this, &SearchTab::onAlbumContextMenu);
|
||||
}
|
||||
|
||||
void SearchTab::setUserPlaylists(const QVector<QPair<qint64, QString>> &playlists)
|
||||
{
|
||||
m_userPlaylists = playlists;
|
||||
}
|
||||
|
||||
void SearchTab::onSearchSubmit()
|
||||
@@ -86,6 +103,7 @@ void SearchTab::onSearchResult(const QJsonObject &result)
|
||||
QStringList{t["title"].toString(), performer, album});
|
||||
item->setData(0, IdRole, static_cast<qint64>(t["id"].toDouble()));
|
||||
item->setData(0, TypeRole, QStringLiteral("track"));
|
||||
item->setData(0, JsonRole, t);
|
||||
}
|
||||
|
||||
// Populate albums
|
||||
@@ -109,8 +127,9 @@ void SearchTab::onSearchResult(const QJsonObject &result)
|
||||
item->setFont(0, hiResFont);
|
||||
item->setTextAlignment(0, Qt::AlignCenter);
|
||||
}
|
||||
item->setData(0, TypeRole, QStringLiteral("album")); // handler reads col 0
|
||||
item->setData(0, TypeRole, QStringLiteral("album"));
|
||||
item->setData(1, IdRole, a["id"].toString());
|
||||
item->setData(0, JsonRole, a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,20 +159,135 @@ void SearchTab::onItemDoubleClicked(QTreeWidgetItem *item, int)
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTab::onTrackContextMenu(const QPoint &pos)
|
||||
{
|
||||
auto *item = m_trackResults->itemAt(pos);
|
||||
if (!item) return;
|
||||
|
||||
const qint64 trackId = item->data(0, IdRole).toLongLong();
|
||||
const QJsonObject trackJson = item->data(0, JsonRole).toJsonObject();
|
||||
if (trackId <= 0) return;
|
||||
|
||||
QMenu menu(this);
|
||||
|
||||
auto *playNow = menu.addAction(tr("Play now"));
|
||||
auto *playNext = menu.addAction(tr("Play next"));
|
||||
auto *addQueue = menu.addAction(tr("Add to queue"));
|
||||
menu.addSeparator();
|
||||
|
||||
auto *addFav = menu.addAction(tr("Add to favorites"));
|
||||
|
||||
// Open album / artist
|
||||
const QString albumId = trackJson["album"].toObject()["id"].toString();
|
||||
const qint64 artistId = static_cast<qint64>(
|
||||
trackJson["performer"].toObject()["id"].toDouble());
|
||||
const QString artistName = trackJson["performer"].toObject()["name"].toString();
|
||||
const QString albumTitle = trackJson["album"].toObject()["title"].toString();
|
||||
|
||||
menu.addSeparator();
|
||||
if (!albumId.isEmpty()) {
|
||||
auto *openAlbum = menu.addAction(tr("Go to album: %1").arg(albumTitle));
|
||||
connect(openAlbum, &QAction::triggered, this, [this, albumId] {
|
||||
emit albumSelected(albumId);
|
||||
});
|
||||
}
|
||||
if (artistId > 0) {
|
||||
auto *openArtist = menu.addAction(tr("Go to artist: %1").arg(artistName));
|
||||
connect(openArtist, &QAction::triggered, this, [this, artistId] {
|
||||
emit artistSelected(artistId);
|
||||
});
|
||||
}
|
||||
|
||||
// Add to playlist submenu
|
||||
if (!m_userPlaylists.isEmpty()) {
|
||||
menu.addSeparator();
|
||||
auto *plMenu = menu.addMenu(tr("Add to playlist"));
|
||||
for (const auto &pl : m_userPlaylists) {
|
||||
auto *act = plMenu->addAction(pl.second);
|
||||
connect(act, &QAction::triggered, this, [this, trackId, plId = pl.first] {
|
||||
emit addToPlaylistRequested(trackId, plId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track info
|
||||
menu.addSeparator();
|
||||
auto *info = menu.addAction(tr("Track info..."));
|
||||
|
||||
connect(playNow, &QAction::triggered, this, [this, trackId] {
|
||||
emit trackPlayRequested(trackId);
|
||||
});
|
||||
connect(playNext, &QAction::triggered, this, [this, trackJson] {
|
||||
m_queue->playNext(trackJson);
|
||||
});
|
||||
connect(addQueue, &QAction::triggered, this, [this, trackJson] {
|
||||
m_queue->addToQueue(trackJson);
|
||||
});
|
||||
connect(addFav, &QAction::triggered, this, [this, trackId] {
|
||||
m_backend->addFavTrack(trackId);
|
||||
});
|
||||
connect(info, &QAction::triggered, this, [this, trackJson] {
|
||||
showTrackInfo(trackJson);
|
||||
});
|
||||
|
||||
menu.exec(m_trackResults->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void SearchTab::onAlbumContextMenu(const QPoint &pos)
|
||||
{
|
||||
auto *item = m_albumResults->itemAt(pos);
|
||||
if (!item) return;
|
||||
|
||||
const QString albumId = item->data(1, IdRole).toString();
|
||||
const QJsonObject albumJson = item->data(0, JsonRole).toJsonObject();
|
||||
if (albumId.isEmpty()) return;
|
||||
|
||||
QMenu menu(this);
|
||||
|
||||
auto *openAlbum = menu.addAction(tr("Open album"));
|
||||
auto *addFav = menu.addAction(tr("Add to favorites"));
|
||||
|
||||
const qint64 artistId = static_cast<qint64>(
|
||||
albumJson["artist"].toObject()["id"].toDouble());
|
||||
const QString artistName = albumJson["artist"].toObject()["name"].toString();
|
||||
if (artistId > 0) {
|
||||
menu.addSeparator();
|
||||
auto *openArtist = menu.addAction(tr("Go to artist: %1").arg(artistName));
|
||||
connect(openArtist, &QAction::triggered, this, [this, artistId] {
|
||||
emit artistSelected(artistId);
|
||||
});
|
||||
}
|
||||
|
||||
connect(openAlbum, &QAction::triggered, this, [this, albumId] {
|
||||
emit albumSelected(albumId);
|
||||
});
|
||||
connect(addFav, &QAction::triggered, this, [this, albumId] {
|
||||
m_backend->addFavAlbum(albumId);
|
||||
});
|
||||
|
||||
menu.exec(m_albumResults->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void SearchTab::showTrackInfo(const QJsonObject &track)
|
||||
{
|
||||
TrackInfoDialog::show(track, this);
|
||||
}
|
||||
|
||||
// ---- View ----
|
||||
|
||||
View::View(QobuzBackend *backend, QWidget *parent)
|
||||
View::View(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
||||
: QDockWidget(tr("Search"), parent)
|
||||
{
|
||||
setObjectName(QStringLiteral("searchPanel"));
|
||||
setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);
|
||||
|
||||
m_search = new SearchTab(backend, this);
|
||||
m_search = new SearchTab(backend, queue, this);
|
||||
setWidget(m_search);
|
||||
|
||||
connect(m_search, &SearchTab::albumSelected, this, &View::albumSelected);
|
||||
connect(m_search, &SearchTab::artistSelected, this, &View::artistSelected);
|
||||
connect(m_search, &SearchTab::trackPlayRequested, this, &View::trackPlayRequested);
|
||||
connect(m_search, &SearchTab::albumSelected, this, &View::albumSelected);
|
||||
connect(m_search, &SearchTab::artistSelected, this, &View::artistSelected);
|
||||
connect(m_search, &SearchTab::trackPlayRequested, this, &View::trackPlayRequested);
|
||||
connect(m_search, &SearchTab::addToPlaylistRequested, this, &View::addToPlaylistRequested);
|
||||
}
|
||||
|
||||
} // namespace SidePanel
|
||||
|
||||
Reference in New Issue
Block a user