6 Commits

Author SHA1 Message Date
joren
92d48e459e Merge branch 'refactor/color-constants'
Some checks failed
Build for Windows / build-windows (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
2139bbb726 Merge branch 'refactor/mainwindow-setup'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
c2e0ff41ac Merge branch 'refactor/playqueue-split'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:19:50 +02:00
joren
d1b9cb1210 refactor: centralize hardcoded color values into Colors namespace
Add src/util/colors.hpp with named constants for all QColor values
(brand accents, badge colors, text shades, surface backgrounds) and
replace scattered QColor constructor calls across 7 source files.
Stylesheet string colors are intentionally left inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:01:45 +02:00
joren
5f79170f48 refactor: split MainWindow constructor into focused setup methods
Extract the ~300-line constructor body into setupDocks(), setupScrobbler(),
setupGapless(), setupMpris(), connectBackendSignals(), connectLibrarySignals(),
connectContentSignals(), and connectToolbarSignals(). No behavioral changes;
all signal/slot connections and widget creation remain identical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:00:19 +02:00
joren
dea16676ce refactor: split PlayQueue from header-only into .hpp/.cpp pair
Move all method implementations from playqueue.hpp into a new
playqueue.cpp, keeping only declarations and trivial inline getters
in the header. No logic or behavior changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 10:58:06 +02:00
13 changed files with 452 additions and 329 deletions

View File

@@ -6,8 +6,9 @@ target_sources(qobuz-qt PRIVATE
mainwindow.hpp mainwindow.hpp
mainwindow.cpp mainwindow.cpp
# Queue (header-only) # Queue
playqueue.hpp playqueue.hpp
playqueue.cpp
# Backend (Qt wrapper around Rust FFI) # Backend (Qt wrapper around Rust FFI)
backend/qobuzbackend.hpp backend/qobuzbackend.hpp

View File

@@ -1,5 +1,6 @@
#include "mainwindow.hpp" #include "mainwindow.hpp"
#include "backend/qobuzbackend.hpp" #include "backend/qobuzbackend.hpp"
#include "util/colors.hpp"
#include <QApplication> #include <QApplication>
#include <QStyleFactory> #include <QStyleFactory>
@@ -15,24 +16,24 @@ int main(int argc, char *argv[])
// Accent: #FFB232 (yellow-orange), Blue: #46B3EE, Backgrounds: #191919 / #141414 // Accent: #FFB232 (yellow-orange), Blue: #46B3EE, Backgrounds: #191919 / #141414
app.setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
QPalette darkPalette; QPalette darkPalette;
darkPalette.setColor(QPalette::Window, QColor(0x19, 0x19, 0x19)); darkPalette.setColor(QPalette::Window, Colors::WindowBg);
darkPalette.setColor(QPalette::WindowText, QColor(0xe8, 0xe8, 0xe8)); darkPalette.setColor(QPalette::WindowText, Colors::LightText);
darkPalette.setColor(QPalette::Base, QColor(0x14, 0x14, 0x14)); darkPalette.setColor(QPalette::Base, Colors::BaseBg);
darkPalette.setColor(QPalette::AlternateBase, QColor(0x1e, 0x1e, 0x1e)); darkPalette.setColor(QPalette::AlternateBase, Colors::AlternateBaseBg);
darkPalette.setColor(QPalette::ToolTipBase, QColor(0x19, 0x19, 0x19)); darkPalette.setColor(QPalette::ToolTipBase, Colors::WindowBg);
darkPalette.setColor(QPalette::ToolTipText, QColor(0xe8, 0xe8, 0xe8)); darkPalette.setColor(QPalette::ToolTipText, Colors::LightText);
darkPalette.setColor(QPalette::Text, QColor(0xe8, 0xe8, 0xe8)); darkPalette.setColor(QPalette::Text, Colors::LightText);
darkPalette.setColor(QPalette::Button, QColor(0x2a, 0x2a, 0x2a)); darkPalette.setColor(QPalette::Button, Colors::ButtonSurface);
darkPalette.setColor(QPalette::ButtonText, QColor(0xe8, 0xe8, 0xe8)); darkPalette.setColor(QPalette::ButtonText, Colors::LightText);
darkPalette.setColor(QPalette::BrightText, QColor(0xFF, 0xB2, 0x32)); darkPalette.setColor(QPalette::BrightText, Colors::QobuzOrange);
darkPalette.setColor(QPalette::Link, QColor(0x46, 0xB3, 0xEE)); // Qobuz blue darkPalette.setColor(QPalette::Link, Colors::QobuzBlue);
darkPalette.setColor(QPalette::Highlight, QColor(0xFF, 0xB2, 0x32)); // Qobuz orange darkPalette.setColor(QPalette::Highlight, Colors::QobuzOrange);
darkPalette.setColor(QPalette::HighlightedText, QColor(0x10, 0x10, 0x10)); // dark on orange darkPalette.setColor(QPalette::HighlightedText, Colors::HighlightedFg);
darkPalette.setColor(QPalette::PlaceholderText, QColor(0x66, 0x66, 0x66)); darkPalette.setColor(QPalette::PlaceholderText, Colors::PlaceholderText);
darkPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(0x55, 0x55, 0x55)); darkPalette.setColor(QPalette::Disabled, QPalette::Text, Colors::DisabledText);
darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(0x55, 0x55, 0x55)); darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, Colors::DisabledText);
darkPalette.setColor(QPalette::Mid, QColor(0x2f, 0x2f, 0x2f)); darkPalette.setColor(QPalette::Mid, Colors::MidSurface);
darkPalette.setColor(QPalette::Dark, QColor(0x0e, 0x0e, 0x0e)); darkPalette.setColor(QPalette::Dark, Colors::DarkSurface);
app.setPalette(darkPalette); app.setPalette(darkPalette);
// Stylesheet tweaks: orange accent on scrollbars, focus rings, etc. // Stylesheet tweaks: orange accent on scrollbars, focus rings, etc.

