diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 385c4f470..367f1dd93 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -48,6 +48,9 @@ set(SRCS controllersettingsdialog.h controllersettingsdialog.ui controllersettingwidgetbinder.h + coverdownloaddialog.cpp + coverdownloaddialog.h + coverdownloaddialog.ui debuggermodels.cpp debuggermodels.h debuggerwindow.cpp diff --git a/src/duckstation-qt/coverdownloaddialog.cpp b/src/duckstation-qt/coverdownloaddialog.cpp new file mode 100644 index 000000000..88f3bcf96 --- /dev/null +++ b/src/duckstation-qt/coverdownloaddialog.cpp @@ -0,0 +1,116 @@ +#include "coverdownloaddialog.h" +#include "common/assert.h" +#include "frontend-common/game_list.h" + +CoverDownloadDialog::CoverDownloadDialog(QWidget* parent /*= nullptr*/) : QDialog(parent) +{ + m_ui.setupUi(this); + updateEnabled(); + + connect(m_ui.start, &QPushButton::clicked, this, &CoverDownloadDialog::onStartClicked); + connect(m_ui.close, &QPushButton::clicked, this, &CoverDownloadDialog::onCloseClicked); + connect(m_ui.urls, &QTextEdit::textChanged, this, &CoverDownloadDialog::updateEnabled); +} + +CoverDownloadDialog::~CoverDownloadDialog() +{ + Assert(!m_thread); +} + +void CoverDownloadDialog::closeEvent(QCloseEvent* ev) +{ + cancelThread(); +} + +void CoverDownloadDialog::onDownloadStatus(const QString& text) +{ + m_ui.status->setText(text); +} + +void CoverDownloadDialog::onDownloadProgress(int value, int range) +{ + // limit to once every five seconds + if (m_last_refresh_time.GetTimeSeconds() >= 5.0f) + { + emit coverRefreshRequested(); + m_last_refresh_time.Reset(); + } + + if (range != m_ui.progress->maximum()) + m_ui.progress->setMaximum(range); + m_ui.progress->setValue(value); +} + +void CoverDownloadDialog::onDownloadComplete() +{ + emit coverRefreshRequested(); + + if (m_thread) + { + m_thread->join(); + m_thread.reset(); + } + + updateEnabled(); + + m_ui.status->setText(tr("Download complete.")); +} + +void CoverDownloadDialog::onStartClicked() +{ + if (m_thread) + cancelThread(); + else + startThread(); +} + +void CoverDownloadDialog::onCloseClicked() +{ + if (m_thread) + cancelThread(); + + done(0); +} + +void CoverDownloadDialog::updateEnabled() +{ + const bool running = static_cast(m_thread); + m_ui.start->setText(running ? tr("Stop") : tr("Start")); + m_ui.start->setEnabled(running || !m_ui.urls->toPlainText().isEmpty()); + m_ui.close->setEnabled(!running); + m_ui.urls->setEnabled(!running); +} + +void CoverDownloadDialog::startThread() +{ + m_thread = std::make_unique(this, m_ui.urls->toPlainText()); + connect(m_thread.get(), &CoverDownloadThread::statusUpdated, this, &CoverDownloadDialog::onDownloadStatus); + connect(m_thread.get(), &CoverDownloadThread::progressUpdated, this, &CoverDownloadDialog::onDownloadProgress); + connect(m_thread.get(), &CoverDownloadThread::threadFinished, this, &CoverDownloadDialog::onDownloadComplete); + m_thread->start(); + updateEnabled(); +} + +void CoverDownloadDialog::cancelThread() +{ + if (!m_thread) + return; + + m_thread->requestInterruption(); + m_thread->join(); + m_thread.reset(); +} + +CoverDownloadDialog::CoverDownloadThread::CoverDownloadThread(QWidget* parent, const QString& urls) + : QtAsyncProgressThread(parent) +{ + for (const QString& str : urls.split(QChar('\n'))) + m_urls.push_back(str.toStdString()); +} + +CoverDownloadDialog::CoverDownloadThread::~CoverDownloadThread() = default; + +void CoverDownloadDialog::CoverDownloadThread::runAsync() +{ + GameList::DownloadCovers(m_urls, this); +} \ No newline at end of file diff --git a/src/duckstation-qt/coverdownloaddialog.h b/src/duckstation-qt/coverdownloaddialog.h new file mode 100644 index 000000000..573b21d1f --- /dev/null +++ b/src/duckstation-qt/coverdownloaddialog.h @@ -0,0 +1,53 @@ +#pragma once +#include "common/timer.h" +#include "common/types.h" +#include "qtprogresscallback.h" +#include "ui_coverdownloaddialog.h" +#include +#include +#include +#include + +class CoverDownloadDialog final : public QDialog +{ + Q_OBJECT + +public: + CoverDownloadDialog(QWidget* parent = nullptr); + ~CoverDownloadDialog(); + +Q_SIGNALS: + void coverRefreshRequested(); + +protected: + void closeEvent(QCloseEvent* ev); + +private Q_SLOTS: + void onDownloadStatus(const QString& text); + void onDownloadProgress(int value, int range); + void onDownloadComplete(); + void onStartClicked(); + void onCloseClicked(); + void updateEnabled(); + +private: + class CoverDownloadThread : public QtAsyncProgressThread + { + public: + CoverDownloadThread(QWidget* parent, const QString& urls); + ~CoverDownloadThread(); + + protected: + void runAsync() override; + + private: + std::vector m_urls; + }; + + void startThread(); + void cancelThread(); + + Ui::CoverDownloadDialog m_ui; + std::unique_ptr m_thread; + Common::Timer m_last_refresh_time; +}; diff --git a/src/duckstation-qt/coverdownloaddialog.ui b/src/duckstation-qt/coverdownloaddialog.ui new file mode 100644 index 000000000..a2faaa2c8 --- /dev/null +++ b/src/duckstation-qt/coverdownloaddialog.ui @@ -0,0 +1,81 @@ + + + CoverDownloadDialog + + + + 0 + 0 + 656 + 343 + + + + Download Covers + + + + + + <html><head/><body><p>DuckStation can automatically download covers for games which do not currently have a cover set. We do not host any cover images, the user must provide their own source for images.</p><p>In the form below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html> + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Waiting to start... + + + + + + + + + + + + false + + + Start + + + true + + + + + + + Close + + + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index 357d778cc..465ab924e 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -16,6 +16,7 @@ + @@ -55,6 +56,7 @@ + @@ -210,6 +212,9 @@ Document + + Document + @@ -231,6 +236,7 @@ + diff --git a/src/duckstation-qt/gamesummarywidget.cpp b/src/duckstation-qt/gamesummarywidget.cpp index a9d28222d..bb7a87e1b 100644 --- a/src/duckstation-qt/gamesummarywidget.cpp +++ b/src/duckstation-qt/gamesummarywidget.cpp @@ -220,7 +220,7 @@ void GameSummaryWidget::onComputeHashClicked() QtConcurrent::run([]() { return &GameDatabase::GetTrackHashesMap(); }); #endif - QtProgressCallback progress_callback(this); + QtModalProgressCallback progress_callback(this); progress_callback.SetProgressRange(image->GetTrackCount()); std::vector track_hashes; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 868338409..3a81f01b7 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -10,6 +10,7 @@ #include "core/memory_card.h" #include "core/settings.h" #include "core/system.h" +#include "coverdownloaddialog.h" #include "debuggerwindow.h" #include "displaywidget.h" #include "frontend-common/game_list.h" @@ -1828,6 +1829,7 @@ void MainWindow::connectSignals() connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); + connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(m_ui.actionCheatManager, &QAction::triggered, this, &MainWindow::onToolsCheatManagerTriggered); connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); @@ -2521,6 +2523,13 @@ void MainWindow::onToolsMemoryCardEditorTriggered() openMemoryCardEditor(QString(), QString()); } +void MainWindow::onToolsCoverDownloaderTriggered() +{ + CoverDownloadDialog dlg(this); + connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers); + dlg.exec(); +} + void MainWindow::onToolsCheatManagerTriggered() { if (!m_cheat_manager_dialog) diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 463132be0..436ad4c46 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -147,6 +147,7 @@ private Q_SLOTS: void onAboutActionTriggered(); void onCheckForUpdatesActionTriggered(); void onToolsMemoryCardEditorTriggered(); + void onToolsCoverDownloaderTriggered(); void onToolsCheatManagerTriggered(); void onToolsOpenDataDirectoryTriggered(); diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 5333f699a..b164ff034 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -17,10 +17,10 @@ DuckStation - + :/icons/duck.png:/icons/duck.png - + @@ -105,7 +105,8 @@ Theme - + + .. @@ -113,7 +114,8 @@ Language - + + .. @@ -229,6 +231,7 @@ &Tools + @@ -468,7 +471,7 @@ - + :/icons/github.png:/icons/github.png @@ -477,7 +480,7 @@ - + :/icons/IssueTracker.png:/icons/IssueTracker.png @@ -486,7 +489,7 @@ - + :/icons/discord.png:/icons/discord.png @@ -504,7 +507,7 @@ - + :/icons/QT.png:/icons/QT.png @@ -513,7 +516,7 @@ - + :/icons/duck_64.png:/icons/duck_64.png @@ -940,6 +943,11 @@ Big Picture + + + Cover Downloader + + diff --git a/src/duckstation-qt/qtprogresscallback.cpp b/src/duckstation-qt/qtprogresscallback.cpp index bf29b0f48..5a70afb31 100644 --- a/src/duckstation-qt/qtprogresscallback.cpp +++ b/src/duckstation-qt/qtprogresscallback.cpp @@ -1,10 +1,11 @@ #include "qtprogresscallback.h" +#include "common/assert.h" #include #include #include #include -QtProgressCallback::QtProgressCallback(QWidget* parent_widget, float show_delay) +QtModalProgressCallback::QtModalProgressCallback(QWidget* parent_widget, float show_delay) : QObject(parent_widget), m_dialog(QString(), QString(), 0, 1, parent_widget), m_show_delay(show_delay) { m_dialog.setWindowTitle(tr("DuckStation")); @@ -15,14 +16,14 @@ QtProgressCallback::QtProgressCallback(QWidget* parent_widget, float show_delay) checkForDelayedShow(); } -QtProgressCallback::~QtProgressCallback() = default; +QtModalProgressCallback::~QtModalProgressCallback() = default; -bool QtProgressCallback::IsCancelled() const +bool QtModalProgressCallback::IsCancelled() const { return m_dialog.wasCanceled(); } -void QtProgressCallback::SetCancellable(bool cancellable) +void QtModalProgressCallback::SetCancellable(bool cancellable) { if (m_cancellable == cancellable) return; @@ -31,12 +32,12 @@ void QtProgressCallback::SetCancellable(bool cancellable) m_dialog.setCancelButtonText(cancellable ? tr("Cancel") : QString()); } -void QtProgressCallback::SetTitle(const char* title) +void QtModalProgressCallback::SetTitle(const char* title) { m_dialog.setWindowTitle(QString::fromUtf8(title)); } -void QtProgressCallback::SetStatusText(const char* text) +void QtModalProgressCallback::SetStatusText(const char* text) { BaseProgressCallback::SetStatusText(text); checkForDelayedShow(); @@ -45,7 +46,7 @@ void QtProgressCallback::SetStatusText(const char* text) m_dialog.setLabelText(QString::fromUtf8(text)); } -void QtProgressCallback::SetProgressRange(u32 range) +void QtModalProgressCallback::SetProgressRange(u32 range) { BaseProgressCallback::SetProgressRange(range); checkForDelayedShow(); @@ -54,7 +55,7 @@ void QtProgressCallback::SetProgressRange(u32 range) m_dialog.setRange(0, m_progress_range); } -void QtProgressCallback::SetProgressValue(u32 value) +void QtModalProgressCallback::SetProgressValue(u32 value) { BaseProgressCallback::SetProgressValue(value); checkForDelayedShow(); @@ -65,43 +66,43 @@ void QtProgressCallback::SetProgressValue(u32 value) QCoreApplication::processEvents(); } -void QtProgressCallback::DisplayError(const char* message) +void QtModalProgressCallback::DisplayError(const char* message) { qWarning() << message; } -void QtProgressCallback::DisplayWarning(const char* message) +void QtModalProgressCallback::DisplayWarning(const char* message) { qWarning() << message; } -void QtProgressCallback::DisplayInformation(const char* message) +void QtModalProgressCallback::DisplayInformation(const char* message) { qWarning() << message; } -void QtProgressCallback::DisplayDebugMessage(const char* message) +void QtModalProgressCallback::DisplayDebugMessage(const char* message) { qWarning() << message; } -void QtProgressCallback::ModalError(const char* message) +void QtModalProgressCallback::ModalError(const char* message) { QMessageBox::critical(&m_dialog, tr("Error"), QString::fromUtf8(message)); } -bool QtProgressCallback::ModalConfirmation(const char* message) +bool QtModalProgressCallback::ModalConfirmation(const char* message) { return (QMessageBox::question(&m_dialog, tr("Question"), QString::fromUtf8(message), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes); } -void QtProgressCallback::ModalInformation(const char* message) +void QtModalProgressCallback::ModalInformation(const char* message) { QMessageBox::information(&m_dialog, tr("Information"), QString::fromUtf8(message)); } -void QtProgressCallback::checkForDelayedShow() +void QtModalProgressCallback::checkForDelayedShow() { if (m_dialog.isVisible()) return; @@ -113,3 +114,109 @@ void QtProgressCallback::checkForDelayedShow() m_dialog.show(); } } + +QtAsyncProgressThread::QtAsyncProgressThread(QWidget* parent) : QThread(parent) {} + +QtAsyncProgressThread::~QtAsyncProgressThread() = default; + +bool QtAsyncProgressThread::IsCancelled() const +{ + return isInterruptionRequested(); +} + +void QtAsyncProgressThread::SetCancellable(bool cancellable) +{ + if (m_cancellable == cancellable) + return; + + BaseProgressCallback::SetCancellable(cancellable); +} + +void QtAsyncProgressThread::SetTitle(const char* title) +{ + emit titleUpdated(QString::fromUtf8(title)); +} + +void QtAsyncProgressThread::SetStatusText(const char* text) +{ + BaseProgressCallback::SetStatusText(text); + emit statusUpdated(QString::fromUtf8(text)); +} + +void QtAsyncProgressThread::SetProgressRange(u32 range) +{ + BaseProgressCallback::SetProgressRange(range); + emit progressUpdated(static_cast(m_progress_value), static_cast(m_progress_range)); +} + +void QtAsyncProgressThread::SetProgressValue(u32 value) +{ + BaseProgressCallback::SetProgressValue(value); + emit progressUpdated(static_cast(m_progress_value), static_cast(m_progress_range)); +} + +void QtAsyncProgressThread::DisplayError(const char* message) +{ + qWarning() << message; +} + +void QtAsyncProgressThread::DisplayWarning(const char* message) +{ + qWarning() << message; +} + +void QtAsyncProgressThread::DisplayInformation(const char* message) +{ + qWarning() << message; +} + +void QtAsyncProgressThread::DisplayDebugMessage(const char* message) +{ + qWarning() << message; +} + +void QtAsyncProgressThread::ModalError(const char* message) +{ + QMessageBox::critical(parentWidget(), tr("Error"), QString::fromUtf8(message)); +} + +bool QtAsyncProgressThread::ModalConfirmation(const char* message) +{ + return (QMessageBox::question(parentWidget(), tr("Question"), QString::fromUtf8(message), QMessageBox::Yes, + QMessageBox::No) == QMessageBox::Yes); +} + +void QtAsyncProgressThread::ModalInformation(const char* message) +{ + QMessageBox::information(parentWidget(), tr("Information"), QString::fromUtf8(message)); +} + +void QtAsyncProgressThread::start() +{ + Assert(!isRunning()); + + QThread::start(); + moveToThread(this); + m_starting_thread = QThread::currentThread(); + m_start_semaphore.release(); +} + +void QtAsyncProgressThread::join() +{ + if (isRunning()) + QThread::wait(); +} + +void QtAsyncProgressThread::run() +{ + m_start_semaphore.acquire(); + emit threadStarting(); + runAsync(); + emit threadFinished(); + moveToThread(m_starting_thread); +} + +QWidget* QtAsyncProgressThread::parentWidget() const +{ + return qobject_cast(parent()); +} diff --git a/src/duckstation-qt/qtprogresscallback.h b/src/duckstation-qt/qtprogresscallback.h index 6f93c3883..8647ddf13 100644 --- a/src/duckstation-qt/qtprogresscallback.h +++ b/src/duckstation-qt/qtprogresscallback.h @@ -1,15 +1,18 @@ #pragma once #include "common/progress_callback.h" #include "common/timer.h" +#include +#include #include +#include -class QtProgressCallback final : public QObject, public BaseProgressCallback +class QtModalProgressCallback final : public QObject, public BaseProgressCallback { Q_OBJECT public: - QtProgressCallback(QWidget* parent_widget, float show_delay = 0.0f); - ~QtProgressCallback(); + QtModalProgressCallback(QWidget* parent_widget, float show_delay = 0.0f); + ~QtModalProgressCallback(); bool IsCancelled() const override; @@ -34,4 +37,51 @@ private: QProgressDialog m_dialog; Common::Timer m_show_timer; float m_show_delay; -}; \ No newline at end of file +}; + +class QtAsyncProgressThread : public QThread, public BaseProgressCallback +{ + Q_OBJECT + +public: + QtAsyncProgressThread(QWidget* parent); + ~QtAsyncProgressThread(); + + bool IsCancelled() const override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + +Q_SIGNALS: + void titleUpdated(const QString& title); + void statusUpdated(const QString& status); + void progressUpdated(int value, int range); + void threadStarting(); + void threadFinished(); + +public Q_SLOTS: + void start(); + void join(); + +protected: + virtual void runAsync() = 0; + void run() final; + +private: + QWidget* parentWidget() const; + + QSemaphore m_start_semaphore; + QThread* m_starting_thread = nullptr; +};