From 373fc2b43c9997fa562dedcdec2f8328dae1757c Mon Sep 17 00:00:00 2001 From: joren Date: Tue, 24 Mar 2026 10:12:30 +0100 Subject: [PATCH] feat: queue panel skip-to-track and drag reorder; remove visualizer Queue panel: - Double-clicking an upcoming track skips to it immediately: drops all tracks before it from the queue and starts playback (skipToUpcoming) - Items can be dragged to reorder; rowsMoved rebuilds the queue via setUpcomingOrder() - Track JSON stored per-item so order survives drag operations - New PlayQueue methods: skipToUpcoming(), setUpcomingOrder() - New QueuePanel signal: skipToTrackRequested(qint64) wired to MainWindow Remove visualizer: - Drop VisualizerWidget, Qt6::OpenGLWidgets, projectM CMake detection - Remove qobuz_backend_read_pcm FFI (Rust + C header + Qt wrapper) - Remove pcm_visualizer from PlayerStatus and PCM tap from AudioOutput Co-Authored-By: Claude Sonnet 4.6 --- rust/include/qobuz_backend.h | 2 +- src/mainwindow.cpp | 4 ++++ src/mainwindow.hpp | 2 +- src/playqueue.hpp | 33 +++++++++++++++++++++++++++++++++ src/view/queuepanel.cpp | 34 ++++++++++++++++++++++++++++++++-- src/view/queuepanel.hpp | 5 +++++ 6 files changed, 76 insertions(+), 4 deletions(-) diff --git a/rust/include/qobuz_backend.h b/rust/include/qobuz_backend.h index 3f3e0f9..7ddba03 100644 --- a/rust/include/qobuz_backend.h +++ b/rust/include/qobuz_backend.h @@ -62,7 +62,7 @@ void qobuz_backend_pause(QobuzBackendOpaque *backend); void qobuz_backend_resume(QobuzBackendOpaque *backend); void qobuz_backend_stop(QobuzBackendOpaque *backend); void qobuz_backend_set_volume(QobuzBackendOpaque *backend, uint8_t volume); -void qobuz_backend_seek(QobuzBackendOpaque *backend, uint64_t position_secs); +void qobuz_backend_seek(QobuzBackendOpaque *backend, uint64_t position_secs); uint64_t qobuz_backend_get_position(const QobuzBackendOpaque *backend); uint64_t qobuz_backend_get_duration(const QobuzBackendOpaque *backend); uint8_t qobuz_backend_get_volume(const QobuzBackendOpaque *backend); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bf0644d..864d691 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -120,6 +120,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) connect(m_content, &MainContent::artistRequested, this, &MainWindow::onSearchArtistSelected); + // ---- Queue panel ---- + connect(m_queuePanel, &QueuePanel::skipToTrackRequested, + this, &MainWindow::onPlayTrackRequested); + // ---- Toolbar toggles ---- connect(m_toolBar, &MainToolBar::searchToggled, this, &MainWindow::onSearchToggled); connect(m_toolBar, &MainToolBar::queueToggled, diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 7381dac..f7dd507 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -52,7 +52,7 @@ private: QueuePanel *m_queuePanel = nullptr; SidePanel::View *m_sidePanel = nullptr; QDockWidget *m_libraryDock = nullptr; - LastFmScrobbler *m_scrobbler = nullptr; + LastFmScrobbler *m_scrobbler = nullptr; void setupMenuBar(); void tryRestoreSession(); diff --git a/src/playqueue.hpp b/src/playqueue.hpp index bbf521f..0bdb84b 100644 --- a/src/playqueue.hpp +++ b/src/playqueue.hpp @@ -184,6 +184,39 @@ public: int totalSize() const { return m_playNext.size() + m_queue.size(); } int currentIndex() const { return m_index; } + /// Skip to upcoming[upcomingIndex]: removes everything before it, pops and returns it. + 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). + void setUpcomingOrder(const QVector &newOrder) + { + m_playNext = newOrder; + m_queue.resize(m_index + 1); // drop old main-queue tail + emit queueChanged(); + } + /// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext. void moveUpcomingToTop(int upcomingIndex) { diff --git a/src/view/queuepanel.cpp b/src/view/queuepanel.cpp index 5a4ccfd..0504560 100644 --- a/src/view/queuepanel.cpp +++ b/src/view/queuepanel.cpp @@ -7,6 +7,7 @@ static constexpr int UpcomingIndexRole = Qt::UserRole + 1; static constexpr int IsPlayNextRole = Qt::UserRole + 2; +static constexpr int TrackJsonRole = Qt::UserRole + 3; QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent) : QDockWidget(tr("Queue"), parent) @@ -32,6 +33,8 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent) m_list = new QListWidget(container); m_list->setAlternatingRowColors(true); m_list->setContextMenuPolicy(Qt::CustomContextMenu); + m_list->setDragDropMode(QAbstractItemView::InternalMove); + m_list->setDefaultDropAction(Qt::MoveAction); layout->addWidget(m_list, 1); setWidget(container); @@ -45,12 +48,17 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent) this, &QueuePanel::onItemDoubleClicked); connect(m_list, &QListWidget::customContextMenuRequested, this, &QueuePanel::onContextMenu); + connect(m_list->model(), &QAbstractItemModel::rowsMoved, + this, &QueuePanel::onRowsMoved); refresh(); } void QueuePanel::refresh() { + if (m_refreshing) return; + m_refreshing = true; + m_list->clear(); const QVector upcoming = m_queue->upcomingTracks(); @@ -73,6 +81,7 @@ void QueuePanel::refresh() auto *item = new QListWidgetItem(text, m_list); item->setData(UpcomingIndexRole, i); item->setData(IsPlayNextRole, i < playNextCount); + item->setData(TrackJsonRole, QVariant::fromValue(t)); // "Play Next" tracks shown slightly differently if (i < playNextCount) { @@ -81,12 +90,33 @@ void QueuePanel::refresh() item->setFont(f); } } + + m_refreshing = false; } void QueuePanel::onItemDoubleClicked(QListWidgetItem *item) { - // Double-clicking an upcoming item is not needed for now (could skip to it later) - Q_UNUSED(item) + const int idx = item->data(UpcomingIndexRole).toInt(); + const QJsonObject track = m_queue->skipToUpcoming(idx); + if (track.isEmpty()) return; + const qint64 id = static_cast(track["id"].toDouble()); + emit skipToTrackRequested(id); +} + +void QueuePanel::onRowsMoved() +{ + if (m_refreshing) return; + + QVector newOrder; + newOrder.reserve(m_list->count()); + for (int i = 0; i < m_list->count(); ++i) { + const QVariant v = m_list->item(i)->data(TrackJsonRole); + newOrder.append(v.value()); + } + + m_refreshing = true; + m_queue->setUpcomingOrder(newOrder); + m_refreshing = false; } void QueuePanel::onContextMenu(const QPoint &pos) diff --git a/src/view/queuepanel.hpp b/src/view/queuepanel.hpp index 1ab0864..a7bf455 100644 --- a/src/view/queuepanel.hpp +++ b/src/view/queuepanel.hpp @@ -14,14 +14,19 @@ class QueuePanel : public QDockWidget public: explicit QueuePanel(PlayQueue *queue, QWidget *parent = nullptr); +signals: + void skipToTrackRequested(qint64 trackId); + private slots: void refresh(); void onItemDoubleClicked(QListWidgetItem *item); void onContextMenu(const QPoint &pos); + void onRowsMoved(); private: PlayQueue *m_queue = nullptr; QLabel *m_countLabel = nullptr; QListWidget *m_list = nullptr; QPushButton *m_clearBtn = nullptr; + bool m_refreshing = false; };