// src/MainWindow.cpp #include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) #ifndef IS_MOBILE #define IS_MOBILE #endif #endif MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle("Yr Crystals"); resize(1280, 800); m_stack = new QStackedWidget(this); setCentralWidget(m_stack); m_welcome = new WelcomeWidget(this); connect(m_welcome, &WelcomeWidget::openFileClicked, this, &MainWindow::onOpenFile); connect(m_welcome, &WelcomeWidget::openFolderClicked, this, &MainWindow::onOpenFolder); m_stack->addWidget(m_welcome); initUi(); m_engine = new AudioEngine(); QThread* audioThread = new QThread(this); m_engine->moveToThread(audioThread); connect(audioThread, &QThread::finished, m_engine, &QObject::deleteLater); connect(m_engine, &AudioEngine::playbackFinished, this, &MainWindow::onTrackFinished); connect(m_engine, &AudioEngine::trackLoaded, this, &MainWindow::onTrackLoaded); connect(m_engine, &AudioEngine::positionChanged, m_playerPage->playback(), &PlaybackWidget::updateSeek); connect(m_engine, &AudioEngine::spectrumReady, m_playerPage->visualizer(), &VisualizerWidget::updateData); // Buffering Logic connect(m_engine, &AudioEngine::bufferingStart, this, [this](){ if (!m_waitDialog) { m_waitDialog = new QProgressDialog("Buffering...", QString(), 0, 0, this); m_waitDialog->setWindowModality(Qt::ApplicationModal); m_waitDialog->setCancelButton(nullptr); m_waitDialog->setMinimumDuration(0); } m_waitDialog->show(); }); connect(m_engine, &AudioEngine::bufferingEnd, this, [this](){ if (m_waitDialog) m_waitDialog->hide(); }); audioThread->start(); } MainWindow::~MainWindow() { if (m_engine) { QMetaObject::invokeMethod(m_engine, "stop", Qt::BlockingQueuedConnection); m_engine->thread()->quit(); m_engine->thread()->wait(); } } void MainWindow::initUi() { m_playerPage = new PlayerPage(this); m_playlist = new QListWidget(); m_playlist->setStyleSheet("QListWidget { background-color: #111; border: none; } QListWidget::item { border-bottom: 1px solid #222; padding: 5px; } QListWidget::item:selected { background-color: #333; }"); m_playlist->setSelectionMode(QAbstractItemView::SingleSelection); QScroller::grabGesture(m_playlist, QScroller::LeftMouseButtonGesture); connect(m_playlist, &QListWidget::itemDoubleClicked, this, &MainWindow::onTrackDoubleClicked); PlaybackWidget* pb = m_playerPage->playback(); SettingsWidget* set = m_playerPage->settings(); VisualizerWidget* viz = m_playerPage->visualizer(); connect(pb, &PlaybackWidget::playClicked, this, &MainWindow::play); connect(pb, &PlaybackWidget::pauseClicked, this, &MainWindow::pause); connect(pb, &PlaybackWidget::nextClicked, this, &MainWindow::nextTrack); connect(pb, &PlaybackWidget::prevClicked, this, &MainWindow::prevTrack); connect(pb, &PlaybackWidget::seekChanged, this, &MainWindow::seek); connect(set, &SettingsWidget::paramsChanged, viz, &VisualizerWidget::setParams); connect(set, &SettingsWidget::dspParamsChanged, this, &MainWindow::onDspChanged); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::onBinsChanged); connect(set, &SettingsWidget::paramsChanged, this, &MainWindow::saveSettings); connect(set, &SettingsWidget::binsChanged, this, &MainWindow::saveSettings); connect(m_playerPage, &PlayerPage::toggleFullScreen, this, &MainWindow::onToggleFullScreen); // Pause on Settings Open, Resume (Safe) on Close connect(m_playerPage, &PlayerPage::settingsOpened, this, &MainWindow::pause); connect(m_playerPage, &PlayerPage::settingsClosed, this, [this](){ QMetaObject::invokeMethod(m_engine, "playSafe", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(true); }); #ifdef IS_MOBILE m_mobileTabs = new QTabWidget(); m_mobileTabs->setStyleSheet("QTabWidget::pane { border: 0; } QTabBar::tab { background: #222; color: white; padding: 15px; min-width: 100px; } QTabBar::tab:selected { background: #444; }"); m_mobileTabs->addTab(m_playerPage, "Visualizer"); m_mobileTabs->addTab(m_playlist, "Playlist"); m_stack->addWidget(m_mobileTabs); #else m_dock = new QDockWidget("Playlist", this); m_dock->setWidget(m_playlist); addDockWidget(Qt::LeftDockWidgetArea, m_dock); m_stack->addWidget(m_playerPage); #endif } void MainWindow::onToggleFullScreen() { static bool isFs = false; isFs = !isFs; m_playerPage->setFullScreen(isFs); #ifdef IS_MOBILE if (m_mobileTabs) { QTabBar* bar = m_mobileTabs->findChild(); if (bar) bar->setVisible(!isFs); } #else if (m_dock) m_dock->setVisible(!isFs); #endif } void MainWindow::onOpenFile() { m_pendingAction = PendingAction::File; QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true)); } void MainWindow::onOpenFolder() { m_pendingAction = PendingAction::Folder; QMetaObject::invokeMethod(this, "onPermissionsResult", Qt::QueuedConnection, Q_ARG(bool, true)); } void MainWindow::onPermissionsResult(bool granted) { if (!granted) return; #ifdef Q_OS_IOS auto callback = [this, recursive = (m_pendingAction == PendingAction::Folder)](QString path) { if (!path.isEmpty()) loadPath(path, recursive); }; Utils::openIosPicker(m_pendingAction == PendingAction::Folder, callback); #else QString initialPath; QString filter = "Audio (*.mp3 *.m4a *.wav *.flac *.ogg)"; #ifdef Q_OS_ANDROID initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); if (initialPath.isEmpty()) initialPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); #else initialPath = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); #endif QString path; if (m_pendingAction == PendingAction::File) { path = QFileDialog::getOpenFileName(nullptr, "Open Audio", initialPath, filter); if (!path.isEmpty()) loadPath(path, false); } else if (m_pendingAction == PendingAction::Folder) { path = QFileDialog::getExistingDirectory(nullptr, "Open Folder", initialPath); if (!path.isEmpty()) loadPath(path, true); } #endif m_pendingAction = PendingAction::None; } void MainWindow::loadPath(const QString& rawPath, bool recursive) { QString path = rawPath; QUrl url(rawPath); if (url.isValid() && url.isLocalFile()) { path = url.toLocalFile(); } else if (rawPath.startsWith("file://")) { path = QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7); } m_tracks.clear(); m_playlist->clear(); QFileInfo info(path); bool isDir = info.isDir(); bool isFile = info.isFile(); if (!isDir && !isFile && QFile::exists(path)) { if (path.endsWith(".mp3") || path.endsWith(".m4a") || path.endsWith(".wav") || path.endsWith(".flac") || path.endsWith(".ogg")) { isFile = true; } else { isDir = true; } } if (isDir || path.startsWith("content://")) { m_settingsDir = path; QStringList files = Utils::scanDirectory(path, recursive); for (const auto& f : files) m_tracks.append({f, Utils::getMetadata(f)}); std::sort(m_tracks.begin(), m_tracks.end(), [](const TrackInfo& a, const TrackInfo& b) { if (a.meta.album != b.meta.album) return a.meta.album < b.meta.album; if (a.meta.trackNumber != b.meta.trackNumber) return a.meta.trackNumber < b.meta.trackNumber; return a.meta.title < b.meta.title; }); for (const auto& t : m_tracks) { QListWidgetItem* item = new QListWidgetItem(m_playlist); item->setSizeHint(QSize(0, 70)); m_playlist->addItem(item); m_playlist->setItemWidget(item, new PlaylistItemWidget(t.meta)); } if (!m_tracks.isEmpty()) loadIndex(0); } else if (isFile) { m_settingsDir = info.path(); TrackInfo t = {path, Utils::getMetadata(path)}; m_tracks.append(t); QListWidgetItem* item = new QListWidgetItem(m_playlist); item->setSizeHint(QSize(0, 70)); m_playlist->addItem(item); m_playlist->setItemWidget(item, new PlaylistItemWidget(t.meta)); loadIndex(0); } loadSettings(); #ifdef IS_MOBILE m_stack->setCurrentWidget(m_mobileTabs); #else m_stack->setCurrentWidget(m_playerPage); #endif } void MainWindow::loadSettings() { if (m_settingsDir.isEmpty()) return; if (m_settingsDir.startsWith("content://")) return; QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); if (f.open(QIODevice::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); QJsonObject root = doc.object(); bool glass = root["glass"].toBool(true); bool focus = root["focus"].toBool(false); bool trails = root["trails"].toBool(false); bool albumColors = root["albumColors"].toBool(false); bool shadow = root["shadow"].toBool(false); bool mirrored = root["mirrored"].toBool(false); int bins = root["bins"].toInt(26); float brightness = root["brightness"].toDouble(1.0); float entropy = root["entropy"].toDouble(1.0); m_playerPage->settings()->setParams(glass, focus, trails, albumColors, shadow, mirrored, bins, brightness, entropy); } } void MainWindow::saveSettings() { if (m_settingsDir.isEmpty()) return; if (m_settingsDir.startsWith("content://")) return; SettingsWidget* s = m_playerPage->settings(); QJsonObject root; root["glass"] = s->isGlass(); root["focus"] = s->isFocus(); root["trails"] = s->isTrails(); root["albumColors"] = s->isAlbumColors(); root["shadow"] = s->isShadow(); root["mirrored"] = s->isMirrored(); root["bins"] = s->getBins(); root["brightness"] = s->getBrightness(); root["entropy"] = s->getEntropy(); QFile f(QDir(m_settingsDir).filePath(".yrcrystals.json")); if (f.open(QIODevice::WriteOnly)) { f.write(QJsonDocument(root).toJson()); } } void MainWindow::loadIndex(int index) { if (index < 0 || index >= m_tracks.size()) return; m_currentIndex = index; const auto& t = m_tracks[index]; m_playlist->setCurrentRow(index); int bins = m_playerPage->settings()->findChild()->value(); auto colors = Utils::extractAlbumColors(t.meta.art, bins); std::vector stdColors; for(const auto& c : colors) stdColors.push_back(c); m_playerPage->visualizer()->setAlbumPalette(stdColors); QMetaObject::invokeMethod(m_engine, "loadTrack", Qt::QueuedConnection, Q_ARG(QString, t.path)); int nextIndex = (m_currentIndex + 1) % m_tracks.size(); if (nextIndex != m_currentIndex) { QMetaObject::invokeMethod(m_engine, "queueNextTrack", Qt::QueuedConnection, Q_ARG(QString, m_tracks[nextIndex].path)); } } void MainWindow::onTrackLoaded(bool success) { if (success) { play(); if (m_currentIndex >= 0) { const auto& t = m_tracks[m_currentIndex]; QString title = t.meta.title; if (!t.meta.artist.isEmpty()) title += " - " + t.meta.artist; setWindowTitle(title); } } else { nextTrack(); } } void MainWindow::onTrackDoubleClicked(QListWidgetItem* item) { loadIndex(m_playlist->row(item)); } void MainWindow::play() { QMetaObject::invokeMethod(m_engine, "play", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(true); } void MainWindow::pause() { QMetaObject::invokeMethod(m_engine, "pause", Qt::QueuedConnection); m_playerPage->playback()->setPlaying(false); } void MainWindow::nextTrack() { int next = m_currentIndex + 1; if (next >= static_cast(m_tracks.size())) next = 0; loadIndex(next); } void MainWindow::prevTrack() { int prev = m_currentIndex - 1; if (prev < 0) prev = static_cast(m_tracks.size()) - 1; loadIndex(prev); } void MainWindow::seek(float pos) { QMetaObject::invokeMethod(m_engine, "seek", Qt::QueuedConnection, Q_ARG(float, pos)); } void MainWindow::onTrackFinished() { nextTrack(); } void MainWindow::updateLoop() { } void MainWindow::onDspChanged(int fft, int hop) { QMetaObject::invokeMethod(m_engine, "setDspParams", Qt::QueuedConnection, Q_ARG(int, fft), Q_ARG(int, hop)); } void MainWindow::closeEvent(QCloseEvent* event) { event->accept(); } void MainWindow::onBinsChanged(int n) { QMetaObject::invokeMethod(m_engine, "setNumBins", Qt::QueuedConnection, Q_ARG(int, n)); m_playerPage->visualizer()->setNumBins(n); if (m_currentIndex >= 0 && m_currentIndex < m_tracks.size()) { const auto& t = m_tracks[m_currentIndex]; auto colors = Utils::extractAlbumColors(t.meta.art, n); std::vector stdColors; for(const auto& c : colors) stdColors.push_back(c); m_playerPage->visualizer()->setAlbumPalette(stdColors); } }