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:
joren
2026-03-23 23:34:23 +01:00
commit 9402dca7ed
40 changed files with 3963 additions and 0 deletions

251
src/mainwindow.cpp Normal file
View File

@@ -0,0 +1,251 @@
#include "mainwindow.hpp"
#include "dialog/login.hpp"
#include "dialog/settings.hpp"
#include "util/settings.hpp"
#include <QApplication>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QStatusBar>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QTimer>
#include <QJsonArray>
MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
: QMainWindow(parent)
, m_backend(backend)
{
setWindowTitle(QStringLiteral("Qobuz"));
setMinimumSize(800, 500);
resize(defaultSize());
// ---- Toolbar ----
m_toolBar = new MainToolBar(m_backend, this);
addToolBar(Qt::TopToolBarArea, m_toolBar);
// ---- Central content ----
m_content = new MainContent(m_backend, this);
setCentralWidget(m_content);
// ---- Library dock (left) ----
m_library = new List::Library(m_backend, this);
m_libraryDock = new QDockWidget(tr("Library"), this);
m_libraryDock->setObjectName(QStringLiteral("libraryDock"));
m_libraryDock->setFeatures(QDockWidget::DockWidgetMovable);
m_libraryDock->setWidget(m_library);
m_libraryDock->setMinimumWidth(200);
addDockWidget(Qt::LeftDockWidgetArea, m_libraryDock);
// ---- Search side panel (right) ----
m_sidePanel = new SidePanel::View(m_backend, this);
m_sidePanel->hide();
addDockWidget(Qt::RightDockWidgetArea, m_sidePanel);
// ---- Menu bar ----
setupMenuBar();
// ---- Status bar ----
statusBar()->showMessage(tr("Ready"));
// ---- Wire signals ----
connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess);
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::albumLoaded, this, &MainWindow::onAlbumLoaded);
connect(m_backend, &QobuzBackend::artistLoaded, this, &MainWindow::onArtistLoaded);
connect(m_backend, &QobuzBackend::playlistLoaded, this, &MainWindow::onPlaylistLoaded);
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
statusBar()->showMessage(tr("Error: %1").arg(msg), 5000);
});
// Library → backend
connect(m_library, &List::Library::favTracksRequested, m_backend, [this] {
m_backend->getFavTracks();
statusBar()->showMessage(tr("Loading favorite tracks…"));
});
connect(m_library, &List::Library::favAlbumsRequested, m_backend, [this] {
m_backend->getFavAlbums();
statusBar()->showMessage(tr("Loading favorite albums…"));
});
connect(m_library, &List::Library::favArtistsRequested, m_backend, [this] {
m_backend->getFavArtists();
statusBar()->showMessage(tr("Loading favorite artists…"));
});
connect(m_library, &List::Library::playlistRequested,
this, [this](qint64 id, const QString &name) {
m_backend->getPlaylist(id);
statusBar()->showMessage(tr("Loading playlist: %1…").arg(name));
});
// Track list → playback
connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
this, &MainWindow::onPlayTrackRequested);
// Search panel → backend
connect(m_sidePanel, &SidePanel::View::albumSelected,
this, &MainWindow::onSearchAlbumSelected);
connect(m_sidePanel, &SidePanel::View::artistSelected,
this, &MainWindow::onSearchArtistSelected);
connect(m_sidePanel, &SidePanel::View::trackPlayRequested,
this, &MainWindow::onPlayTrackRequested);
// Toolbar search toggle → side panel visibility
connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled);
// Restore or prompt login
tryRestoreSession();
}
void MainWindow::setupMenuBar()
{
auto *fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(tr("&Sign in…"), this, &MainWindow::showLoginDialog);
fileMenu->addSeparator();
fileMenu->addAction(tr("&Settings…"), this, &MainWindow::showSettingsDialog);
fileMenu->addSeparator();
fileMenu->addAction(tr("&Quit"), qApp, &QApplication::quit, QKeySequence::Quit);
auto *viewMenu = menuBar()->addMenu(tr("&View"));
viewMenu->addAction(m_libraryDock->toggleViewAction());
viewMenu->addAction(m_sidePanel->toggleViewAction());
auto *helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(tr("&About"), this, [this] {
QMessageBox::about(this, tr("About Qobuz"),
tr("<h3>qobuz-qt</h3>"
"<p>A lightweight Qt client for the Qobuz streaming service.</p>"
"<p>Audio decoding powered by Symphonia (Rust).</p>"));
});
}
void MainWindow::tryRestoreSession()
{
const QString token = AppSettings::instance().authToken();
if (!token.isEmpty()) {
m_backend->setToken(token);
m_library->refresh();
const QString name = AppSettings::instance().displayName();
statusBar()->showMessage(tr("Signed in as %1").arg(
name.isEmpty() ? AppSettings::instance().userEmail() : name));
} else {
// No saved session — prompt login after a short delay so the window is visible first
QTimer::singleShot(200, this, &MainWindow::showLoginDialog);
}
}
// ---- slots ----
void MainWindow::showLoginDialog()
{
auto *dlg = new LoginDialog(this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
// Pre-fill if remembered
if (AppSettings::instance().rememberLogin())
dlg->findChild<QLineEdit *>(); // just show with empty fields
connect(dlg, &LoginDialog::loginRequested,
this, [this, dlg](const QString &email, const QString &password) {
dlg->setBusy(true);
m_backend->login(email, password);
});
connect(m_backend, &QobuzBackend::loginSuccess, dlg, [dlg](const QString &, const QJsonObject &) {
dlg->accept();
});
connect(m_backend, &QobuzBackend::loginError, dlg, [dlg](const QString &err) {
dlg->setError(err);
});
dlg->exec();
}
void MainWindow::showSettingsDialog()
{
SettingsDialog dlg(this);
dlg.exec();
}
void MainWindow::onLoginSuccess(const QString &token, const QJsonObject &user)
{
AppSettings::instance().setAuthToken(token);
const QString displayName = user["display_name"].toString();
const QString email = user["email"].toString();
AppSettings::instance().setDisplayName(displayName);
AppSettings::instance().setUserEmail(email);
statusBar()->showMessage(tr("Signed in as %1").arg(
displayName.isEmpty() ? email : displayName));
m_library->refresh();
}
void MainWindow::onLoginError(const QString &error)
{
statusBar()->showMessage(tr("Login failed: %1").arg(error), 6000);
}
void MainWindow::onFavTracksLoaded(const QJsonObject &result)
{
m_content->showFavTracks(result);
statusBar()->showMessage(tr("Favorite tracks loaded."), 3000);
}
void MainWindow::onFavAlbumsLoaded(const QJsonObject &result)
{
// Show albums as a track list (each album as a row)
// For now just show the raw tracks from each album
// A proper album grid view would go here in a future iteration
const QJsonArray albums = result["items"].toArray();
statusBar()->showMessage(tr("%1 favorite albums").arg(albums.size()), 3000);
// TODO: show album grid
}
void MainWindow::onAlbumLoaded(const QJsonObject &album)
{
m_content->showAlbum(album);
const QString title = album["title"].toString();
statusBar()->showMessage(tr("Album: %1").arg(title), 3000);
}
void MainWindow::onArtistLoaded(const QJsonObject &artist)
{
// TODO: show artist view with top tracks + discography
statusBar()->showMessage(tr("Artist: %1").arg(artist["name"].toString()), 3000);
}
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
{
m_content->showPlaylist(playlist);
const QString name = playlist["name"].toString();
statusBar()->showMessage(tr("Playlist: %1").arg(name), 3000);
}
void MainWindow::onPlayTrackRequested(qint64 trackId)
{
const int format = AppSettings::instance().preferredFormat();
m_backend->playTrack(trackId, format);
statusBar()->showMessage(tr("Loading track…"));
}
void MainWindow::onSearchAlbumSelected(const QString &albumId)
{
m_backend->getAlbum(albumId);
statusBar()->showMessage(tr("Loading album…"));
}
void MainWindow::onSearchArtistSelected(qint64 artistId)
{
m_backend->getArtist(artistId);
statusBar()->showMessage(tr("Loading artist…"));
}
void MainWindow::onSearchToggled(bool visible)
{
m_sidePanel->setVisible(visible);
}