Initial implementation of qobuz-qt
- Rust backend (qobuz-backend static lib): Qobuz API client (reqwest/tokio), Symphonia audio decoder, CPAL audio output, extern "C" FFI bridge - Qt 6 frontend mirroring spotify-qt layout: toolbar with playback controls, left library dock, central track list, right search panel - Auth: email/password login with MD5-signed requests; session token persisted via QSettings - Playback: double-click a track → Rust fetches stream URL → Symphonia decodes → CPAL outputs to default audio device - Dark Fusion palette matching spotify-qt feel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
139
src/view/sidepanel/view.cpp
Normal file
139
src/view/sidepanel/view.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
#include "view.hpp"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QJsonArray>
|
||||
|
||||
static constexpr int IdRole = Qt::UserRole + 1;
|
||||
static constexpr int TypeRole = Qt::UserRole + 2;
|
||||
|
||||
namespace SidePanel
|
||||
{
|
||||
|
||||
// ---- SearchTab ----
|
||||
|
||||
SearchTab::SearchTab(QobuzBackend *backend, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_backend(backend)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
|
||||
// Search bar
|
||||
auto *barLayout = new QHBoxLayout;
|
||||
m_searchBox = new QLineEdit(this);
|
||||
m_searchBox->setPlaceholderText(tr("Search Qobuz…"));
|
||||
m_searchBox->setClearButtonEnabled(true);
|
||||
auto *searchBtn = new QPushButton(tr("Go"), this);
|
||||
barLayout->addWidget(m_searchBox);
|
||||
barLayout->addWidget(searchBtn);
|
||||
layout->addLayout(barLayout);
|
||||
|
||||
// Result tabs
|
||||
m_resultTabs = new QTabWidget(this);
|
||||
|
||||
m_trackResults = new QTreeWidget(this);
|
||||
m_trackResults->setHeaderLabels({tr("Title"), tr("Artist"), tr("Album")});
|
||||
m_trackResults->setRootIsDecorated(false);
|
||||
|
||||
m_albumResults = new QTreeWidget(this);
|
||||
m_albumResults->setHeaderLabels({tr("Album"), tr("Artist")});
|
||||
m_albumResults->setRootIsDecorated(false);
|
||||
|
||||
m_artistResults = new QTreeWidget(this);
|
||||
m_artistResults->setHeaderLabels({tr("Artist")});
|
||||
m_artistResults->setRootIsDecorated(false);
|
||||
|
||||
m_resultTabs->addTab(m_trackResults, tr("Tracks"));
|
||||
m_resultTabs->addTab(m_albumResults, tr("Albums"));
|
||||
m_resultTabs->addTab(m_artistResults, tr("Artists"));
|
||||
layout->addWidget(m_resultTabs);
|
||||
|
||||
connect(searchBtn, &QPushButton::clicked, this, &SearchTab::onSearchSubmit);
|
||||
connect(m_searchBox, &QLineEdit::returnPressed, this, &SearchTab::onSearchSubmit);
|
||||
|
||||
connect(m_backend, &QobuzBackend::searchResult, this, &SearchTab::onSearchResult);
|
||||
|
||||
connect(m_trackResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
connect(m_albumResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
connect(m_artistResults, &QTreeWidget::itemDoubleClicked, this, &SearchTab::onItemDoubleClicked);
|
||||
}
|
||||
|
||||
void SearchTab::onSearchSubmit()
|
||||
{
|
||||
const QString q = m_searchBox->text().trimmed();
|
||||
if (!q.isEmpty())
|
||||
m_backend->search(q, 0, 20);
|
||||
}
|
||||
|
||||
void SearchTab::onSearchResult(const QJsonObject &result)
|
||||
{
|
||||
// Populate tracks
|
||||
m_trackResults->clear();
|
||||
const QJsonArray tracks = result["tracks"].toObject()["items"].toArray();
|
||||
for (const auto &v : tracks) {
|
||||
const QJsonObject t = v.toObject();
|
||||
const QString performer = t["performer"].toObject()["name"].toString();
|
||||
const QString album = t["album"].toObject()["title"].toString();
|
||||
auto *item = new QTreeWidgetItem(m_trackResults,
|
||||
QStringList{t["title"].toString(), performer, album});
|
||||
item->setData(0, IdRole, static_cast<qint64>(t["id"].toDouble()));
|
||||
item->setData(0, TypeRole, QStringLiteral("track"));
|
||||
}
|
||||
|
||||
// Populate albums
|
||||
m_albumResults->clear();
|
||||
const QJsonArray albums = result["albums"].toObject()["items"].toArray();
|
||||
for (const auto &v : albums) {
|
||||
const QJsonObject a = v.toObject();
|
||||
const QString artist = a["artist"].toObject()["name"].toString();
|
||||
auto *item = new QTreeWidgetItem(m_albumResults,
|
||||
QStringList{a["title"].toString(), artist});
|
||||
item->setData(0, IdRole, a["id"].toString());
|
||||
item->setData(0, TypeRole, QStringLiteral("album"));
|
||||
}
|
||||
|
||||
// Populate artists
|
||||
m_artistResults->clear();
|
||||
const QJsonArray artists = result["artists"].toObject()["items"].toArray();
|
||||
for (const auto &v : artists) {
|
||||
const QJsonObject ar = v.toObject();
|
||||
auto *item = new QTreeWidgetItem(m_artistResults,
|
||||
QStringList{ar["name"].toString()});
|
||||
item->setData(0, IdRole, static_cast<qint64>(ar["id"].toDouble()));
|
||||
item->setData(0, TypeRole, QStringLiteral("artist"));
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTab::onItemDoubleClicked(QTreeWidgetItem *item, int)
|
||||
{
|
||||
if (!item) return;
|
||||
const QString type = item->data(0, TypeRole).toString();
|
||||
|
||||
if (type == QStringLiteral("track")) {
|
||||
emit trackPlayRequested(item->data(0, IdRole).toLongLong());
|
||||
} else if (type == QStringLiteral("album")) {
|
||||
emit albumSelected(item->data(0, IdRole).toString());
|
||||
} else if (type == QStringLiteral("artist")) {
|
||||
emit artistSelected(item->data(0, IdRole).toLongLong());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- View ----
|
||||
|
||||
View::View(QobuzBackend *backend, QWidget *parent)
|
||||
: QDockWidget(tr("Search"), parent)
|
||||
{
|
||||
setObjectName(QStringLiteral("searchPanel"));
|
||||
setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable);
|
||||
|
||||
m_search = new SearchTab(backend, 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);
|
||||
}
|
||||
|
||||
} // namespace SidePanel
|
||||
Reference in New Issue
Block a user