View File

@@ -38,6 +38,27 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
m_content = new MainContent(m_backend, m_queue, this); m_content = new MainContent(m_backend, m_queue, this);
setCentralWidget(m_content); setCentralWidget(m_content);
setupDocks();
setupMenuBar();
statusBar()->showMessage(tr("Ready"));
setupScrobbler();
setupGapless();
setupMpris();
connectBackendSignals();
connectLibrarySignals();
connectContentSignals();
connectToolbarSignals();
// Apply playback options from saved settings
m_backend->setReplayGain(AppSettings::instance().replayGainEnabled());
m_backend->setGapless(AppSettings::instance().gaplessEnabled());
tryRestoreSession();
}
void MainWindow::setupDocks()
{
// ---- Library dock (left) ---- // ---- Library dock (left) ----
m_library = new List::Library(m_backend, this); m_library = new List::Library(m_backend, this);
m_libraryDock = new QDockWidget(tr("Library"), this); m_libraryDock = new QDockWidget(tr("Library"), this);
@@ -60,44 +81,95 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
m_sidePanel = new SidePanel::View(m_backend, m_queue, this); m_sidePanel = new SidePanel::View(m_backend, m_queue, this);
m_sidePanel->hide(); m_sidePanel->hide();
addDockWidget(Qt::RightDockWidgetArea, m_sidePanel); addDockWidget(Qt::RightDockWidgetArea, m_sidePanel);
}
setupMenuBar(); void MainWindow::setupScrobbler()
statusBar()->showMessage(tr("Ready")); {
m_scrobbler = new LastFmScrobbler(this);
connect(m_backend, &QobuzBackend::trackChanged,
m_scrobbler, &LastFmScrobbler::onTrackStarted);
connect(m_backend, &QobuzBackend::positionChanged,
m_scrobbler, &LastFmScrobbler::onPositionChanged);
connect(m_backend, &QobuzBackend::trackFinished,
m_scrobbler, &LastFmScrobbler::onTrackFinished);
// ---- Scrobbler ---- // Scrobble the finished track during a gapless transition
m_scrobbler = new LastFmScrobbler(this); connect(m_backend, &QobuzBackend::trackTransitioned,
connect(m_backend, &QobuzBackend::trackChanged, m_scrobbler, &LastFmScrobbler::onTrackFinished);
m_scrobbler, &LastFmScrobbler::onTrackStarted); }
connect(m_backend, &QobuzBackend::positionChanged,
m_scrobbler, &LastFmScrobbler::onPositionChanged);
connect(m_backend, &QobuzBackend::trackFinished,
m_scrobbler, &LastFmScrobbler::onTrackFinished);
// 1. Scrobble the finished track during a gapless transition void MainWindow::setupGapless()
connect(m_backend, &QobuzBackend::trackTransitioned, {
m_scrobbler, &LastFmScrobbler::onTrackFinished); connect(m_backend, &QobuzBackend::positionChanged, this, [this](quint64 pos, quint64 dur) {
if (!AppSettings::instance().gaplessEnabled() || dur == 0) return;
// ---- Gapless Signal ---- // Trigger prefetch if we pass the 50% mark OR are within 60 seconds of the end
connect(m_backend, &QobuzBackend::positionChanged, this, [this](quint64 pos, quint64 dur) { if ((pos > dur / 2) || (dur > 60 && (dur - pos) <= 60)) {
if (!AppSettings::instance().gaplessEnabled() || dur == 0) return; if (!m_nextTrackPrefetched && m_queue->canGoNext()) {
m_nextTrackPrefetched = true; // Lock it so it only fires once
// Trigger prefetch if we pass the 50% mark OR are within 60 seconds of the end const auto upcoming = m_queue->upcomingTracks(1);
if ((pos > dur / 2) || (dur > 60 && (dur - pos) <= 60)) { if (!upcoming.isEmpty()) {
if (!m_nextTrackPrefetched && m_queue->canGoNext()) { const qint64 nextId = static_cast<qint64>(upcoming.first()["id"].toDouble());
m_nextTrackPrefetched = true; // Lock it so it only fires once if (nextId > 0) {
m_backend->prefetchTrack(nextId, AppSettings::instance().preferredFormat());
const auto upcoming = m_queue->upcomingTracks(1);
if (!upcoming.isEmpty()) {
const qint64 nextId = static_cast<qint64>(upcoming.first()["id"].toDouble());
if (nextId > 0) {
m_backend->prefetchTrack(nextId, AppSettings::instance().preferredFormat());
}
} }
} }
} }
}); }
});
}
// ---- Backend signals ---- void MainWindow::setupMpris()
{
#ifdef USE_DBUS
m_mpris = new Mpris(this);
connect(m_mpris->player(), &MprisPlayerAdaptor::playRequested, m_backend, [this] {
if (m_backend->state() == 2) m_backend->resume();
});
connect(m_mpris->player(), &MprisPlayerAdaptor::pauseRequested, m_backend, &QobuzBackend::pause);
connect(m_mpris->player(), &MprisPlayerAdaptor::playPauseRequested, m_backend, [this] {
if (m_backend->state() == 1)
m_backend->pause();
else
m_backend->resume();
});
connect(m_mpris->player(), &MprisPlayerAdaptor::stopRequested, m_backend, &QobuzBackend::stop);
connect(m_mpris->player(), &MprisPlayerAdaptor::nextRequested, this, [this] {
if (!m_queue->canGoNext()) return;
const qint64 id = static_cast<qint64>(m_queue->advance()["id"].toDouble());
if (id > 0) m_backend->playTrack(id);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::previousRequested, this, [this] {
if (!m_queue->canGoPrev()) return;
const qint64 id = static_cast<qint64>(m_queue->stepBack()["id"].toDouble());
if (id > 0) m_backend->playTrack(id);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::seekRequested, m_backend, [this](qlonglong offsetMicroseconds) {
qint64 newPos = m_backend->position() + (offsetMicroseconds / 1000000LL);
if (newPos < 0) newPos = 0;
m_backend->seek(newPos);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::seekToRequested, m_backend, [this](qlonglong positionMicroseconds) {
m_backend->seek(positionMicroseconds / 1000000LL);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::volumeChangeRequested, m_backend, [this](double vol) {
m_backend->setVolume(vol * 100);
});
connect(m_backend, &QobuzBackend::stateChanged, this, [this](const QString &state) {
if (state == "playing") m_mpris->player()->setPlaybackStatus("Playing");
else if (state == "paused") m_mpris->player()->setPlaybackStatus("Paused");
else m_mpris->player()->setPlaybackStatus("Stopped");
});
connect(m_backend, &QobuzBackend::positionChanged, this, [this](quint64 pos) {
m_mpris->player()->updatePosition(pos);
});
#endif
}
void MainWindow::connectBackendSignals()
{
connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess); connect(m_backend, &QobuzBackend::loginSuccess, this, &MainWindow::onLoginSuccess);
connect(m_backend, &QobuzBackend::loginError, this, &MainWindow::onLoginError); connect(m_backend, &QobuzBackend::loginError, this, &MainWindow::onLoginError);
connect(m_backend, &QobuzBackend::userLoaded, this, [this](const QJsonObject &user) { connect(m_backend, &QobuzBackend::userLoaded, this, [this](const QJsonObject &user) {
@@ -145,8 +217,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) { connect(m_backend, &QobuzBackend::error, this, [this](const QString &msg) {
statusBar()->showMessage(tr("Error: %1").arg(msg), 6000); statusBar()->showMessage(tr("Error: %1").arg(msg), 6000);
}); });
}
// ---- Library signals ---- void MainWindow::connectLibrarySignals()
{
connect(m_library, &List::Library::userPlaylistIdsChanged, connect(m_library, &List::Library::userPlaylistIdsChanged,
this, [this](const QSet<qint64> &playlistIds) { this, [this](const QSet<qint64> &playlistIds) {
m_userPlaylistIds = playlistIds; m_userPlaylistIds = playlistIds;
@@ -167,52 +241,6 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
m_backend->getFavTracks(); m_backend->getFavTracks();
statusBar()->showMessage(tr("Loading favorite tracks…")); statusBar()->showMessage(tr("Loading favorite tracks…"));
}); });
#ifdef USE_DBUS
m_mpris = new Mpris(this);
connect(m_mpris->player(), &MprisPlayerAdaptor::playRequested, m_backend, [this] {
if (m_backend->state() == 2) m_backend->resume();
});
connect(m_mpris->player(), &MprisPlayerAdaptor::pauseRequested, m_backend, &QobuzBackend::pause);
connect(m_mpris->player(), &MprisPlayerAdaptor::playPauseRequested, m_backend, [this] {
if (m_backend->state() == 1)
m_backend->pause();
else
m_backend->resume();
});
connect(m_mpris->player(), &MprisPlayerAdaptor::stopRequested, m_backend, &QobuzBackend::stop);
connect(m_mpris->player(), &MprisPlayerAdaptor::nextRequested, this, [this] {
if (!m_queue->canGoNext()) return;
const qint64 id = static_cast<qint64>(m_queue->advance()["id"].toDouble());
if (id > 0) m_backend->playTrack(id);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::previousRequested, this, [this] {
if (!m_queue->canGoPrev()) return;
const qint64 id = static_cast<qint64>(m_queue->stepBack()["id"].toDouble());
if (id > 0) m_backend->playTrack(id);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::seekRequested, m_backend, [this](qlonglong offsetMicroseconds) {
qint64 newPos = m_backend->position() + (offsetMicroseconds / 1000000LL);
if (newPos < 0) newPos = 0;
m_backend->seek(newPos);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::seekToRequested, m_backend, [this](qlonglong positionMicroseconds) {
m_backend->seek(positionMicroseconds / 1000000LL);
});
connect(m_mpris->player(), &MprisPlayerAdaptor::volumeChangeRequested, m_backend, [this](double vol) {
m_backend->setVolume(vol * 100);
});
connect(m_backend, &QobuzBackend::stateChanged, this, [this](const QString &state) {
if (state == "playing") m_mpris->player()->setPlaybackStatus("Playing");
else if (state == "paused") m_mpris->player()->setPlaybackStatus("Paused");
else m_mpris->player()->setPlaybackStatus("Stopped");
});
connect(m_backend, &QobuzBackend::positionChanged, this, [this](quint64 pos) {
m_mpris->player()->updatePosition(pos);
});
#endif
connect(m_library, &List::Library::favAlbumsRequested, this, [this] { connect(m_library, &List::Library::favAlbumsRequested, this, [this] {
m_showFavAlbumsOnLoad = true; m_showFavAlbumsOnLoad = true;
m_backend->getFavAlbums(); m_backend->getFavAlbums();
@@ -236,7 +264,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
m_content->showPlaylistBrowser(); m_content->showPlaylistBrowser();
statusBar()->showMessage(tr("Browse Playlists")); statusBar()->showMessage(tr("Browse Playlists"));
}); });
}
void MainWindow::connectContentSignals()
{
// ---- Track list → playback / playlist management ---- // ---- Track list → playback / playlist management ----
connect(m_content->tracksList(), &List::Tracks::playTrackRequested, connect(m_content->tracksList(), &List::Tracks::playTrackRequested,
this, &MainWindow::onPlayTrackRequested); this, &MainWindow::onPlayTrackRequested);
@@ -300,8 +331,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
// ---- Queue panel ---- // ---- Queue panel ----
connect(m_queuePanel, &QueuePanel::skipToTrackRequested, connect(m_queuePanel, &QueuePanel::skipToTrackRequested,
this, &MainWindow::onPlayTrackRequested); this, &MainWindow::onPlayTrackRequested);
}
// ---- Toolbar toggles ---- void MainWindow::connectToolbarSignals()
{
connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled); connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled);
connect(m_toolBar, &MainToolBar::queueToggled, connect(m_toolBar, &MainToolBar::queueToggled,
this, [this](bool v) { m_queuePanel->setVisible(v); }); this, [this](bool v) { m_queuePanel->setVisible(v); });
@@ -314,12 +347,6 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
connect(m_toolBar, &MainToolBar::albumRequested, this, &MainWindow::onSearchAlbumSelected); connect(m_toolBar, &MainToolBar::albumRequested, this, &MainWindow::onSearchAlbumSelected);
connect(m_toolBar, &MainToolBar::artistRequested, this, &MainWindow::onSearchArtistSelected); connect(m_toolBar, &MainToolBar::artistRequested, this, &MainWindow::onSearchArtistSelected);
// Apply playback options from saved settings
m_backend->setReplayGain(AppSettings::instance().replayGainEnabled());
m_backend->setGapless(AppSettings::instance().gaplessEnabled());
tryRestoreSession();
} }
void MainWindow::setupMenuBar() void MainWindow::setupMenuBar()

View File

@@ -72,5 +72,13 @@ private:
bool m_nextTrackPrefetched = false; bool m_nextTrackPrefetched = false;
void setupMenuBar(); void setupMenuBar();
void setupDocks();
void setupScrobbler();
void setupGapless();
void setupMpris();
void connectBackendSignals();
void connectLibrarySignals();
void connectContentSignals();
void connectToolbarSignals();
void tryRestoreSession(); void tryRestoreSession();
}; };

View File

@@ -1,4 +1,5 @@
#include "tracklistmodel.hpp" #include "tracklistmodel.hpp"
#include "../util/colors.hpp"
#include <QJsonValue> #include <QJsonValue>
#include <QColor> #include <QColor>
@@ -255,7 +256,7 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
QFont f; f.setBold(true); return f; QFont f; f.setBold(true); return f;
} }
if (role == Qt::ForegroundRole) if (role == Qt::ForegroundRole)
return QColor(0xFF, 0xB2, 0x32); return Colors::QobuzOrange;
return {}; return {};
} }
@@ -278,8 +279,8 @@ QVariant TrackListModel::data(const QModelIndex &index, int role) const
} }
if (role == Qt::ForegroundRole) { if (role == Qt::ForegroundRole) {
if (!t.streamable) return QColor(0x55, 0x55, 0x55); if (!t.streamable) return Colors::DisabledText;
if (isPlaying) return QColor(0xFF, 0xB2, 0x32); // Qobuz orange if (isPlaying) return Colors::QobuzOrange;
} }
if (role == Qt::DecorationRole && index.column() == ColNumber && isPlaying) { if (role == Qt::DecorationRole && index.column() == ColNumber && isPlaying) {

238
src/playqueue.cpp Normal file
View File

@@ -0,0 +1,238 @@
#include "playqueue.hpp"
#include <algorithm>
#include <random>
PlayQueue::PlayQueue(QObject *parent) : QObject(parent) {}
void PlayQueue::setContext(const QJsonArray &tracks, int startIndex)
{
m_queue.clear();
m_playNext.clear();
// Only queue streamable tracks; find the filtered index for startIndex
int filteredStart = 0;
int filteredIdx = 0;
bool found = false;
for (int orig = 0; orig < tracks.size(); ++orig) {
const QJsonObject t = tracks[orig].toObject();
if (!t["streamable"].toBool(true))
continue;
if (!found && orig >= startIndex) {
filteredStart = filteredIdx;
found = true;
}
m_queue.append(t);
++filteredIdx;
}
m_index = qBound(0, filteredStart, qMax(0, m_queue.size() - 1));
if (m_shuffle)
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::reorderContext(const QJsonArray &tracks, qint64 currentId)
{
m_queue.clear();
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
m_index = 0;
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == currentId) {
m_index = i;
break;
}
}
emit queueChanged();
}
void PlayQueue::clearUpcoming()
{
m_playNext.clear();
if (m_index < m_queue.size())
m_queue.resize(m_index + 1); // keep up to and including current
emit queueChanged();
}
void PlayQueue::removeUpcoming(int upcomingIndex)
{
if (upcomingIndex < m_playNext.size()) {
m_playNext.removeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx < m_queue.size())
m_queue.removeAt(queueIdx);
}
emit queueChanged();
}
void PlayQueue::setShuffle(bool enabled)
{
if (m_shuffle == enabled) return;
m_shuffle = enabled;
if (enabled && !m_queue.isEmpty())
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::shuffleNow()
{
if (m_queue.isEmpty()) return;
shuffleQueue(m_index);
emit queueChanged();
}
void PlayQueue::addToQueue(const QJsonObject &track)
{
m_playNext.append(track);
emit queueChanged();
}
void PlayQueue::playNext(const QJsonObject &track)
{
m_playNext.prepend(track);
emit queueChanged();
}
bool PlayQueue::hasCurrent() const
{
return (!m_playNext.isEmpty()) || (!m_queue.isEmpty());
}
QJsonObject PlayQueue::current() const
{
if (!m_playNext.isEmpty()) return m_playNext.first();
if (m_index < m_queue.size()) return m_queue.at(m_index);
return {};
}
qint64 PlayQueue::currentId() const
{
return static_cast<qint64>(current()["id"].toDouble());
}
QJsonObject PlayQueue::advance()
{
if (!m_playNext.isEmpty()) {
// Return the playNext item directly — do NOT call current() after
// removal, as that would fall back to the already-playing m_index track.
const QJsonObject next = m_playNext.takeFirst();
emit queueChanged();
return next;
}
++m_index;
emit queueChanged();
return current();
}
QJsonObject PlayQueue::stepBack()
{
if (m_index > 0) --m_index;
emit queueChanged();
return current();
}
bool PlayQueue::canGoNext() const
{
return !m_playNext.isEmpty() || (m_index + 1 < m_queue.size());
}
void PlayQueue::setCurrentById(qint64 id)
{
m_playNext.clear();
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == id) {
m_index = i;
emit queueChanged();
return;
}
}
}
QVector<QJsonObject> PlayQueue::upcomingTracks(int maxCount) const
{
QVector<QJsonObject> result;
result.append(m_playNext);
for (int i = m_index + 1; i < m_queue.size() && result.size() < maxCount; ++i)
result.append(m_queue.at(i));
return result;
}
QJsonObject PlayQueue::skipToUpcoming(int upcomingIndex)
{
// Remove items 0..upcomingIndex-1 from the front of upcoming
for (int i = 0; i < upcomingIndex; ++i) {
if (!m_playNext.isEmpty())
m_playNext.removeFirst();
else if (m_index + 1 < m_queue.size())
++m_index;
}
// Pop and return the target (now at upcoming[0])
if (!m_playNext.isEmpty()) {
const QJsonObject t = m_playNext.takeFirst();
emit queueChanged();
return t;
}
if (m_index + 1 < m_queue.size()) {
++m_index;
emit queueChanged();
return m_queue.at(m_index);
}
emit queueChanged();
return {};
}
void PlayQueue::setUpcomingOrder(const QVector<QJsonObject> &newOrder)
{
m_playNext = newOrder;
m_queue.resize(m_index + 1); // drop old main-queue tail
emit queueChanged();
}
void PlayQueue::appendToContext(const QJsonArray &tracks)
{
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
emit queueChanged();
}
void PlayQueue::moveUpcomingToTop(int upcomingIndex)
{
if (upcomingIndex < 0) return;
QJsonObject track;
if (upcomingIndex < m_playNext.size()) {
if (upcomingIndex == 0) return; // already at top
track = m_playNext.takeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx >= m_queue.size()) return;
track = m_queue.takeAt(queueIdx);
}
m_playNext.prepend(track);
emit queueChanged();
}
void PlayQueue::shuffleQueue(int keepAtFront)
{
if (m_queue.isEmpty()) return;
// Keep the current track at index 0 of the remaining queue
if (keepAtFront >= 0 && keepAtFront < m_queue.size()) {
QJsonObject current = m_queue.takeAt(keepAtFront);
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
m_queue.prepend(current);
} else {
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
}
m_index = 0;
}

