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 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 10:12:30 +01:00
parent 74e43b9713
commit 373fc2b43c
6 changed files with 76 additions and 4 deletions

View File

@@ -62,7 +62,7 @@ void qobuz_backend_pause(QobuzBackendOpaque *backend);
void qobuz_backend_resume(QobuzBackendOpaque *backend); void qobuz_backend_resume(QobuzBackendOpaque *backend);
void qobuz_backend_stop(QobuzBackendOpaque *backend); void qobuz_backend_stop(QobuzBackendOpaque *backend);
void qobuz_backend_set_volume(QobuzBackendOpaque *backend, uint8_t volume); 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_position(const QobuzBackendOpaque *backend);
uint64_t qobuz_backend_get_duration(const QobuzBackendOpaque *backend); uint64_t qobuz_backend_get_duration(const QobuzBackendOpaque *backend);
uint8_t qobuz_backend_get_volume(const QobuzBackendOpaque *backend); uint8_t qobuz_backend_get_volume(const QobuzBackendOpaque *backend);

View File

@@ -120,6 +120,10 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
connect(m_content, &MainContent::artistRequested, connect(m_content, &MainContent::artistRequested,
this, &MainWindow::onSearchArtistSelected); this, &MainWindow::onSearchArtistSelected);
// ---- Queue panel ----
connect(m_queuePanel, &QueuePanel::skipToTrackRequested,
this, &MainWindow::onPlayTrackRequested);
// ---- Toolbar toggles ---- // ---- Toolbar toggles ----
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,

View File

@@ -52,7 +52,7 @@ private:
QueuePanel *m_queuePanel = nullptr; QueuePanel *m_queuePanel = nullptr;
SidePanel::View *m_sidePanel = nullptr; SidePanel::View *m_sidePanel = nullptr;
QDockWidget *m_libraryDock = nullptr; QDockWidget *m_libraryDock = nullptr;
LastFmScrobbler *m_scrobbler = nullptr; LastFmScrobbler *m_scrobbler = nullptr;
void setupMenuBar(); void setupMenuBar();
void tryRestoreSession(); void tryRestoreSession();

View File

@@ -184,6 +184,39 @@ public:
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.
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<QJsonObject> &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. /// Move an upcoming item (by its index in upcomingTracks()) to the front of playNext.
void moveUpcomingToTop(int upcomingIndex) void moveUpcomingToTop(int upcomingIndex)
{ {

View File

@@ -7,6 +7,7 @@
static constexpr int UpcomingIndexRole = Qt::UserRole + 1; static constexpr int UpcomingIndexRole = Qt::UserRole + 1;
static constexpr int IsPlayNextRole = Qt::UserRole + 2; static constexpr int IsPlayNextRole = Qt::UserRole + 2;
static constexpr int TrackJsonRole = Qt::UserRole + 3;
QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent) QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
: QDockWidget(tr("Queue"), parent) : QDockWidget(tr("Queue"), parent)
@@ -32,6 +33,8 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
m_list = new QListWidget(container); m_list = new QListWidget(container);
m_list->setAlternatingRowColors(true); m_list->setAlternatingRowColors(true);
m_list->setContextMenuPolicy(Qt::CustomContextMenu); m_list->setContextMenuPolicy(Qt::CustomContextMenu);
m_list->setDragDropMode(QAbstractItemView::InternalMove);
m_list->setDefaultDropAction(Qt::MoveAction);
layout->addWidget(m_list, 1); layout->addWidget(m_list, 1);
setWidget(container); setWidget(container);
@@ -45,12 +48,17 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
this, &QueuePanel::onItemDoubleClicked); this, &QueuePanel::onItemDoubleClicked);
connect(m_list, &QListWidget::customContextMenuRequested, connect(m_list, &QListWidget::customContextMenuRequested,
this, &QueuePanel::onContextMenu); this, &QueuePanel::onContextMenu);
connect(m_list->model(), &QAbstractItemModel::rowsMoved,
this, &QueuePanel::onRowsMoved);
refresh(); refresh();
} }
void QueuePanel::refresh() void QueuePanel::refresh()
{ {
if (m_refreshing) return;
m_refreshing = true;
m_list->clear(); m_list->clear();
const QVector<QJsonObject> upcoming = m_queue->upcomingTracks(); const QVector<QJsonObject> upcoming = m_queue->upcomingTracks();
@@ -73,6 +81,7 @@ void QueuePanel::refresh()
auto *item = new QListWidgetItem(text, m_list); auto *item = new QListWidgetItem(text, m_list);
item->setData(UpcomingIndexRole, i); item->setData(UpcomingIndexRole, i);
item->setData(IsPlayNextRole, i < playNextCount); item->setData(IsPlayNextRole, i < playNextCount);
item->setData(TrackJsonRole, QVariant::fromValue(t));
// "Play Next" tracks shown slightly differently // "Play Next" tracks shown slightly differently
if (i < playNextCount) { if (i < playNextCount) {
@@ -81,12 +90,33 @@ void QueuePanel::refresh()
item->setFont(f); item->setFont(f);
} }
} }
m_refreshing = false;
} }
void QueuePanel::onItemDoubleClicked(QListWidgetItem *item) void QueuePanel::onItemDoubleClicked(QListWidgetItem *item)
{ {
// Double-clicking an upcoming item is not needed for now (could skip to it later) const int idx = item->data(UpcomingIndexRole).toInt();
Q_UNUSED(item) const QJsonObject track = m_queue->skipToUpcoming(idx);
if (track.isEmpty()) return;
const qint64 id = static_cast<qint64>(track["id"].toDouble());
emit skipToTrackRequested(id);
}
void QueuePanel::onRowsMoved()
{
if (m_refreshing) return;
QVector<QJsonObject> 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<QJsonObject>());
}
m_refreshing = true;
m_queue->setUpcomingOrder(newOrder);
m_refreshing = false;
} }
void QueuePanel::onContextMenu(const QPoint &pos) void QueuePanel::onContextMenu(const QPoint &pos)

View File

@@ -14,14 +14,19 @@ class QueuePanel : public QDockWidget
public: public:
explicit QueuePanel(PlayQueue *queue, QWidget *parent = nullptr); explicit QueuePanel(PlayQueue *queue, QWidget *parent = nullptr);
signals:
void skipToTrackRequested(qint64 trackId);
private slots: private slots:
void refresh(); void refresh();
void onItemDoubleClicked(QListWidgetItem *item); void onItemDoubleClicked(QListWidgetItem *item);
void onContextMenu(const QPoint &pos); void onContextMenu(const QPoint &pos);
void onRowsMoved();
private: private:
PlayQueue *m_queue = nullptr; PlayQueue *m_queue = nullptr;
QLabel *m_countLabel = nullptr; QLabel *m_countLabel = nullptr;
QListWidget *m_list = nullptr; QListWidget *m_list = nullptr;
QPushButton *m_clearBtn = nullptr; QPushButton *m_clearBtn = nullptr;
bool m_refreshing = false;
}; };