Qt: Add cover downloader

This commit is contained in:
Connor McLaughlin 2022-09-13 20:29:17 +10:00
parent 21b7261dc9
commit 389143db64
11 changed files with 464 additions and 30 deletions

View file

@ -48,6 +48,9 @@ set(SRCS
controllersettingsdialog.h
controllersettingsdialog.ui
controllersettingwidgetbinder.h
coverdownloaddialog.cpp
coverdownloaddialog.h
coverdownloaddialog.ui
debuggermodels.cpp
debuggermodels.h
debuggerwindow.cpp

View file

@ -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<bool>(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<CoverDownloadThread>(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);
}

View file

@ -0,0 +1,53 @@
#pragma once
#include "common/timer.h"
#include "common/types.h"
#include "qtprogresscallback.h"
#include "ui_coverdownloaddialog.h"
#include <QtWidgets/QDialog>
#include <array>
#include <memory>
#include <string>
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<std::string> m_urls;
};
void startThread();
void cancelThread();
Ui::CoverDownloadDialog m_ui;
std::unique_ptr<CoverDownloadThread> m_thread;
Common::Timer m_last_refresh_time;
};

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CoverDownloadDialog</class>
<widget class="QDialog" name="CoverDownloadDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>656</width>
<height>343</height>
</rect>
</property>
<property name="windowTitle">
<string>Download Covers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;In the form below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game's filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QTextEdit" name="urls"/>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string>Waiting to start...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QProgressBar" name="progress"/>
</item>
<item>
<widget class="QPushButton" name="start">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Start</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -16,6 +16,7 @@
<ClCompile Include="controllerbindingwidgets.cpp" />
<ClCompile Include="controllerglobalsettingswidget.cpp" />
<ClCompile Include="controllersettingsdialog.cpp" />
<ClCompile Include="coverdownloaddialog.cpp" />
<ClCompile Include="emulationsettingswidget.cpp" />
<ClCompile Include="debuggermodels.cpp" />
<ClCompile Include="debuggerwindow.cpp" />
@ -55,6 +56,7 @@
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="cheatmanagerdialog.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="coverdownloaddialog.h" />
<QtMoc Include="enhancementsettingswidget.h" />
<QtMoc Include="memorycardsettingswidget.h" />
<QtMoc Include="memorycardeditordialog.h" />
@ -210,6 +212,9 @@
<QtUi Include="foldersettingswidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="coverdownloaddialog.ui">
<FileType>Document</FileType>
</QtUi>
</ItemGroup>
<ItemGroup>
<QtResource Include="resources\resources.qrc">
@ -231,6 +236,7 @@
<ClCompile Include="$(IntDir)moc_controllerbindingwidgets.cpp" />
<ClCompile Include="$(IntDir)moc_controllerglobalsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_controllersettingsdialog.cpp" />
<ClCompile Include="$(IntDir)moc_coverdownloaddialog.cpp" />
<ClCompile Include="$(IntDir)moc_displaywidget.cpp" />
<ClCompile Include="$(IntDir)moc_emulationsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_enhancementsettingswidget.cpp" />

View file

@ -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<CDImageHasher::Hash> track_hashes;

View file

@ -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)

View file

@ -147,6 +147,7 @@ private Q_SLOTS:
void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered();
void onToolsMemoryCardEditorTriggered();
void onToolsCoverDownloaderTriggered();
void onToolsCheatManagerTriggered();
void onToolsOpenDataDirectoryTriggered();

View file

@ -17,10 +17,10 @@
<string>DuckStation</string>
</property>
<property name="windowIcon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<widget class="QStackedWidget" name="mainContainer" />
<widget class="QStackedWidget" name="mainContainer"/>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
@ -105,7 +105,8 @@
<string>Theme</string>
</property>
<property name="icon">
<iconset theme="paint-brush-line"/>
<iconset theme="paint-brush-line">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
<widget class="QMenu" name="menuSettingsLanguage">
@ -113,7 +114,8 @@
<string>Language</string>
</property>
<property name="icon">
<iconset theme="global-line"/>
<iconset theme="global-line">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
<addaction name="separator"/>
@ -229,6 +231,7 @@
<string>&amp;Tools</string>
</property>
<addaction name="actionMemory_Card_Editor"/>
<addaction name="actionCoverDownloader"/>
<addaction name="actionCheatManager"/>
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
@ -468,7 +471,7 @@
</action>
<action name="actionGitHubRepository">
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/github.png</normaloff>:/icons/github.png</iconset>
</property>
<property name="text">
@ -477,7 +480,7 @@
</action>
<action name="actionIssueTracker">
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/IssueTracker.png</normaloff>:/icons/IssueTracker.png</iconset>
</property>
<property name="text">
@ -486,7 +489,7 @@
</action>
<action name="actionDiscordServer">
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/discord.png</normaloff>:/icons/discord.png</iconset>
</property>
<property name="text">
@ -504,7 +507,7 @@
</action>
<action name="actionAboutQt">
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/QT.png</normaloff>:/icons/QT.png</iconset>
</property>
<property name="text">
@ -513,7 +516,7 @@
</action>
<action name="actionAbout">
<property name="icon">
<iconset resource="resources/resources.qrc">
<iconset>
<normaloff>:/icons/duck_64.png</normaloff>:/icons/duck_64.png</iconset>
</property>
<property name="text">
@ -940,6 +943,11 @@
<string>Big Picture</string>
</property>
</action>
<action name="actionCoverDownloader">
<property name="text">
<string>Cover Downloader</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>

View file

@ -1,10 +1,11 @@
#include "qtprogresscallback.h"
#include "common/assert.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtWidgets/QMessageBox>
#include <array>
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<int>(m_progress_value), static_cast<int>(m_progress_range));
}
void QtAsyncProgressThread::SetProgressValue(u32 value)
{
BaseProgressCallback::SetProgressValue(value);
emit progressUpdated(static_cast<int>(m_progress_value), static_cast<int>(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<QWidget*>(parent());
}

View file

@ -1,15 +1,18 @@
#pragma once
#include "common/progress_callback.h"
#include "common/timer.h"
#include <QtCore/QThread>
#include <QtCore/QSemaphore>
#include <QtWidgets/QProgressDialog>
#include <atomic>
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;
};
};
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;
};