View File

@@ -4,8 +4,6 @@
#include <QVector> #include <QVector>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
#include <algorithm>
#include <random>
/// Local playback queue. Holds the ordered list of tracks for the current /// Local playback queue. Holds the ordered list of tracks for the current
/// context (album / playlist / search result / favourites) plus a separate /// context (album / playlist / search result / favourites) plus a separate
@@ -15,259 +13,83 @@ class PlayQueue : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit PlayQueue(QObject *parent = nullptr) : QObject(parent) {} explicit PlayQueue(QObject *parent = nullptr);
// ---- Loading a new context ---- // ---- Loading a new context ----
/// Replace the queue with all tracks from an album/playlist JSON context. /// Replace the queue with all tracks from an album/playlist JSON context.
/// @param startIndex Index of the track to start playing (-1 = first). /// @param startIndex Index of the track to start playing (-1 = first).
void setContext(const QJsonArray &tracks, int startIndex = 0) void setContext(const QJsonArray &tracks, int startIndex = 0);
{
m_queue.clear();
m_playNext.clear();
// Only queue streamable tracks; find the filtered index for startIndex
int filteredStart = 0;
int filteredIdx = 0;
bool found = false;
for (int orig = 0; orig < tracks.size(); ++orig) {
const QJsonObject t = tracks[orig].toObject();
if (!t["streamable"].toBool(true))
continue;
if (!found && orig >= startIndex) {
filteredStart = filteredIdx;
found = true;
}
m_queue.append(t);
++filteredIdx;
}
m_index = qBound(0, filteredStart, qMax(0, m_queue.size() - 1));
if (m_shuffle)
shuffleQueue(m_index);
emit queueChanged();
}
// ---- Re-order after a sort (keeps m_playNext, updates m_index) ---- // ---- Re-order after a sort (keeps m_playNext, updates m_index) ----
void reorderContext(const QJsonArray &tracks, qint64 currentId) void reorderContext(const QJsonArray &tracks, qint64 currentId);
{
m_queue.clear();
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
m_index = 0;
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == currentId) {
m_index = i;
break;
}
}
emit queueChanged();
}
// ---- Clear / remove upcoming ---- // ---- Clear / remove upcoming ----
/// Remove all "up next" entries (playNext + remaining main queue after current). /// Remove all "up next" entries (playNext + remaining main queue after current).
void clearUpcoming() void clearUpcoming();
{
m_playNext.clear();
if (m_index < m_queue.size())
m_queue.resize(m_index + 1); // keep up to and including current
emit queueChanged();
}
/// Remove one upcoming track by its index in upcomingTracks(). /// Remove one upcoming track by its index in upcomingTracks().
void removeUpcoming(int upcomingIndex) void removeUpcoming(int upcomingIndex);
{
if (upcomingIndex < m_playNext.size()) {
m_playNext.removeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx < m_queue.size())
m_queue.removeAt(queueIdx);
}
emit queueChanged();
}
// ---- Shuffle ---- // ---- Shuffle ----
bool shuffleEnabled() const { return m_shuffle; } bool shuffleEnabled() const { return m_shuffle; }
void setShuffle(bool enabled) void setShuffle(bool enabled);
{
if (m_shuffle == enabled) return;
m_shuffle = enabled;
if (enabled && !m_queue.isEmpty())
shuffleQueue(m_index);
emit queueChanged();
}
/// Shuffle the current queue once without changing the global shuffle flag. /// Shuffle the current queue once without changing the global shuffle flag.
void shuffleNow() void shuffleNow();
{
if (m_queue.isEmpty()) return;
shuffleQueue(m_index);
emit queueChanged();
}
// ---- Play-next prepend queue (like "Add to queue" ---- // ---- Play-next prepend queue (like "Add to queue" ----
void addToQueue(const QJsonObject &track) void addToQueue(const QJsonObject &track);
{
m_playNext.append(track);
emit queueChanged();
}
void playNext(const QJsonObject &track) void playNext(const QJsonObject &track);
{
m_playNext.prepend(track);
emit queueChanged();
}
// ---- Navigation ---- // ---- Navigation ----
bool hasCurrent() const bool hasCurrent() const;
{
return (!m_playNext.isEmpty()) || (!m_queue.isEmpty());
}
QJsonObject current() const QJsonObject current() const;
{
if (!m_playNext.isEmpty()) return m_playNext.first();
if (m_index < m_queue.size()) return m_queue.at(m_index);
return {};
}
qint64 currentId() const qint64 currentId() const;
{
return static_cast<qint64>(current()["id"].toDouble());
}
/// Advance and return the track to play next. Returns {} at end of queue. /// Advance and return the track to play next. Returns {} at end of queue.
QJsonObject advance() QJsonObject advance();
{
if (!m_playNext.isEmpty()) {
// Return the playNext item directly — do NOT call current() after
// removal, as that would fall back to the already-playing m_index track.
const QJsonObject next = m_playNext.takeFirst();
emit queueChanged();
return next;
}
++m_index;
emit queueChanged();
return current();
}
/// Step backwards in the main queue (play-next is not affected). /// Step backwards in the main queue (play-next is not affected).
QJsonObject stepBack() QJsonObject stepBack();
{
if (m_index > 0) --m_index;
emit queueChanged();
return current();
}
bool canGoNext() const bool canGoNext() const;
{
return !m_playNext.isEmpty() || (m_index + 1 < m_queue.size());
}
bool canGoPrev() const { return m_index > 0; } bool canGoPrev() const { return m_index > 0; }
// ---- Index lookup ---- // ---- Index lookup ----
/// Set the current position by track id (after user double-clicks a row). /// Set the current position by track id (after user double-clicks a row).
void setCurrentById(qint64 id) void setCurrentById(qint64 id);
{
m_playNext.clear();
for (int i = 0; i < m_queue.size(); ++i) {
if (static_cast<qint64>(m_queue[i]["id"].toDouble()) == id) {
m_index = i;
emit queueChanged();
return;
}
}
}
// ---- Accessors for queue panel ---- // ---- Accessors for queue panel ----
QVector<QJsonObject> upcomingTracks(int maxCount = 200) const QVector<QJsonObject> upcomingTracks(int maxCount = 200) const;
{
QVector<QJsonObject> result;
result.append(m_playNext);
for (int i = m_index + 1; i < m_queue.size() && result.size() < maxCount; ++i)
result.append(m_queue.at(i));
return result;
}
int playNextCount() const { return m_playNext.size(); } int playNextCount() const { return m_playNext.size(); }
int totalSize() const { return m_playNext.size() + m_queue.size(); } int totalSize() const { return m_playNext.size() + m_queue.size(); }
int currentIndex() const { return m_index; } int currentIndex() const { return m_index; }
/// Skip to upcoming[upcomingIndex]: removes everything before it, pops and returns it. /// Skip to upcoming[upcomingIndex]: removes everything before it, pops and returns it.
QJsonObject skipToUpcoming(int upcomingIndex) QJsonObject skipToUpcoming(int upcomingIndex);
{
// Remove items 0..upcomingIndex-1 from the front of upcoming
for (int i = 0; i < upcomingIndex; ++i) {
if (!m_playNext.isEmpty())
m_playNext.removeFirst();
else if (m_index + 1 < m_queue.size())
++m_index;
}
// Pop and return the target (now at upcoming[0])
if (!m_playNext.isEmpty()) {
const QJsonObject t = m_playNext.takeFirst();
emit queueChanged();
return t;
}
if (m_index + 1 < m_queue.size()) {
++m_index;
emit queueChanged();
return m_queue.at(m_index);
}
emit queueChanged();
return {};
}
/// Replace the upcoming list with a new order (used after drag-reorder in UI). /// Replace the upcoming list with a new order (used after drag-reorder in UI).
void setUpcomingOrder(const QVector<QJsonObject> &newOrder) void setUpcomingOrder(const QVector<QJsonObject> &newOrder);
{
m_playNext = newOrder;
m_queue.resize(m_index + 1); // drop old main-queue tail
emit queueChanged();
}
/// Append tracks to the main queue tail (autoplay/discovery). /// Append tracks to the main queue tail (autoplay/discovery).
void appendToContext(const QJsonArray &tracks) void appendToContext(const QJsonArray &tracks);
{
for (const auto &v : tracks) {
const QJsonObject t = v.toObject();
if (t["streamable"].toBool(true))
m_queue.append(t);
}
emit queueChanged();
}
/// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext. /// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext.
void moveUpcomingToTop(int upcomingIndex) void moveUpcomingToTop(int upcomingIndex);
{
if (upcomingIndex < 0) return;
QJsonObject track;
if (upcomingIndex < m_playNext.size()) {
if (upcomingIndex == 0) return; // already at top
track = m_playNext.takeAt(upcomingIndex);
} else {
const int queueIdx = m_index + 1 + (upcomingIndex - m_playNext.size());
if (queueIdx >= m_queue.size()) return;
track = m_queue.takeAt(queueIdx);
}
m_playNext.prepend(track);
emit queueChanged();
}
signals: signals:
void queueChanged(); void queueChanged();
@@ -278,19 +100,5 @@ private:
int m_index = 0; int m_index = 0;
bool m_shuffle = false; bool m_shuffle = false;
void shuffleQueue(int keepAtFront) void shuffleQueue(int keepAtFront);
{
if (m_queue.isEmpty()) return;
// Keep the current track at index 0 of the remaining queue
if (keepAtFront >= 0 && keepAtFront < m_queue.size()) {
QJsonObject current = m_queue.takeAt(keepAtFront);
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
m_queue.prepend(current);
} else {
std::mt19937 rng(std::random_device{}());
std::shuffle(m_queue.begin(), m_queue.end(), rng);
}
m_index = 0;
}
}; };

