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_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);

View File

@@ -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,

View File

@@ -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();

View File

@@ -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<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.
void moveUpcomingToTop(int upcomingIndex)
{

View File

@@ -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<QJsonObject> 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<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)

View File

@@ -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;
};