373 lines
13 KiB
C++
373 lines
13 KiB
C++
// src/MainWindow.cpp
|
|
|
|
#include "MainWindow.h"
|
|
#include <QApplication>
|
|
#include <QHeaderView>
|
|
#include <QScrollBar>
|
|
#include <QFileInfo>
|
|
#include <QCloseEvent>
|
|
#include <QFileDialog>
|
|
#include <QScroller>
|
|
#include <QThread>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QDir>
|
|
#include <QStandardPaths>
|
|
#include <QUrl>
|
|
#include <QProgressDialog>
|
|
#include <algorithm>
|
|
|
|
#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<QTabBar*>();
|
|
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<QSlider*>()->value();
|
|
auto colors = Utils::extractAlbumColors(t.meta.art, bins);
|
|
std::vector<QColor> 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<int>(m_tracks.size())) next = 0;
|
|
loadIndex(next);
|
|
}
|
|
void MainWindow::prevTrack() {
|
|
int prev = m_currentIndex - 1;
|
|
if (prev < 0) prev = static_cast<int>(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<QColor> stdColors;
|
|
for(const auto& c : colors) stdColors.push_back(c);
|
|
m_playerPage->visualizer()->setAlbumPalette(stdColors);
|
|
}
|
|
} |