32
src/util/colors.hpp Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include <QColor>
namespace Colors {
// Brand accents
inline const QColor QobuzOrange{0xFF, 0xB2, 0x32};
inline const QColor QobuzBlue {0x46, 0xB3, 0xEE};
// Badge / indicator colors used in tree-view item foregrounds
inline const QColor BadgeGreen {QStringLiteral("#2FA84F")};
inline const QColor BadgeBlue {QStringLiteral("#2B7CD3")};
inline const QColor BadgeGray {QStringLiteral("#8E8E93")};
// Text
inline const QColor LightText {0xe8, 0xe8, 0xe8};
inline const QColor SubduedText {0xaa, 0xaa, 0xaa};
inline const QColor PlaceholderText{0x66, 0x66, 0x66};
inline const QColor DisabledText {0x55, 0x55, 0x55};
// Surfaces / backgrounds
inline const QColor WindowBg {0x19, 0x19, 0x19};
inline const QColor BaseBg {0x14, 0x14, 0x14};
inline const QColor AlternateBaseBg{0x1e, 0x1e, 0x1e};
inline const QColor ButtonSurface {0x2a, 0x2a, 0x2a};
inline const QColor ContextBg {0x1a, 0x1a, 0x1a};
inline const QColor MidSurface {0x2f, 0x2f, 0x2f};
inline const QColor DarkSurface {0x0e, 0x0e, 0x0e};
inline const QColor HighlightedFg {0x10, 0x10, 0x10};
} // namespace Colors

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "../util/colors.hpp"
#include <QTreeWidget> #include <QTreeWidget>
#include <QTreeWidgetItem> #include <QTreeWidgetItem>
#include <QHeaderView> #include <QHeaderView>
@@ -89,7 +91,7 @@ public:
auto *item = new QTreeWidgetItem(this); auto *item = new QTreeWidgetItem(this);
if (hiRes) { if (hiRes) {
item->setText(0, QStringLiteral("H")); item->setText(0, QStringLiteral("H"));
item->setForeground(0, QColor(QStringLiteral("#FFB232"))); item->setForeground(0, Colors::QobuzOrange);
item->setFont(0, hiResFont); item->setFont(0, hiResFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "../../backend/qobuzbackend.hpp" #include "../../backend/qobuzbackend.hpp"
#include "../../util/colors.hpp"
#include <QDockWidget> #include <QDockWidget>
#include <QWidget> #include <QWidget>
@@ -32,11 +33,11 @@ namespace Context
{ {
QPainter p(this); QPainter p(this);
if (m_pix.isNull()) { if (m_pix.isNull()) {
p.fillRect(rect(), QColor(0x1a, 0x1a, 0x1a)); p.fillRect(rect(), Colors::ContextBg);
return; return;
} }
const QPixmap scaled = m_pix.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); const QPixmap scaled = m_pix.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
p.fillRect(rect(), QColor(0x1a, 0x1a, 0x1a)); p.fillRect(rect(), Colors::ContextBg);
p.drawPixmap((width() - scaled.width()) / 2, p.drawPixmap((width() - scaled.width()) / 2,
(height() - scaled.height()) / 2, (height() - scaled.height()) / 2,
scaled); scaled);

View File

@@ -1,4 +1,5 @@
#include "genrebrowser.hpp" #include "genrebrowser.hpp"
#include "../util/colors.hpp"
#include <QAction> #include <QAction>
#include <QDialog> #include <QDialog>
@@ -787,7 +788,7 @@ void GenreBrowserView::setPlaylistItems(const QJsonArray &items, bool append)
auto *item = new QTreeWidgetItem(m_playlistList, auto *item = new QTreeWidgetItem(m_playlistList,
QStringList{QStringLiteral("P"), name, owner, tracksCount > 0 ? QString::number(tracksCount) : QString()}); QStringList{QStringLiteral("P"), name, owner, tracksCount > 0 ? QString::number(tracksCount) : QString()});
item->setData(0, Qt::UserRole, playlistId); item->setData(0, Qt::UserRole, playlistId);
item->setForeground(0, QColor(QStringLiteral("#2B7CD3"))); item->setForeground(0, Colors::BadgeBlue);
item->setFont(0, tagFont); item->setFont(0, tagFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
} }

View File

@@ -1,4 +1,5 @@
#include "view.hpp" #include "view.hpp"
#include "../../util/colors.hpp"
#include "../../util/trackinfo.hpp" #include "../../util/trackinfo.hpp"
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -125,7 +126,7 @@ void SearchTab::onMostPopularResult(const QJsonObject &result)
const QString artist = content["performer"].toObject()["name"].toString(); const QString artist = content["performer"].toObject()["name"].toString();
const QString album = content["album"].toObject()["title"].toString(); const QString album = content["album"].toObject()["title"].toString();
item->setText(0, QStringLiteral("T")); item->setText(0, QStringLiteral("T"));
item->setForeground(0, QColor(QStringLiteral("#2FA84F"))); item->setForeground(0, Colors::BadgeGreen);
item->setFont(0, badgeFont); item->setFont(0, badgeFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
item->setText(1, title); item->setText(1, title);
@@ -139,8 +140,8 @@ void SearchTab::onMostPopularResult(const QJsonObject &result)
|| content["rights"].toObject()["hires_streamable"].toBool(); || content["rights"].toObject()["hires_streamable"].toBool();
item->setText(0, hiRes ? QStringLiteral("H") : QStringLiteral("A")); item->setText(0, hiRes ? QStringLiteral("H") : QStringLiteral("A"));
item->setForeground(0, hiRes item->setForeground(0, hiRes
? QColor(QStringLiteral("#FFB232")) ? Colors::QobuzOrange
: QColor(QStringLiteral("#8E8E93"))); : Colors::BadgeGray);
item->setFont(0, badgeFont); item->setFont(0, badgeFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
item->setText(1, title); item->setText(1, title);
@@ -149,7 +150,7 @@ void SearchTab::onMostPopularResult(const QJsonObject &result)
item->setData(1, IdRole, content["id"].toString()); item->setData(1, IdRole, content["id"].toString());
} else if (type == QStringLiteral("artists")) { } else if (type == QStringLiteral("artists")) {
item->setText(0, QStringLiteral("A")); item->setText(0, QStringLiteral("A"));
item->setForeground(0, QColor(QStringLiteral("#2B7CD3"))); item->setForeground(0, Colors::BadgeBlue);
item->setFont(0, badgeFont); item->setFont(0, badgeFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
item->setText(1, content["name"].toString()); item->setText(1, content["name"].toString());
@@ -193,7 +194,7 @@ void SearchTab::onSearchResult(const QJsonObject &result)
QStringList{QString(), a["title"].toString(), artist}); QStringList{QString(), a["title"].toString(), artist});
if (hiRes) { if (hiRes) {
item->setText(0, QStringLiteral("H")); item->setText(0, QStringLiteral("H"));
item->setForeground(0, QColor(QStringLiteral("#FFB232"))); item->setForeground(0, Colors::QobuzOrange);
item->setFont(0, hiResFont); item->setFont(0, hiResFont);
item->setTextAlignment(0, Qt::AlignCenter); item->setTextAlignment(0, Qt::AlignCenter);
} }

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "../util/colors.hpp"
#include <QWidget> #include <QWidget>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -63,7 +65,7 @@ public:
m_meta = new QLabel(info); m_meta = new QLabel(info);
QPalette mp = m_meta->palette(); QPalette mp = m_meta->palette();
mp.setColor(QPalette::WindowText, QColor(0xaa, 0xaa, 0xaa)); mp.setColor(QPalette::WindowText, Colors::SubduedText);
m_meta->setPalette(mp); m_meta->setPalette(mp);
vlay->addWidget(m_meta); vlay->addWidget(m_meta);