feat: initial qobuz-qt source
Lightweight Qt6 desktop client for Qobuz with a Rust audio backend (Symphonia/CPAL via staticlib FFI). Mirrors the spotify-qt layout: toolbar with playback controls, library/context docks on the left, tabbed search side panel on the right, queue panel, now-playing dock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#include "mainwindow.hpp"
|
||||
|
||||
#include "dialog/login.hpp"
|
||||
#include "dialog/settings.hpp"
|
||||
#include "util/settings.hpp"
|
||||
#include "util/icon.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMenuBar>
|
||||
@@ -23,12 +23,15 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
setMinimumSize(800, 500);
|
||||
resize(defaultSize());
|
||||
|
||||
// ---- Queue (owned here, shared with toolbar and track list) ----
|
||||
m_queue = new PlayQueue(this);
|
||||
|
||||
// ---- Toolbar ----
|
||||
m_toolBar = new MainToolBar(m_backend, this);
|
||||
m_toolBar = new MainToolBar(m_backend, m_queue, this);
|
||||
addToolBar(Qt::TopToolBarArea, m_toolBar);
|
||||
|
||||
// ---- Central content ----
|
||||
m_content = new MainContent(m_backend, this);
|
||||
m_content = new MainContent(m_backend, m_queue, this);
|
||||
setCentralWidget(m_content);
|
||||
|
||||
// ---- Library dock (left) ----
|
||||
@@ -40,39 +43,46 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
m_libraryDock->setMinimumWidth(200);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_libraryDock);
|
||||
|
||||
// ---- Now-playing context dock (left, below library) ----
|
||||
m_contextView = new Context::View(m_backend, this);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_contextView);
|
||||
|
||||
// ---- Queue panel (right) ----
|
||||
m_queuePanel = new QueuePanel(m_queue, this);
|
||||
m_queuePanel->hide();
|
||||
addDockWidget(Qt::RightDockWidgetArea, m_queuePanel);
|
||||
|
||||
// ---- 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);
|
||||
// ---- Backend 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::trackChanged, this, &MainWindow::onTrackChanged);
|
||||
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
|
||||
statusBar()->showMessage(tr("Error: %1").arg(msg), 5000);
|
||||
statusBar()->showMessage(tr("Error: %1").arg(msg), 6000);
|
||||
});
|
||||
|
||||
// Library → backend
|
||||
connect(m_library, &List::Library::favTracksRequested, m_backend, [this] {
|
||||
// ---- Library → backend ----
|
||||
connect(m_library, &List::Library::favTracksRequested, this, [this] {
|
||||
m_backend->getFavTracks();
|
||||
statusBar()->showMessage(tr("Loading favorite tracks…"));
|
||||
});
|
||||
connect(m_library, &List::Library::favAlbumsRequested, m_backend, [this] {
|
||||
connect(m_library, &List::Library::favAlbumsRequested, this, [this] {
|
||||
m_backend->getFavAlbums();
|
||||
statusBar()->showMessage(tr("Loading favorite albums…"));
|
||||
});
|
||||
connect(m_library, &List::Library::favArtistsRequested, m_backend, [this] {
|
||||
connect(m_library, &List::Library::favArtistsRequested, this, [this] {
|
||||
m_backend->getFavArtists();
|
||||
statusBar()->showMessage(tr("Loading favorite artists…"));
|
||||
});
|
||||
@@ -82,11 +92,11 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
statusBar()->showMessage(tr("Loading playlist: %1…").arg(name));
|
||||
});
|
||||
|
||||
// Track list → playback
|
||||
// ---- Track list → playback ----
|
||||
connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
|
||||
this, &MainWindow::onPlayTrackRequested);
|
||||
|
||||
// Search panel → backend
|
||||
// ---- Search panel ----
|
||||
connect(m_sidePanel, &SidePanel::View::albumSelected,
|
||||
this, &MainWindow::onSearchAlbumSelected);
|
||||
connect(m_sidePanel, &SidePanel::View::artistSelected,
|
||||
@@ -94,32 +104,40 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
|
||||
connect(m_sidePanel, &SidePanel::View::trackPlayRequested,
|
||||
this, &MainWindow::onPlayTrackRequested);
|
||||
|
||||
// Toolbar search toggle → side panel visibility
|
||||
// ---- Toolbar toggles ----
|
||||
connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled);
|
||||
connect(m_toolBar, &MainToolBar::queueToggled,
|
||||
this, [this](bool v) { m_queuePanel->setVisible(v); });
|
||||
|
||||
// Restore or prompt login
|
||||
tryRestoreSession();
|
||||
}
|
||||
|
||||
void MainWindow::setupMenuBar()
|
||||
{
|
||||
auto *fileMenu = menuBar()->addMenu(tr("&File"));
|
||||
fileMenu->addAction(tr("&Sign in…"), this, &MainWindow::showLoginDialog);
|
||||
fileMenu->addAction(Icon::get("im-user-away"), tr("&Sign in…"),
|
||||
this, &MainWindow::showLoginDialog);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("&Settings…"), this, &MainWindow::showSettingsDialog);
|
||||
fileMenu->addAction(Icon::settings(), tr("&Settings…"),
|
||||
this, &MainWindow::showSettingsDialog);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("&Quit"), qApp, &QApplication::quit, QKeySequence::Quit);
|
||||
auto *quitAction = fileMenu->addAction(Icon::get("application-exit"), tr("&Quit"),
|
||||
qApp, &QApplication::quit);
|
||||
quitAction->setShortcut(QKeySequence::Quit);
|
||||
|
||||
auto *viewMenu = menuBar()->addMenu(tr("&View"));
|
||||
viewMenu->addAction(m_libraryDock->toggleViewAction());
|
||||
viewMenu->addAction(m_contextView->toggleViewAction());
|
||||
viewMenu->addAction(m_queuePanel->toggleViewAction());
|
||||
viewMenu->addAction(m_sidePanel->toggleViewAction());
|
||||
|
||||
auto *helpMenu = menuBar()->addMenu(tr("&Help"));
|
||||
helpMenu->addAction(tr("&About"), this, [this] {
|
||||
helpMenu->addAction(Icon::get("help-about"), 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>"));
|
||||
"<p>Audio engine: <b>Symphonia</b> (Rust) via CPAL/ALSA.<br>"
|
||||
"Icons: <b>spotify-qt</b> (dark variant).</p>"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -133,7 +151,6 @@ void MainWindow::tryRestoreSession()
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -145,16 +162,11 @@ 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();
|
||||
});
|
||||
@@ -178,10 +190,8 @@ void MainWindow::onLoginSuccess(const QString &token, const QJsonObject &user)
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -190,47 +200,59 @@ void MainWindow::onLoginError(const QString &error)
|
||||
statusBar()->showMessage(tr("Login failed: %1").arg(error), 6000);
|
||||
}
|
||||
|
||||
void MainWindow::onTrackChanged(const QJsonObject &track)
|
||||
{
|
||||
// Update playing row highlight in the track list
|
||||
const qint64 id = static_cast<qint64>(track["id"].toDouble());
|
||||
m_content->tracksList()->setPlayingTrackId(id);
|
||||
|
||||
// Update status bar with track name
|
||||
const QString title = track["title"].toString();
|
||||
const QString artist = track["performer"].toObject()["name"].toString().isEmpty()
|
||||
? track["album"].toObject()["artist"].toObject()["name"].toString()
|
||||
: track["performer"].toObject()["name"].toString();
|
||||
statusBar()->showMessage(
|
||||
artist.isEmpty() ? title : QStringLiteral("▶ %1 — %2").arg(artist, title));
|
||||
}
|
||||
|
||||
void MainWindow::onFavTracksLoaded(const QJsonObject &result)
|
||||
{
|
||||
m_content->showFavTracks(result);
|
||||
statusBar()->showMessage(tr("Favorite tracks loaded."), 3000);
|
||||
statusBar()->showMessage(
|
||||
tr("%1 favorite tracks").arg(result["total"].toInt()), 4000);
|
||||
}
|
||||
|
||||
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
|
||||
const int total = result["total"].toInt();
|
||||
statusBar()->showMessage(tr("%1 favorite albums").arg(total), 4000);
|
||||
// TODO: album grid view
|
||||
}
|
||||
|
||||
void MainWindow::onAlbumLoaded(const QJsonObject &album)
|
||||
{
|
||||
m_content->showAlbum(album);
|
||||
const QString title = album["title"].toString();
|
||||
statusBar()->showMessage(tr("Album: %1").arg(title), 3000);
|
||||
statusBar()->showMessage(
|
||||
tr("Album: %1").arg(album["title"].toString()), 4000);
|
||||
}
|
||||
|
||||
void MainWindow::onArtistLoaded(const QJsonObject &artist)
|
||||
{
|
||||
// TODO: show artist view with top tracks + discography
|
||||
statusBar()->showMessage(tr("Artist: %1").arg(artist["name"].toString()), 3000);
|
||||
statusBar()->showMessage(
|
||||
tr("Artist: %1").arg(artist["name"].toString()), 4000);
|
||||
// TODO: artist view
|
||||
}
|
||||
|
||||
void MainWindow::onPlaylistLoaded(const QJsonObject &playlist)
|
||||
{
|
||||
m_content->showPlaylist(playlist);
|
||||
const QString name = playlist["name"].toString();
|
||||
statusBar()->showMessage(tr("Playlist: %1").arg(name), 3000);
|
||||
statusBar()->showMessage(
|
||||
tr("Playlist: %1").arg(playlist["name"].toString()), 4000);
|
||||
}
|
||||
|
||||
void MainWindow::onPlayTrackRequested(qint64 trackId)
|
||||
{
|
||||
const int format = AppSettings::instance().preferredFormat();
|
||||
m_backend->playTrack(trackId, format);
|
||||
statusBar()->showMessage(tr("Loading track…"));
|
||||
m_backend->playTrack(trackId, AppSettings::instance().preferredFormat());
|
||||
}
|
||||
|
||||
void MainWindow::onSearchAlbumSelected(const QString &albumId)
|
||||
|
||||
Reference in New Issue
Block a user