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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user