From e62ab4626ca706c239f5e6f27c9175ac3bd55c46 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Fri, 19 Jun 2026 13:05:45 +0200 Subject: [PATCH] refactor: MainWindow class into multiple items / controllers ; fix: Bug with PlaylistBrowserDialog.cpp : List item "No playlists available" was selectable --- CMakeLists.txt | 32 +- src/controllers/LibraryController.cpp | 98 ++++++ src/controllers/LibraryController.h | 44 +++ src/controllers/PlaybackController.cpp | 78 +++++ src/controllers/PlaybackController.h | 54 +++ src/gui/MainWindow.cpp | 426 +++++++----------------- src/gui/MainWindow.h | 72 +--- src/gui/NowPlayingPanel.cpp | 107 ++++++ src/gui/NowPlayingPanel.h | 49 +++ src/playlists/PlaylistBrowserDialog.cpp | 4 +- 10 files changed, 573 insertions(+), 391 deletions(-) create mode 100644 src/controllers/LibraryController.cpp create mode 100644 src/controllers/LibraryController.h create mode 100644 src/controllers/PlaybackController.cpp create mode 100644 src/controllers/PlaybackController.h create mode 100644 src/gui/NowPlayingPanel.cpp create mode 100644 src/gui/NowPlayingPanel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fd784a..b745b2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/controllers/LibraryController.cpp b/src/controllers/LibraryController.cpp new file mode 100644 index 0000000..5dc4c88 --- /dev/null +++ b/src/controllers/LibraryController.cpp @@ -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 +#include + +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(parent())); + if (dlg.exec() == QDialog::Accepted) + loadPlaylistByName(dlg.getPlaylist().name); +} + +void LibraryController::loadPlaylist() +{ + PlaylistBrowserDialog dlg( + qobject_cast(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(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)); +} \ No newline at end of file diff --git a/src/controllers/LibraryController.h b/src/controllers/LibraryController.h new file mode 100644 index 0000000..b79f60c --- /dev/null +++ b/src/controllers/LibraryController.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include +#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 }; +}; \ No newline at end of file diff --git a/src/controllers/PlaybackController.cpp b/src/controllers/PlaybackController.cpp new file mode 100644 index 0000000..740ade0 --- /dev/null +++ b/src/controllers/PlaybackController.cpp @@ -0,0 +1,78 @@ +#include "PlaybackController.h" +#include "AudioEngine.h" +#include + +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(); +} \ No newline at end of file diff --git a/src/controllers/PlaybackController.h b/src/controllers/PlaybackController.h new file mode 100644 index 0000000..0b1cc21 --- /dev/null +++ b/src/controllers/PlaybackController.h @@ -0,0 +1,54 @@ +#pragma once +#include + +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 &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 }; +}; \ No newline at end of file diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d8fb1e7..8d7f493 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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 -#include +#include +#include +#include #include #include -#include #include -#include -#include +#include +#include +#include #include -#include #include #include 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"); } \ No newline at end of file diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index cc9f3b6..cd3d271 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -1,86 +1,42 @@ #pragma once - #include -#include -#include -#include -#include -#include #include -#include -#include -#include -// 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; }; \ No newline at end of file diff --git a/src/gui/NowPlayingPanel.cpp b/src/gui/NowPlayingPanel.cpp new file mode 100644 index 0000000..c9720d1 --- /dev/null +++ b/src/gui/NowPlayingPanel.cpp @@ -0,0 +1,107 @@ +#include "NowPlayingPanel.h" +#include "CoverArtWidget.h" +#include "engine/MusicLibrary.h" + +#include +#include +#include + +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"); +} \ No newline at end of file diff --git a/src/gui/NowPlayingPanel.h b/src/gui/NowPlayingPanel.h new file mode 100644 index 0000000..e8e4dde --- /dev/null +++ b/src/gui/NowPlayingPanel.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include + +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 }; +}; \ No newline at end of file diff --git a/src/playlists/PlaylistBrowserDialog.cpp b/src/playlists/PlaylistBrowserDialog.cpp index 621c124..04f88f2 100644 --- a/src/playlists/PlaylistBrowserDialog.cpp +++ b/src/playlists/PlaylistBrowserDialog.cpp @@ -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 {