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>
239 lines
7.2 KiB
C++
239 lines
7.2 KiB
C++
#include "maintoolbar.hpp"
|
|
#include "../util/settings.hpp"
|
|
#include "../model/tracklistmodel.hpp"
|
|
|
|
#include <QWidget>
|
|
#include <QHBoxLayout>
|
|
#include <QNetworkRequest>
|
|
|
|
MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
|
: QToolBar(parent)
|
|
, m_backend(backend)
|
|
, m_queue(queue)
|
|
{
|
|
setMovable(false);
|
|
setFloatable(false);
|
|
setContextMenuPolicy(Qt::PreventContextMenu);
|
|
setIconSize(QSize(22, 22));
|
|
|
|
m_nam = new QNetworkAccessManager(this);
|
|
connect(m_nam, &QNetworkAccessManager::finished, this, &MainToolBar::onAlbumArtReady);
|
|
|
|
// --- Album art thumbnail ---
|
|
m_artLabel = new QLabel(this);
|
|
m_artLabel->setFixedSize(36, 36);
|
|
m_artLabel->setScaledContents(true);
|
|
m_artLabel->setStyleSheet("border: 1px solid #444; background: #1a1a1a;");
|
|
m_artLabel->setPixmap(QIcon(":/res/icons/view-media-album-cover.svg")
|
|
.pixmap(32, 32));
|
|
addWidget(m_artLabel);
|
|
addSeparator();
|
|
|
|
// --- Playback controls ---
|
|
m_previous = addAction(Icon::previous(), tr("Previous"));
|
|
connect(m_previous, &QAction::triggered, this, &MainToolBar::onPrevious);
|
|
|
|
m_playPause = addAction(Icon::play(), tr("Play"));
|
|
connect(m_playPause, &QAction::triggered, this, &MainToolBar::onPlayPause);
|
|
|
|
m_next = addAction(Icon::next(), tr("Next"));
|
|
connect(m_next, &QAction::triggered, this, &MainToolBar::onNext);
|
|
|
|
addSeparator();
|
|
|
|
// --- Track info label ---
|
|
m_trackLabel = new QLabel(tr("Not playing"), this);
|
|
m_trackLabel->setMinimumWidth(180);
|
|
m_trackLabel->setMaximumWidth(340);
|
|
m_trackLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
|
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(160);
|
|
m_progress->setMaximumWidth(380);
|
|
addWidget(m_progress);
|
|
|
|
connect(m_progress, &QSlider::sliderPressed, this, [this] { m_seeking = true; });
|
|
connect(m_progress, &QSlider::sliderReleased, this, &MainToolBar::onProgressReleased);
|
|
|
|
m_timeLabel = new QLabel("0:00 / 0:00", this);
|
|
addWidget(m_timeLabel);
|
|
|
|
addSeparator();
|
|
|
|
// --- Volume ---
|
|
m_volume = new VolumeButton(this);
|
|
m_volume->setValue(AppSettings::instance().volume());
|
|
addWidget(m_volume);
|
|
connect(m_volume, &VolumeButton::volumeChanged, this, &MainToolBar::onVolumeChanged);
|
|
|
|
addSeparator();
|
|
|
|
// --- Shuffle ---
|
|
m_shuffle = addAction(Icon::get(QStringLiteral("media-playlist-shuffle")), tr("Shuffle"));
|
|
m_shuffle->setCheckable(true);
|
|
connect(m_shuffle, &QAction::toggled, this, &MainToolBar::onShuffleToggled);
|
|
|
|
addSeparator();
|
|
|
|
// --- Queue toggle ---
|
|
m_queueBtn = addAction(Icon::queue(), tr("Queue"));
|
|
m_queueBtn->setCheckable(true);
|
|
connect(m_queueBtn, &QAction::toggled, this, &MainToolBar::queueToggled);
|
|
|
|
// --- Search toggle ---
|
|
m_search = addAction(Icon::search(), tr("Search"));
|
|
m_search->setCheckable(true);
|
|
connect(m_search, &QAction::toggled, this, &MainToolBar::searchToggled);
|
|
|
|
// --- 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);
|
|
|
|
// --- Queue signals ---
|
|
connect(m_queue, &PlayQueue::queueChanged, this, &MainToolBar::onQueueChanged);
|
|
onQueueChanged(); // initialise button states
|
|
}
|
|
|
|
// ---- public ----
|
|
|
|
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 QString artist = track["performer"].toObject()["name"].toString().isEmpty()
|
|
? track["album"].toObject()["artist"].toObject()["name"].toString()
|
|
: track["performer"].toObject()["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));
|
|
}
|
|
|
|
// Album art
|
|
const QString artUrl = track["album"].toObject()["image"].toObject()["small"].toString();
|
|
if (!artUrl.isEmpty() && artUrl != m_currentArtUrl) {
|
|
m_currentArtUrl = artUrl;
|
|
fetchAlbumArt(artUrl);
|
|
}
|
|
}
|
|
|
|
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);
|
|
m_timeLabel->setText(
|
|
QStringLiteral("%1 / %2")
|
|
.arg(TrackListModel::formatDuration(static_cast<qint64>(position)),
|
|
TrackListModel::formatDuration(static_cast<qint64>(duration))));
|
|
}
|
|
|
|
// ---- private slots ----
|
|
|
|
void MainToolBar::onPlayPause()
|
|
{
|
|
if (m_playing) m_backend->pause();
|
|
else m_backend->resume();
|
|
}
|
|
|
|
void MainToolBar::onPrevious()
|
|
{
|
|
if (!m_queue->canGoPrev()) return;
|
|
const QJsonObject track = m_queue->stepBack();
|
|
const qint64 id = static_cast<qint64>(track["id"].toDouble());
|
|
if (id > 0)
|
|
m_backend->playTrack(id, AppSettings::instance().preferredFormat());
|
|
}
|
|
|
|
void MainToolBar::onNext()
|
|
{
|
|
if (!m_queue->canGoNext()) return;
|
|
const QJsonObject track = m_queue->advance();
|
|
const qint64 id = static_cast<qint64>(track["id"].toDouble());
|
|
if (id > 0)
|
|
m_backend->playTrack(id, AppSettings::instance().preferredFormat());
|
|
}
|
|
|
|
void MainToolBar::onProgressReleased()
|
|
{
|
|
m_seeking = false;
|
|
}
|
|
|
|
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()
|
|
{
|
|
// Auto-advance queue
|
|
if (m_queue->canGoNext()) {
|
|
onNext();
|
|
} else {
|
|
setPlaying(false);
|
|
m_progress->setValue(0);
|
|
m_timeLabel->setText("0:00 / 0:00");
|
|
}
|
|
}
|
|
|
|
void MainToolBar::onQueueChanged()
|
|
{
|
|
m_previous->setEnabled(m_queue->canGoPrev());
|
|
m_next->setEnabled(m_queue->canGoNext());
|
|
}
|
|
|
|
void MainToolBar::onShuffleToggled(bool checked)
|
|
{
|
|
m_queue->setShuffle(checked);
|
|
}
|
|
|
|
void MainToolBar::fetchAlbumArt(const QString &url)
|
|
{
|
|
m_nam->get(QNetworkRequest(QUrl(url)));
|
|
}
|
|
|
|
void MainToolBar::onAlbumArtReady(QNetworkReply *reply)
|
|
{
|
|
reply->deleteLater();
|
|
if (reply->error() != QNetworkReply::NoError) return;
|
|
QPixmap pix;
|
|
if (pix.loadFromData(reply->readAll()))
|
|
m_artLabel->setPixmap(pix);
|
|
}
|