refactor: MainWindow class into multiple items / controllers ; fix: Bug with PlaylistBrowserDialog.cpp : List item "No playlists available" was selectable

This commit is contained in:
2026-06-19 13:05:45 +02:00
parent 97166e9f4e
commit e62ab4626c
10 changed files with 573 additions and 391 deletions
+9 -23
View File
@@ -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)
+98
View File
@@ -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));
}
+44
View File
@@ -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 };
};
+78
View File
@@ -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();
}
+54
View File
@@ -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
View File
@@ -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);
// Doubleclick 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
View File
@@ -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;
};
+107
View File
@@ -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");
}
+49
View File
@@ -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.01.0
void volumeChanged(int percent); // 0100
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 };
};
+3 -1
View File
@@ -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 {