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

173
src/view/maintoolbar.cpp Normal file
View File

@@ -0,0 +1,173 @@
#include "maintoolbar.hpp"
#include "../util/settings.hpp"
#include "../model/tracklistmodel.hpp"
#include <QWidget>
#include <QHBoxLayout>
MainToolBar::MainToolBar(QobuzBackend *backend, QWidget *parent)
: QToolBar(parent)
, m_backend(backend)
{
setMovable(false);
setFloatable(false);
setContextMenuPolicy(Qt::PreventContextMenu);
// Previous
m_previous = addAction(Icon::previous(), tr("Previous"));
connect(m_previous, &QAction::triggered, this, &MainToolBar::onPrevious);
// Play/Pause
m_playPause = addAction(Icon::play(), tr("Play"));
m_playPause->setCheckable(false);
connect(m_playPause, &QAction::triggered, this, [this](bool) { onPlayPause(false); });
// Next
m_next = addAction(Icon::next(), tr("Next"));
connect(m_next, &QAction::triggered, this, &MainToolBar::onNext);
addSeparator();
// Track label
m_trackLabel = new QLabel(tr("Not playing"), this);
m_trackLabel->setMinimumWidth(200);
m_trackLabel->setMaximumWidth(320);
addWidget(m_trackLabel);
addSeparator();
// Progress slider
m_progress = new ClickableSlider(Qt::Horizontal, this);
m_progress->setRange(0, 1000);
m_progress->setValue(0);
m_progress->setMinimumWidth(180);
m_progress->setMaximumWidth(400);
addWidget(m_progress);
connect(m_progress, &QSlider::sliderPressed, this, [this] { m_seeking = true; });
connect(m_progress, &QSlider::sliderReleased, this, &MainToolBar::onProgressReleased);
// Time label
m_timeLabel = new QLabel("0:00 / 0:00", this);
addWidget(m_timeLabel);
addSeparator();
// Volume button
m_volume = new VolumeButton(this);
m_volume->setValue(AppSettings::instance().volume());
addWidget(m_volume);
connect(m_volume, &VolumeButton::volumeChanged, this, &MainToolBar::onVolumeChanged);
addSeparator();
// Search toggle
m_search = addAction(Icon::search(), tr("Search"));
m_search->setCheckable(true);
connect(m_search, &QAction::toggled, this, &MainToolBar::searchToggled);
// Connect to backend signals
connect(m_backend, &QobuzBackend::stateChanged, this, &MainToolBar::onBackendStateChanged);
connect(m_backend, &QobuzBackend::trackChanged, this, &MainToolBar::onTrackChanged);
connect(m_backend, &QobuzBackend::positionChanged, this, &MainToolBar::onPositionChanged);
connect(m_backend, &QobuzBackend::trackFinished, this, &MainToolBar::onTrackFinished);
}
void MainToolBar::setPlaying(bool playing)
{
m_playing = playing;
m_playPause->setIcon(playing ? Icon::pause() : Icon::play());
m_playPause->setText(playing ? tr("Pause") : tr("Play"));
}
void MainToolBar::setCurrentTrack(const QJsonObject &track)
{
const QString title = track["title"].toString();
const QJsonObject performer = track["performer"].toObject();
const QString artist = performer["name"].toString();
if (title.isEmpty()) {
m_trackLabel->setText(tr("Not playing"));
} else if (artist.isEmpty()) {
m_trackLabel->setText(title);
} else {
m_trackLabel->setText(QStringLiteral("%1 — %2").arg(artist, title));
}
}
void MainToolBar::updateProgress(quint64 position, quint64 duration)
{
if (m_seeking) return;
const int sliderPos = duration > 0
? static_cast<int>(position * 1000 / duration)
: 0;
m_progress->blockSignals(true);
m_progress->setValue(sliderPos);
m_progress->blockSignals(false);
const QString pos = TrackListModel::formatDuration(static_cast<qint64>(position));
const QString dur = TrackListModel::formatDuration(static_cast<qint64>(duration));
m_timeLabel->setText(QStringLiteral("%1 / %2").arg(pos, dur));
}
void MainToolBar::setVolume(int volume)
{
m_volume->setValue(volume);
}
// --- private slots ---
void MainToolBar::onPlayPause(bool)
{
if (m_playing) {
m_backend->pause();
} else {
m_backend->resume();
}
}
void MainToolBar::onPrevious()
{
// TODO: wire up to a queue / playlist
}
void MainToolBar::onNext()
{
// TODO: wire up to a queue / playlist
}
void MainToolBar::onProgressReleased()
{
m_seeking = false;
// Seek is not yet implemented in the Rust player, but we can at least
// update the displayed position optimistically.
}
void MainToolBar::onVolumeChanged(int volume)
{
m_backend->setVolume(volume);
AppSettings::instance().setVolume(volume);
}
void MainToolBar::onBackendStateChanged(const QString &state)
{
setPlaying(state == QStringLiteral("playing"));
}
void MainToolBar::onTrackChanged(const QJsonObject &track)
{
setCurrentTrack(track);
}
void MainToolBar::onPositionChanged(quint64 position, quint64 duration)
{
updateProgress(position, duration);
}
void MainToolBar::onTrackFinished()
{
setPlaying(false);
m_progress->setValue(0);
m_timeLabel->setText("0:00 / 0:00");
}