refactor: MainWindow class into multiple items / controllers ; fix: Bug with PlaylistBrowserDialog.cpp : List item "No playlists available" was selectable
This commit is contained in:
+9
-23
@@ -1,70 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(SubWave LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# dependencies
|
||||
find_package(Qt6 REQUIRED COMPONENTS
|
||||
Core Widgets Multimedia OpenGL OpenGLWidgets
|
||||
)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
|
||||
libavformat
|
||||
libavcodec
|
||||
libavutil
|
||||
libswresample
|
||||
)
|
||||
|
||||
pkg_check_modules(FFTW REQUIRED IMPORTED_TARGET fftw3)
|
||||
|
||||
# sources
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/config/Config.cpp
|
||||
|
||||
src/gui/CoverArtWidget.cpp
|
||||
src/gui/SpectrogramWidget.cpp
|
||||
src/gui/GradientDialog.cpp
|
||||
src/gui/EqualizerWidget.cpp
|
||||
src/gui/TrackDelegate.cpp
|
||||
src/gui/MainWindow.cpp
|
||||
|
||||
src/gui/NowPlayingPanel.cpp
|
||||
# controllers
|
||||
src/controllers/PlaybackController.cpp
|
||||
src/controllers/LibraryController.cpp
|
||||
# engine
|
||||
src/engine/AudioEngine.cpp
|
||||
src/engine/EqProcessor.cpp
|
||||
src/engine/SpectrumAnalyzer.cpp
|
||||
src/engine/MusicLibrary.cpp
|
||||
src/engine/MetadataReader.cpp
|
||||
|
||||
# ffmpeg
|
||||
src/engine/ffmpeg/FormatContext.cpp
|
||||
src/engine/ffmpeg/CodecContext.cpp
|
||||
src/engine/ffmpeg/Resampler.cpp
|
||||
src/engine/ffmpeg/FFmpegDecoder.cpp
|
||||
|
||||
# playlists
|
||||
src/playlists/PlaylistModel.cpp
|
||||
src/playlists/PlaylistManager.cpp
|
||||
src/playlists/PlaylistCreatorDialog.cpp
|
||||
src/playlists/PlaylistBrowserDialog.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
src/config/Config.h
|
||||
|
||||
src/gui/CoverArtWidget.h
|
||||
src/gui/SpectrogramWidget.h
|
||||
src/gui/GradientDialog.h
|
||||
src/gui/EqualizerWidget.h
|
||||
src/gui/TrackDelegate.h
|
||||
src/gui/MainWindow.h
|
||||
|
||||
src/gui/NowPlayingPanel.h
|
||||
# controllers
|
||||
src/controllers/PlaybackController.h
|
||||
src/controllers/LibraryController.h
|
||||
# engine
|
||||
src/engine/AudioEngine.h
|
||||
src/engine/EqProcessor.h
|
||||
@@ -73,50 +67,43 @@ set(HEADERS
|
||||
src/engine/Track.h
|
||||
src/engine/MetadataReader.h
|
||||
src/engine/MusicLibrary.h
|
||||
|
||||
# ffmpeg wrapper
|
||||
src/engine/ffmpeg/FormatContext.h
|
||||
src/engine/ffmpeg/CodecContext.h
|
||||
src/engine/ffmpeg/Resampler.h
|
||||
src/engine/ffmpeg/FFmpegDecoder.h
|
||||
|
||||
# playlists
|
||||
src/playlists/PlaylistModel.h
|
||||
src/playlists/PlaylistManager.h
|
||||
src/playlists/PlaylistCreatorDialog.h
|
||||
src/playlists/PlaylistBrowserDialog.h
|
||||
)
|
||||
|
||||
# qrc resources
|
||||
set(RESOURCES
|
||||
src/shaders.qrc
|
||||
src/application.qrc
|
||||
)
|
||||
|
||||
# target
|
||||
add_executable(SubWave ${SOURCES} ${HEADERS} ${RESOURCES})
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.")
|
||||
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build." FORCE)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(SubWave PRIVATE /O2 /fp:fast)
|
||||
else()
|
||||
target_compile_options(SubWave PRIVATE -O3 -ffast-math -march=native)
|
||||
endif()
|
||||
|
||||
# headers
|
||||
target_include_directories(SubWave PRIVATE
|
||||
src
|
||||
src/config
|
||||
src/controllers
|
||||
src/engine
|
||||
src/engine/ffmpeg
|
||||
src/playlists
|
||||
src/gui
|
||||
)
|
||||
|
||||
# link to qt and ffmpeg and fftw
|
||||
target_link_libraries(SubWave PRIVATE
|
||||
Qt6::Core
|
||||
@@ -127,7 +114,6 @@ target_link_libraries(SubWave PRIVATE
|
||||
PkgConfig::FFMPEG
|
||||
PkgConfig::FFTW
|
||||
)
|
||||
|
||||
# For windows stupid ahh
|
||||
if(WIN32)
|
||||
set_target_properties(SubWave PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "LibraryController.h"
|
||||
#include "AudioEngine.h"
|
||||
#include "engine/MusicLibrary.h"
|
||||
#include "PlaylistModel.h"
|
||||
#include "PlaylistManager.h"
|
||||
#include "PlaylistCreatorDialog.h"
|
||||
#include "PlaylistBrowserDialog.h"
|
||||
#include "Config.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
|
||||
LibraryController::LibraryController(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_library(new engine::MusicLibrary(this))
|
||||
, m_model(new PlaylistModel(this))
|
||||
{
|
||||
m_library->setRootPath(Config::loadMusicRoot());
|
||||
|
||||
connect(m_library, &engine::MusicLibrary::scanStarted, this, [this] {
|
||||
m_model->setTracks({});
|
||||
emit statusMessage("Scanning...");
|
||||
emit scanStarted();
|
||||
});
|
||||
|
||||
connect(m_library, &engine::MusicLibrary::trackFound,
|
||||
this, [this](const engine::Track &t) {
|
||||
m_model->appendTrack(t);
|
||||
int n = m_model->rowCount({});
|
||||
emit statusMessage(QString("%1 tracks found").arg(n));
|
||||
emit trackCountChanged(n);
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(m_library, &engine::MusicLibrary::scanComplete,
|
||||
this, [this](int n) {
|
||||
m_model->setTracks(m_library->tracks());
|
||||
emit statusMessage(QString("%1 tracks.").arg(n));
|
||||
emit trackCountChanged(n);
|
||||
emit scanComplete(n);
|
||||
}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void LibraryController::scan()
|
||||
{
|
||||
m_library->scan();
|
||||
}
|
||||
|
||||
void LibraryController::setRootPath(const QString &path)
|
||||
{
|
||||
m_library->setRootPath(path);
|
||||
Config::saveMusicRoot(path);
|
||||
}
|
||||
|
||||
void LibraryController::onPlayRequested(int index)
|
||||
{
|
||||
if (!m_engine) return;
|
||||
engine::Track *t = m_model->trackAt(index);
|
||||
if (!t) return;
|
||||
m_model->setCurrentIndex(index);
|
||||
m_engine->play(*t);
|
||||
emit trackChanged(t, index);
|
||||
}
|
||||
|
||||
void LibraryController::createPlaylist()
|
||||
{
|
||||
PlaylistCreatorDialog dlg(
|
||||
qobject_cast<QWidget *>(parent()));
|
||||
if (dlg.exec() == QDialog::Accepted)
|
||||
loadPlaylistByName(dlg.getPlaylist().name);
|
||||
}
|
||||
|
||||
void LibraryController::loadPlaylist()
|
||||
{
|
||||
PlaylistBrowserDialog dlg(
|
||||
qobject_cast<QWidget *>(parent()));
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
QString name = dlg.getSelectedPlaylist();
|
||||
if (!name.isEmpty())
|
||||
loadPlaylistByName(name);
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryController::loadPlaylistByName(const QString &name)
|
||||
{
|
||||
auto playlist = PlaylistManager::loadPlaylist(name);
|
||||
if (playlist.tracks.isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
qobject_cast<QWidget *>(parent()),
|
||||
"Empty Playlist",
|
||||
QString("Playlist \"%1\" is empty.").arg(name));
|
||||
return;
|
||||
}
|
||||
m_model->setTracks(playlist.tracks);
|
||||
int n = playlist.tracks.size();
|
||||
emit trackCountChanged(n);
|
||||
emit statusMessage(
|
||||
QString("Playlist: %1 (%2 tracks)").arg(name).arg(n));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include "MusicLibrary.h"
|
||||
|
||||
namespace engine { struct Track; }
|
||||
class PlaylistModel;
|
||||
class AudioEngine;
|
||||
|
||||
class LibraryController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LibraryController(QObject *parent = nullptr);
|
||||
|
||||
PlaylistModel *model() const { return m_model; }
|
||||
engine::MusicLibrary *library() const { return m_library; }
|
||||
|
||||
void setEngine(AudioEngine *engine) { m_engine = engine; }
|
||||
|
||||
public slots:
|
||||
void scan();
|
||||
void setRootPath(const QString &path);
|
||||
|
||||
void onPlayRequested(int index);
|
||||
|
||||
void createPlaylist(); // opens PlaylistCreatorDialog
|
||||
void loadPlaylist(); // opens PlaylistBrowserDialog
|
||||
|
||||
signals:
|
||||
void scanStarted();
|
||||
void trackCountChanged(int n); // PlaybackController::setTrackCount
|
||||
void scanComplete(int n);
|
||||
void statusMessage(QString msg);
|
||||
|
||||
void trackChanged(const engine::Track *track, int index);
|
||||
|
||||
private:
|
||||
void loadPlaylistByName(const QString &name);
|
||||
|
||||
engine::MusicLibrary *m_library;
|
||||
PlaylistModel *m_model;
|
||||
AudioEngine *m_engine { nullptr };
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "PlaybackController.h"
|
||||
#include "AudioEngine.h"
|
||||
#include <QRandomGenerator>
|
||||
|
||||
PlaybackController::PlaybackController(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_engine(new AudioEngine(this))
|
||||
{
|
||||
connect(m_engine, &AudioEngine::trackEnded,
|
||||
this, &PlaybackController::onTrackEnded, Qt::QueuedConnection);
|
||||
|
||||
connect(m_engine, &AudioEngine::positionChanged,
|
||||
this, [this](qint64 posFrames) {
|
||||
emit positionChanged(posFrames,
|
||||
m_engine->totalFrames(),
|
||||
float(m_engine->sampleRate()));
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(m_engine, &AudioEngine::fftReady,
|
||||
this, &PlaybackController::fftReady, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void PlaybackController::playTrack(int index)
|
||||
{
|
||||
m_currentIndex = index;
|
||||
emit playRequested(index);
|
||||
emit playbackStarted();
|
||||
}
|
||||
|
||||
void PlaybackController::togglePlayPause()
|
||||
{
|
||||
if (m_engine->state() == AudioEngine::State::Stopped) {
|
||||
int start = (m_currentIndex >= 0) ? m_currentIndex : 0;
|
||||
if (m_trackCount > 0)
|
||||
playTrack(start);
|
||||
return;
|
||||
}
|
||||
m_engine->togglePause();
|
||||
bool paused = (m_engine->state() == AudioEngine::State::Paused);
|
||||
emit (paused ? playbackPaused() : playbackStarted());
|
||||
}
|
||||
|
||||
void PlaybackController::playNext()
|
||||
{
|
||||
if (m_trackCount == 0) return;
|
||||
if (m_repeat && m_currentIndex >= 0) { playTrack(m_currentIndex); return; }
|
||||
int next = m_shuffle
|
||||
? QRandomGenerator::global()->bounded(m_trackCount)
|
||||
: ((m_currentIndex + 1) % m_trackCount);
|
||||
playTrack(next);
|
||||
}
|
||||
|
||||
void PlaybackController::playPrev()
|
||||
{
|
||||
if (m_currentIndex > 0)
|
||||
playTrack(m_currentIndex - 1);
|
||||
}
|
||||
|
||||
void PlaybackController::stop()
|
||||
{
|
||||
m_engine->stop();
|
||||
emit playbackStopped();
|
||||
}
|
||||
|
||||
void PlaybackController::seekFraction(double f)
|
||||
{
|
||||
m_engine->seekFraction(f);
|
||||
}
|
||||
|
||||
void PlaybackController::setVolume(int percent)
|
||||
{
|
||||
m_engine->setVolume(percent / 100.f);
|
||||
}
|
||||
|
||||
void PlaybackController::onTrackEnded()
|
||||
{
|
||||
playNext();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
class AudioEngine;
|
||||
namespace engine { struct Track; }
|
||||
|
||||
class PlaybackController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PlaybackController(QObject *parent = nullptr);
|
||||
|
||||
AudioEngine *engine() const { return m_engine; }
|
||||
|
||||
// ui state
|
||||
bool shuffleEnabled() const { return m_shuffle; }
|
||||
bool repeatEnabled() const { return m_repeat; }
|
||||
int currentIndex() const { return m_currentIndex; }
|
||||
int trackCount() const { return m_trackCount; }
|
||||
|
||||
public slots:
|
||||
// called by mainwindow to update track index
|
||||
void setTrackCount(int n) { m_trackCount = n; }
|
||||
|
||||
// Transport
|
||||
void playTrack(int index); // emits playRequested(index) for LibraryController to resolve
|
||||
void togglePlayPause();
|
||||
void playNext();
|
||||
void playPrev();
|
||||
void stop();
|
||||
void seekFraction(double f);
|
||||
void setVolume(int percent); // 0-100
|
||||
|
||||
void setShuffle(bool on) { m_shuffle = on; }
|
||||
void setRepeat(bool on) { m_repeat = on; }
|
||||
|
||||
signals:
|
||||
void playRequested(int index);
|
||||
void playbackStarted();
|
||||
void playbackPaused();
|
||||
void playbackStopped();
|
||||
void positionChanged(qint64 posFrames, qint64 totalFrames, float sampleRate);
|
||||
void fftReady(const QVector<float> &bins);
|
||||
|
||||
private slots:
|
||||
void onTrackEnded();
|
||||
|
||||
private:
|
||||
AudioEngine *m_engine;
|
||||
int m_currentIndex { -1 };
|
||||
int m_trackCount { 0 };
|
||||
bool m_shuffle { false };
|
||||
bool m_repeat { false };
|
||||
};
|
||||
+117
-309
@@ -1,165 +1,111 @@
|
||||
#include "MainWindow.h"
|
||||
#include "AudioEngine.h"
|
||||
#include "engine/MusicLibrary.h"
|
||||
#include "PlaylistModel.h"
|
||||
#include "TrackDelegate.h"
|
||||
#include "CoverArtWidget.h"
|
||||
#include "PlaybackController.h"
|
||||
#include "LibraryController.h"
|
||||
#include "NowPlayingPanel.h"
|
||||
#include "SpectrogramWidget.h"
|
||||
#include "EqualizerWidget.h"
|
||||
#include "PlaylistModel.h"
|
||||
#include "TrackDelegate.h"
|
||||
#include "MusicLibrary.h"
|
||||
#include "Config.h"
|
||||
#include "PlaylistCreatorDialog.h"
|
||||
#include "PlaylistBrowserDialog.h"
|
||||
#include "PlaylistManager.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QListView>
|
||||
#include <QLabel>
|
||||
#include <QTabWidget>
|
||||
#include <QMenuBar>
|
||||
#include <QAction>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QInputDialog>
|
||||
#include <QCloseEvent>
|
||||
#include <QFileDialog>
|
||||
#include <QCloseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QRandomGenerator>
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_playback(new PlaybackController(this))
|
||||
, m_library(new LibraryController(this))
|
||||
{
|
||||
setWindowTitle("SubWave");
|
||||
setMinimumSize(700, 760);
|
||||
|
||||
m_engine = new AudioEngine(this);
|
||||
m_library = new engine::MusicLibrary(this);
|
||||
m_model = new PlaylistModel(this);
|
||||
m_library->setRootPath(Config::loadMusicRoot());
|
||||
m_library->setEngine(m_playback->engine());
|
||||
|
||||
buildUI();
|
||||
wireSignals();
|
||||
|
||||
m_uiTimer.setInterval(500);
|
||||
connect(&m_uiTimer, &QTimer::timeout, this, &MainWindow::onUiTimer);
|
||||
connect(&m_uiTimer, &QTimer::timeout, this, [this] {
|
||||
auto *e = m_playback->engine();
|
||||
m_nowPlaying->updatePosition(
|
||||
e->positionFrames(),
|
||||
e->totalFrames(),
|
||||
float(e->sampleRate()));
|
||||
|
||||
if (e->state() == AudioEngine::State::Stopped)
|
||||
m_nowPlaying->setPlaybackActive(false);
|
||||
});
|
||||
m_uiTimer.start();
|
||||
|
||||
reloadLibrary();
|
||||
m_library->scan();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
m_eqWidget->saveState();
|
||||
m_engine->stop();
|
||||
m_library->shutdown();
|
||||
m_playback->stop();
|
||||
m_library->library()->shutdown();
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e)
|
||||
{
|
||||
m_eqWidget->saveState();
|
||||
m_engine->stop();
|
||||
m_library->shutdown();
|
||||
m_playback->stop();
|
||||
m_library->library()->shutdown();
|
||||
e->accept();
|
||||
}
|
||||
|
||||
// ── UI Construction ─────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::buildUI()
|
||||
{
|
||||
auto *central = new QWidget;
|
||||
setCentralWidget(central);
|
||||
auto *central = new QWidget;
|
||||
auto *mainVlay = new QVBoxLayout(central);
|
||||
mainVlay->setSpacing(4);
|
||||
mainVlay->setContentsMargins(4, 4, 4, 4);
|
||||
setCentralWidget(central);
|
||||
|
||||
mainVlay->addWidget(buildNowPlayingPanel());
|
||||
m_nowPlaying = new NowPlayingPanel;
|
||||
mainVlay->addWidget(m_nowPlaying);
|
||||
|
||||
m_spectrogram = new SpectrogramWidget;
|
||||
mainVlay->addWidget(m_spectrogram, 1);
|
||||
|
||||
mainVlay->addWidget(buildBottomTabs());
|
||||
|
||||
buildMenuBar();
|
||||
}
|
||||
|
||||
QGroupBox *MainWindow::buildNowPlayingPanel()
|
||||
{
|
||||
auto *box = new QGroupBox("Now Playing");
|
||||
auto *hlay = new QHBoxLayout(box);
|
||||
|
||||
m_coverWidget = new CoverArtWidget;
|
||||
m_coverWidget->setFixedSize(130, 130);
|
||||
hlay->addWidget(m_coverWidget);
|
||||
|
||||
auto *right = new QVBoxLayout;
|
||||
right->setSpacing(4);
|
||||
|
||||
QFont boldF = m_lblTitle->font();
|
||||
boldF.setBold(true);
|
||||
boldF.setPointSizeF(11);
|
||||
m_lblTitle->setFont(boldF);
|
||||
right->addWidget(m_lblTitle);
|
||||
right->addWidget(m_lblArtist);
|
||||
right->addWidget(m_lblAlbum);
|
||||
|
||||
// Seek row
|
||||
auto *seekRow = new QHBoxLayout;
|
||||
m_seekBar->setRange(0, 1000);
|
||||
m_seekBar->setValue(0);
|
||||
seekRow->addWidget(m_seekBar);
|
||||
seekRow->addWidget(m_lblTime);
|
||||
right->addLayout(seekRow);
|
||||
|
||||
right->addLayout(buildTransportRow());
|
||||
hlay->addLayout(right, 1);
|
||||
return box;
|
||||
}
|
||||
|
||||
QHBoxLayout *MainWindow::buildTransportRow()
|
||||
{
|
||||
auto *row = new QHBoxLayout;
|
||||
auto *btns = new QHBoxLayout;
|
||||
btns->setSpacing(3);
|
||||
btns->addWidget(m_btnPrev);
|
||||
btns->addWidget(m_btnPlay);
|
||||
btns->addWidget(m_btnNext);
|
||||
btns->addSpacing(8);
|
||||
btns->addWidget(m_chkShuffle);
|
||||
btns->addWidget(m_chkRepeat);
|
||||
row->addLayout(btns);
|
||||
row->addStretch();
|
||||
|
||||
row->addWidget(new QLabel("Volume:"));
|
||||
m_volSlider->setRange(0, 100);
|
||||
m_volSlider->setValue(80);
|
||||
m_volSlider->setFixedWidth(90);
|
||||
row->addWidget(m_volSlider);
|
||||
return row;
|
||||
}
|
||||
|
||||
QTabWidget *MainWindow::buildBottomTabs()
|
||||
{
|
||||
auto *tabs = new QTabWidget;
|
||||
|
||||
// Playlist tab
|
||||
auto *plPanel = new QWidget;
|
||||
auto *plVlay = new QVBoxLayout(plPanel);
|
||||
plVlay->setContentsMargins(0, 2, 0, 0);
|
||||
|
||||
m_playlistView = new QListView;
|
||||
m_playlistView->setModel(m_model);
|
||||
m_playlistView->setModel(m_library->model());
|
||||
m_playlistView->setItemDelegate(new TrackDelegate(this));
|
||||
m_playlistView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_playlistView->setUniformItemSizes(true);
|
||||
m_playlistView->setAlternatingRowColors(true);
|
||||
m_playlistView->setMinimumHeight(220);
|
||||
|
||||
m_lblStatus = new QLabel("No library loaded.");
|
||||
plVlay->addWidget(m_playlistView);
|
||||
plVlay->addWidget(m_lblStatus);
|
||||
tabs->addTab(plPanel, "Playlist");
|
||||
|
||||
// EQ tab
|
||||
m_eqWidget = new EqualizerWidget(m_engine);
|
||||
m_eqWidget = new EqualizerWidget(m_playback->engine());
|
||||
tabs->addTab(m_eqWidget, "Equalizer");
|
||||
|
||||
return tabs;
|
||||
mainVlay->addWidget(tabs);
|
||||
|
||||
buildMenuBar();
|
||||
}
|
||||
|
||||
void MainWindow::buildMenuBar()
|
||||
@@ -167,253 +113,115 @@ void MainWindow::buildMenuBar()
|
||||
auto *mb = menuBar();
|
||||
auto *file = mb->addMenu("&File");
|
||||
|
||||
auto *chooseAct = file->addAction("Choose Music Location…");
|
||||
auto *chooseAct = file->addAction("Choose Music Location...");
|
||||
chooseAct->setShortcut(QKeySequence::Open);
|
||||
connect(chooseAct, &QAction::triggered, this, [this]{
|
||||
connect(chooseAct, &QAction::triggered, this, [this] {
|
||||
QString dir = QFileDialog::getExistingDirectory(
|
||||
this, "Select Music Folder",
|
||||
m_library->rootPath(),
|
||||
m_library->library()->rootPath(),
|
||||
QFileDialog::ShowDirsOnly);
|
||||
if (dir.isEmpty()) return;
|
||||
m_library->setRootPath(dir);
|
||||
Config::saveMusicRoot(dir);
|
||||
setWindowTitle("SubWave - " + QDir(dir).dirName());
|
||||
reloadLibrary();
|
||||
m_library->scan();
|
||||
});
|
||||
|
||||
auto *refreshAct = file->addAction("Refresh Library");
|
||||
refreshAct->setShortcut(Qt::Key_F5);
|
||||
connect(refreshAct, &QAction::triggered, this, &MainWindow::reloadLibrary);
|
||||
connect(refreshAct, &QAction::triggered, m_library, &LibraryController::scan);
|
||||
|
||||
file->addSeparator();
|
||||
auto *quitAct = file->addAction("Quit");
|
||||
quitAct->setShortcut(QKeySequence::Quit);
|
||||
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
|
||||
|
||||
// Playlists menu
|
||||
auto *playlists = mb->addMenu("&Playlists");
|
||||
|
||||
auto *newPlaylistAct = playlists->addAction("Create Playlist…");
|
||||
connect(newPlaylistAct, &QAction::triggered, this, &MainWindow::onCreatePlaylist);
|
||||
|
||||
auto *loadPlaylistAct = playlists->addAction("Load Playlist…");
|
||||
connect(loadPlaylistAct, &QAction::triggered, this, &MainWindow::onLoadPlaylist);
|
||||
auto *plMenu = mb->addMenu("&Playlists");
|
||||
connect(plMenu->addAction("Create Playlist…"), &QAction::triggered,
|
||||
m_library, &LibraryController::createPlaylist);
|
||||
connect(plMenu->addAction("Load Playlist…"), &QAction::triggered,
|
||||
m_library, &LibraryController::loadPlaylist);
|
||||
|
||||
auto *help = mb->addMenu("&Help");
|
||||
auto *about = help->addAction("About SubWave");
|
||||
connect(about, &QAction::triggered, this, [this]{
|
||||
QMessageBox::about(this, "About SubWave",
|
||||
"SubWave - Qt6/FFmpeg music player\n\n"
|
||||
"Supports MP3, FLAC, WAV, OGG, AAC, M4A, Opus, WMA.\n"
|
||||
"Features a 10-band parametric EQ with user presets,\n"
|
||||
"real-time spectrogram & waterfall display, shuffle,\n"
|
||||
"repeat, and persistent library + EQ state.\n\n"
|
||||
"Config: ~/.subwave/");
|
||||
connect(about, &QAction::triggered, this, [this] {
|
||||
QMessageBox::about(this, "SubWave",
|
||||
"SubWave\n"
|
||||
"Its a music player.\n\n"
|
||||
"Thats it.\n"
|
||||
"~/.subwave/");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Signal/Slot Wiring ──────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::wireSignals()
|
||||
{
|
||||
// Transport buttons
|
||||
connect(m_btnPlay, &QPushButton::clicked, this, &MainWindow::togglePlayPause);
|
||||
/*connect(m_btnStop, &QPushButton::clicked, this, [this]{
|
||||
m_engine->stop();
|
||||
m_btnPlay->setText("Play");
|
||||
});*/
|
||||
connect(m_btnPrev, &QPushButton::clicked, this, &MainWindow::playPrev);
|
||||
connect(m_btnNext, &QPushButton::clicked, this, &MainWindow::playNext);
|
||||
connect(m_volSlider, &QSlider::valueChanged, this, [this](int v){
|
||||
m_engine->setVolume(v / 100.f);
|
||||
});
|
||||
connect(m_nowPlaying, &NowPlayingPanel::playPauseClicked,
|
||||
m_playback, &PlaybackController::togglePlayPause);
|
||||
connect(m_nowPlaying, &NowPlayingPanel::prevClicked,
|
||||
m_playback, &PlaybackController::playPrev);
|
||||
connect(m_nowPlaying, &NowPlayingPanel::nextClicked,
|
||||
m_playback, &PlaybackController::playNext);
|
||||
connect(m_nowPlaying, &NowPlayingPanel::seekRequested,
|
||||
m_playback, &PlaybackController::seekFraction);
|
||||
connect(m_nowPlaying, &NowPlayingPanel::volumeChanged,
|
||||
m_playback, &PlaybackController::setVolume);
|
||||
|
||||
// Seek
|
||||
connect(m_seekBar, &QSlider::sliderPressed, this, [this]{ m_seekDragging = true; });
|
||||
connect(m_seekBar, &QSlider::sliderReleased, this, [this]{
|
||||
m_seekDragging = false;
|
||||
m_engine->seekFraction(m_seekBar->value() / 1000.0);
|
||||
});
|
||||
// shuffle / repeat checkboxes -> PlaybackController
|
||||
connect(m_nowPlaying->shuffleCheckBox(), &QCheckBox::toggled,
|
||||
m_playback, &PlaybackController::setShuffle);
|
||||
connect(m_nowPlaying->repeatCheckBox(), &QCheckBox::toggled,
|
||||
m_playback, &PlaybackController::setRepeat);
|
||||
|
||||
// Double‑click to play
|
||||
connect(m_playlistView, &QListView::doubleClicked,
|
||||
this, [this](const QModelIndex &idx){ playTrack(idx.row()); });
|
||||
|
||||
// Engine callbacks
|
||||
connect(m_engine, &AudioEngine::trackEnded,
|
||||
this, &MainWindow::playNext, Qt::QueuedConnection);
|
||||
|
||||
connect(m_engine, &AudioEngine::positionChanged,
|
||||
this, [this](qint64 posFrames){
|
||||
if (!m_seekDragging) {
|
||||
qint64 total = m_engine->totalFrames();
|
||||
if (total > 0)
|
||||
m_seekBar->setValue(int(posFrames * 1000 / total));
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(m_engine, &AudioEngine::fftReady,
|
||||
// PlaybackController -> NowPlayingPanel Spectrogram
|
||||
connect(m_playback, &PlaybackController::playbackStarted,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(true); });
|
||||
connect(m_playback, &PlaybackController::playbackPaused,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(false); });
|
||||
connect(m_playback, &PlaybackController::playbackStopped,
|
||||
m_nowPlaying, [this]{ m_nowPlaying->setPlaybackActive(false); });
|
||||
connect(m_playback, &PlaybackController::fftReady,
|
||||
m_spectrogram, &SpectrogramWidget::receiveFft, Qt::QueuedConnection);
|
||||
|
||||
connect(m_engine, &AudioEngine::durationKnown,
|
||||
this, [this](qint64 secs){
|
||||
if (m_currentIndex >= 0) {
|
||||
engine::Track *t = m_model->trackAt(m_currentIndex);
|
||||
if (t && t->durationSecs == 0) {
|
||||
t->durationSecs = secs;
|
||||
m_model->refreshRow(m_currentIndex);
|
||||
}
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
// PlaybackController -> LibraryController (resolve track index)
|
||||
connect(m_playback, &PlaybackController::playRequested,
|
||||
m_library, &LibraryController::onPlayRequested);
|
||||
|
||||
// Library scanning
|
||||
connect(m_library, &engine::MusicLibrary::scanStarted, this, [this]{
|
||||
m_model->setTracks({});
|
||||
m_lblStatus->setText("Scanning...");
|
||||
});
|
||||
connect(m_library, &engine::MusicLibrary::trackFound, this, [this](const engine::Track &t){
|
||||
m_model->appendTrack(t);
|
||||
m_lblStatus->setText(QString("%1 tracks found").arg(m_model->rowCount({})));
|
||||
}, Qt::QueuedConnection);
|
||||
connect(m_library, &engine::MusicLibrary::scanComplete, this, [this](int n){
|
||||
m_model->setTracks(m_library->tracks()); // sorted version
|
||||
m_lblStatus->setText(QString("%1 tracks.").arg(n));
|
||||
}, Qt::QueuedConnection);
|
||||
// LibraryController -> NowPlayingPanel (new track metadata)
|
||||
connect(m_library, &LibraryController::trackChanged,
|
||||
m_nowPlaying, [this](const engine::Track *t, int) {
|
||||
m_nowPlaying->updateTrack(t);
|
||||
m_spectrogram->reset();
|
||||
m_playlistView->scrollTo(
|
||||
m_library->model()->index(m_playback->currentIndex()));
|
||||
});
|
||||
|
||||
// LibraryController -> ui status / track count
|
||||
connect(m_library, &LibraryController::statusMessage,
|
||||
m_lblStatus, &QLabel::setText);
|
||||
connect(m_library, &LibraryController::trackCountChanged,
|
||||
m_playback, &PlaybackController::setTrackCount);
|
||||
|
||||
connect(m_playlistView, &QListView::doubleClicked,
|
||||
this, [this](const QModelIndex &idx) {
|
||||
m_playback->playTrack(idx.row());
|
||||
});
|
||||
}
|
||||
|
||||
// ── Playback Control ────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::reloadLibrary()
|
||||
void MainWindow::keyPressEvent(QKeyEvent *e)
|
||||
{
|
||||
m_library->scan();
|
||||
}
|
||||
|
||||
void MainWindow::playTrack(int index)
|
||||
{
|
||||
engine::Track *t = m_model->trackAt(index);
|
||||
if (!t) return;
|
||||
m_currentIndex = index;
|
||||
m_model->setCurrentIndex(index);
|
||||
m_engine->play(*t);
|
||||
m_coverWidget->setTrack(t);
|
||||
m_lblTitle->setText(t->displayTitle());
|
||||
m_lblArtist->setText(t->artist);
|
||||
m_lblAlbum->setText(t->album + (t->year > 0 ? " (" + QString::number(t->year) + ")" : ""));
|
||||
m_seekBar->setValue(0);
|
||||
m_btnPlay->setText("Pause");
|
||||
m_spectrogram->reset();
|
||||
m_playlistView->scrollTo(m_model->index(index));
|
||||
}
|
||||
|
||||
void MainWindow::togglePlayPause()
|
||||
{
|
||||
if (m_engine->state() == AudioEngine::State::Stopped) {
|
||||
if (m_currentIndex < 0 && m_model->rowCount({}) > 0)
|
||||
playTrack(0);
|
||||
else if (m_currentIndex >= 0)
|
||||
playTrack(m_currentIndex);
|
||||
} else {
|
||||
m_engine->togglePause();
|
||||
bool paused = (m_engine->state() == AudioEngine::State::Paused);
|
||||
m_btnPlay->setText(paused ? "Play" : "Pause");
|
||||
switch (e->key()) {
|
||||
case Qt::Key_MediaPlay:
|
||||
case Qt::Key_MediaPause:
|
||||
case Qt::Key_MediaTogglePlayPause:
|
||||
case Qt::Key_Space:
|
||||
m_playback->togglePlayPause(); e->accept(); return;
|
||||
case Qt::Key_MediaNext:
|
||||
m_playback->playNext(); e->accept(); return;
|
||||
case Qt::Key_MediaPrevious:
|
||||
m_playback->playPrev(); e->accept(); return;
|
||||
case Qt::Key_MediaStop:
|
||||
m_playback->stop(); e->accept(); return;
|
||||
default:
|
||||
QMainWindow::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::playNext()
|
||||
{
|
||||
int n = m_model->rowCount({});
|
||||
if (n == 0) return;
|
||||
if (m_chkRepeat->isChecked() && m_currentIndex >= 0) {
|
||||
playTrack(m_currentIndex);
|
||||
return;
|
||||
}
|
||||
int next = m_chkShuffle->isChecked()
|
||||
? QRandomGenerator::global()->bounded(n)
|
||||
: ((m_currentIndex + 1) % n);
|
||||
playTrack(next);
|
||||
}
|
||||
|
||||
void MainWindow::playPrev()
|
||||
{
|
||||
if (m_currentIndex <= 0) return;
|
||||
playTrack(m_currentIndex - 1);
|
||||
}
|
||||
|
||||
// ── UI Timer ────────────────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::onUiTimer()
|
||||
{
|
||||
auto fmt = [](qint64 s) -> QString {
|
||||
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
|
||||
};
|
||||
|
||||
float sr = float(m_engine->sampleRate());
|
||||
qint64 posF = m_engine->positionFrames();
|
||||
qint64 totF = m_engine->totalFrames();
|
||||
qint64 posSec = qint64(posF / sr);
|
||||
|
||||
engine::Track *currentTrack = (m_currentIndex >= 0) ? m_model->trackAt(m_currentIndex) : nullptr;
|
||||
qint64 totSec = totF > 0 ? qint64(totF / sr)
|
||||
: (currentTrack ? currentTrack->durationSecs : 0);
|
||||
|
||||
// Write back duration if we now know it
|
||||
if (totF > 0 && m_currentIndex >= 0) {
|
||||
engine::Track *t = m_model->trackAt(m_currentIndex);
|
||||
if (t && t->durationSecs == 0) {
|
||||
t->durationSecs = totSec;
|
||||
m_model->refreshRow(m_currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
m_lblTime->setText(fmt(posSec) + " / " + fmt(totSec));
|
||||
|
||||
if (m_engine->state() == AudioEngine::State::Stopped)
|
||||
m_btnPlay->setText("Play");
|
||||
}
|
||||
|
||||
// ── Playlist Management ─────────────────────────────────────────────────────
|
||||
|
||||
void MainWindow::onCreatePlaylist()
|
||||
{
|
||||
PlaylistCreatorDialog dialog(this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
// Playlist was created and saved
|
||||
// Optionally auto-load it
|
||||
auto playlist = dialog.getPlaylist();
|
||||
loadPlaylistIntoModel(playlist.name);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onLoadPlaylist()
|
||||
{
|
||||
PlaylistBrowserDialog dialog(this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QString playlistName = dialog.getSelectedPlaylist();
|
||||
if (!playlistName.isEmpty()) {
|
||||
loadPlaylistIntoModel(playlistName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadPlaylistIntoModel(const QString &playlistName)
|
||||
{
|
||||
auto playlist = PlaylistManager::loadPlaylist(playlistName);
|
||||
|
||||
if (playlist.tracks.isEmpty()) {
|
||||
QMessageBox::warning(this, "Empty Playlist",
|
||||
QString("Playlist \"%1\" is empty.").arg(playlistName));
|
||||
return;
|
||||
}
|
||||
|
||||
m_model->setTracks(playlist.tracks);
|
||||
m_lblStatus->setText(
|
||||
QString("Playlist: %1 (%2 tracks)")
|
||||
.arg(playlistName)
|
||||
.arg(playlist.tracks.size())
|
||||
);
|
||||
|
||||
// Reset playback state
|
||||
m_currentIndex = -1;
|
||||
m_engine->stop();
|
||||
m_btnPlay->setText("Play");
|
||||
}
|
||||
+14
-58
@@ -1,86 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QSlider>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QListView>
|
||||
#include <QTimer>
|
||||
#include <QTabWidget>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
// Forward declarations
|
||||
class AudioEngine;
|
||||
namespace engine {
|
||||
class MusicLibrary;
|
||||
}
|
||||
class PlaylistModel;
|
||||
class CoverArtWidget;
|
||||
class PlaybackController;
|
||||
class LibraryController;
|
||||
class NowPlayingPanel;
|
||||
class SpectrogramWidget;
|
||||
class EqualizerWidget;
|
||||
class PlaylistCreatorDialog;
|
||||
class PlaylistBrowserDialog;
|
||||
class QListView;
|
||||
class QLabel;
|
||||
class QTabWidget;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
void buildUI();
|
||||
QGroupBox *buildNowPlayingPanel();
|
||||
QHBoxLayout *buildTransportRow();
|
||||
QTabWidget *buildBottomTabs();
|
||||
void buildMenuBar();
|
||||
void wireSignals();
|
||||
|
||||
void reloadLibrary();
|
||||
void playTrack(int index);
|
||||
void togglePlayPause();
|
||||
void playNext();
|
||||
void playPrev();
|
||||
void onUiTimer();
|
||||
void onCreatePlaylist();
|
||||
void onLoadPlaylist();
|
||||
void loadPlaylistIntoModel(const QString &playlistName);
|
||||
// Controllers
|
||||
PlaybackController *m_playback;
|
||||
LibraryController *m_library;
|
||||
|
||||
// Core
|
||||
AudioEngine *m_engine;
|
||||
engine::MusicLibrary *m_library;
|
||||
PlaylistModel *m_model;
|
||||
|
||||
// Transport – plain ASCII labels now
|
||||
QLabel *m_lblTitle = new QLabel("No track selected");
|
||||
QLabel *m_lblArtist = new QLabel(" ");
|
||||
QLabel *m_lblAlbum = new QLabel(" ");
|
||||
QLabel *m_lblTime = new QLabel("0:00 / 0:00");
|
||||
QSlider *m_seekBar = new QSlider(Qt::Horizontal);
|
||||
QPushButton *m_btnPrev = new QPushButton("|<");
|
||||
QPushButton *m_btnPlay = new QPushButton("> Play");
|
||||
QPushButton *m_btnNext = new QPushButton(">|");
|
||||
QCheckBox *m_chkShuffle = new QCheckBox("Shuffle");
|
||||
QCheckBox *m_chkRepeat = new QCheckBox("Repeat");
|
||||
QSlider *m_volSlider = new QSlider(Qt::Horizontal);
|
||||
|
||||
// Playlist
|
||||
QListView *m_playlistView;
|
||||
QLabel *m_lblStatus = new QLabel("No library loaded.");
|
||||
|
||||
// Panels
|
||||
CoverArtWidget *m_coverWidget;
|
||||
// Widgets
|
||||
NowPlayingPanel *m_nowPlaying;
|
||||
SpectrogramWidget *m_spectrogram;
|
||||
EqualizerWidget *m_eqWidget;
|
||||
QListView *m_playlistView;
|
||||
QLabel *m_lblStatus;
|
||||
|
||||
// State
|
||||
int m_currentIndex { -1 };
|
||||
bool m_seekDragging { false };
|
||||
QTimer m_uiTimer;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "NowPlayingPanel.h"
|
||||
#include "CoverArtWidget.h"
|
||||
#include "engine/MusicLibrary.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFont>
|
||||
|
||||
NowPlayingPanel::NowPlayingPanel(QWidget *parent)
|
||||
: QGroupBox("Now Playing", parent)
|
||||
, m_cover(new CoverArtWidget)
|
||||
{
|
||||
buildLayout();
|
||||
|
||||
connect(m_btnPlay, &QPushButton::clicked, this, &NowPlayingPanel::playPauseClicked);
|
||||
connect(m_btnPrev, &QPushButton::clicked, this, &NowPlayingPanel::prevClicked);
|
||||
connect(m_btnNext, &QPushButton::clicked, this, &NowPlayingPanel::nextClicked);
|
||||
|
||||
connect(m_seekBar, &QSlider::sliderPressed, this, [this]{ m_seekDragging = true; });
|
||||
connect(m_seekBar, &QSlider::sliderReleased, this, [this]{
|
||||
m_seekDragging = false;
|
||||
emit seekRequested(m_seekBar->value() / 1000.0);
|
||||
});
|
||||
|
||||
connect(m_volSlider, &QSlider::valueChanged, this, &NowPlayingPanel::volumeChanged);
|
||||
}
|
||||
|
||||
void NowPlayingPanel::buildLayout()
|
||||
{
|
||||
m_cover->setFixedSize(130, 130);
|
||||
|
||||
QFont boldF = m_lblTitle->font();
|
||||
boldF.setBold(true);
|
||||
boldF.setPointSizeF(11);
|
||||
m_lblTitle->setFont(boldF);
|
||||
|
||||
m_seekBar->setRange(0, 1000);
|
||||
m_seekBar->setValue(0);
|
||||
|
||||
m_volSlider->setRange(0, 100);
|
||||
m_volSlider->setValue(80);
|
||||
m_volSlider->setFixedWidth(90);
|
||||
|
||||
auto *seekRow = new QHBoxLayout;
|
||||
seekRow->addWidget(m_seekBar);
|
||||
seekRow->addWidget(m_lblTime);
|
||||
|
||||
auto *btns = new QHBoxLayout;
|
||||
btns->setSpacing(3);
|
||||
btns->addWidget(m_btnPrev);
|
||||
btns->addWidget(m_btnPlay);
|
||||
btns->addWidget(m_btnNext);
|
||||
btns->addSpacing(8);
|
||||
btns->addWidget(m_chkShuffle);
|
||||
btns->addWidget(m_chkRepeat);
|
||||
|
||||
auto *transport = new QHBoxLayout;
|
||||
transport->addLayout(btns);
|
||||
transport->addStretch();
|
||||
transport->addWidget(new QLabel("Volume:"));
|
||||
transport->addWidget(m_volSlider);
|
||||
|
||||
auto *right = new QVBoxLayout;
|
||||
right->setSpacing(4);
|
||||
right->addWidget(m_lblTitle);
|
||||
right->addWidget(m_lblArtist);
|
||||
right->addWidget(m_lblAlbum);
|
||||
right->addLayout(seekRow);
|
||||
right->addLayout(transport);
|
||||
|
||||
auto *hlay = new QHBoxLayout(this);
|
||||
hlay->addWidget(m_cover);
|
||||
hlay->addLayout(right, 1);
|
||||
}
|
||||
|
||||
void NowPlayingPanel::updateTrack(const engine::Track *t)
|
||||
{
|
||||
if (!t) return;
|
||||
m_cover->setTrack(t);
|
||||
m_lblTitle->setText(t->displayTitle());
|
||||
m_lblArtist->setText(t->artist);
|
||||
m_lblAlbum->setText(t->album +
|
||||
(t->year > 0 ? " (" + QString::number(t->year) + ")" : ""));
|
||||
m_seekBar->setValue(0);
|
||||
m_btnPlay->setText("Pause");
|
||||
}
|
||||
|
||||
void NowPlayingPanel::updatePosition(qint64 posFrames, qint64 totalFrames, float sampleRate)
|
||||
{
|
||||
auto fmt = [](qint64 s) {
|
||||
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
|
||||
};
|
||||
|
||||
qint64 posSec = (sampleRate > 0) ? qint64(posFrames / sampleRate) : 0;
|
||||
qint64 totSec = (sampleRate > 0 && totalFrames > 0)
|
||||
? qint64(totalFrames / sampleRate) : 0;
|
||||
|
||||
m_lblTime->setText(fmt(posSec) + " / " + fmt(totSec));
|
||||
|
||||
if (!m_seekDragging && totalFrames > 0)
|
||||
m_seekBar->setValue(int(posFrames * 1000 / totalFrames));
|
||||
}
|
||||
|
||||
void NowPlayingPanel::setPlaybackActive(bool playing)
|
||||
{
|
||||
m_btnPlay->setText(playing ? "Pause" : "Play");
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QSlider>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
|
||||
class CoverArtWidget;
|
||||
namespace engine { struct Track; }
|
||||
|
||||
class NowPlayingPanel : public QGroupBox
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit NowPlayingPanel(QWidget *parent = nullptr);
|
||||
|
||||
QCheckBox *shuffleCheckBox() const { return m_chkShuffle; }
|
||||
QCheckBox *repeatCheckBox() const { return m_chkRepeat; }
|
||||
|
||||
public slots:
|
||||
void updateTrack(const engine::Track *track);
|
||||
void updatePosition(qint64 posFrames, qint64 totalFrames, float sampleRate);
|
||||
void setPlaybackActive(bool playing);
|
||||
|
||||
signals:
|
||||
void playPauseClicked();
|
||||
void prevClicked();
|
||||
void nextClicked();
|
||||
void seekRequested(double fraction); // 0.0–1.0
|
||||
void volumeChanged(int percent); // 0–100
|
||||
|
||||
private:
|
||||
void buildLayout();
|
||||
|
||||
CoverArtWidget *m_cover;
|
||||
QLabel *m_lblTitle = new QLabel("No track selected");
|
||||
QLabel *m_lblArtist = new QLabel(" ");
|
||||
QLabel *m_lblAlbum = new QLabel(" ");
|
||||
QLabel *m_lblTime = new QLabel("0:00 / 0:00");
|
||||
QSlider *m_seekBar = new QSlider(Qt::Horizontal);
|
||||
QPushButton *m_btnPrev = new QPushButton("|<");
|
||||
QPushButton *m_btnPlay = new QPushButton("Play");
|
||||
QPushButton *m_btnNext = new QPushButton(">|");
|
||||
QCheckBox *m_chkShuffle = new QCheckBox("Shuffle");
|
||||
QCheckBox *m_chkRepeat = new QCheckBox("Repeat");
|
||||
QSlider *m_volSlider = new QSlider(Qt::Horizontal);
|
||||
|
||||
bool m_seekDragging { false };
|
||||
};
|
||||
@@ -102,7 +102,9 @@ void PlaylistBrowserDialog::refreshPlaylistList()
|
||||
}
|
||||
|
||||
if (playlists.isEmpty()) {
|
||||
m_playlistList->addItem("(No playlists created)");
|
||||
auto *item = new QListWidgetItem("No playlists found");
|
||||
item->setFlags(Qt::NoItemFlags);
|
||||
m_playlistList->addItem(item);
|
||||
m_deleteBtn->setEnabled(false);
|
||||
m_loadBtn->setEnabled(false);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user