Qt: Add cover downloader

This commit is contained in:
Connor McLaughlin 2022-09-03 21:29:02 +10:00 committed by refractionpcsx2
parent 6b245f34a2
commit 8b16a7a8c7
17 changed files with 859 additions and 5 deletions

View File

@ -17,6 +17,9 @@ target_sources(pcsx2-qt PRIVATE
AutoUpdaterDialog.cpp AutoUpdaterDialog.cpp
AutoUpdaterDialog.h AutoUpdaterDialog.h
AutoUpdaterDialog.ui AutoUpdaterDialog.ui
CoverDownloadDialog.cpp
CoverDownloadDialog.h
CoverDownloadDialog.ui
DisplayWidget.cpp DisplayWidget.cpp
DisplayWidget.h DisplayWidget.h
EarlyHardwareCheck.cpp EarlyHardwareCheck.cpp
@ -28,6 +31,8 @@ target_sources(pcsx2-qt PRIVATE
QtHost.cpp QtHost.cpp
QtHost.h QtHost.h
QtKeyCodes.cpp QtKeyCodes.cpp
QtProgressCallback.cpp
QtProgressCallback.h
QtUtils.cpp QtUtils.cpp
QtUtils.h QtUtils.h
SettingWidgetBinder.h SettingWidgetBinder.h

View File

@ -0,0 +1,139 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/Assertions.h"
#include "pcsx2/Frontend/GameList.h"
#include "CoverDownloadDialog.h"
CoverDownloadDialog::CoverDownloadDialog(QWidget* parent /*= nullptr*/)
: QDialog(parent)
{
m_ui.setupUi(this);
m_ui.coverIcon->setPixmap(QIcon::fromTheme("image-fill").pixmap(32));
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()
{
pxAssert(!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, otherwise it's way too flickery.
// Ideally in the future we'd have some way to invalidate only a single cover.
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(), m_ui.useSerialFileNames->isChecked());
m_last_refresh_time.Reset();
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, bool use_serials)
: QtAsyncProgressThread(parent), m_use_serials(use_serials)
{
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, m_use_serials, this);
}

View File

@ -0,0 +1,69 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/Timer.h"
#include "common/Pcsx2Defs.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, bool use_serials);
~CoverDownloadThread();
protected:
void runAsync() override;
private:
std::vector<std::string> m_urls;
bool m_use_serials;
};
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,117 @@
<?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>720</width>
<height>380</height>
</rect>
</property>
<property name="windowTitle">
<string>Download Covers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="coverIcon">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources/resources.qrc">:/icons/black/svg/image-fill.svg</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>PCSX2 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.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box 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>
<widget class="QTextEdit" name="urls"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>By default, the downloaded covers will be saved with the game's title. If this is not desired, you can check the &quot;Use Serial File Names&quot; box below. Using serials instead of game titles will prevent conflicts when multiple regions of the same game are used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="useSerialFileNames">
<property name="text">
<string>Use Serial File Names</string>
</property>
</widget>
</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>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -247,7 +247,10 @@ bool DisplayWidget::event(QEvent* event)
// Forward text input to imgui. // Forward text input to imgui.
if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress) if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress)
{ {
const QString text(key_event->text()); // Don't forward backspace characters. We send the backspace as a normal key event,
// so if we send the character too, it double-deletes.
QString text(key_event->text());
text.remove(QChar('\b'));
if (!text.isEmpty()) if (!text.isEmpty())
ImGuiManager::AddTextInput(text.toStdString()); ImGuiManager::AddTextInput(text.toStdString());
} }

View File

@ -40,6 +40,7 @@
#include "AboutDialog.h" #include "AboutDialog.h"
#include "AutoUpdaterDialog.h" #include "AutoUpdaterDialog.h"
#include "CoverDownloadDialog.h"
#include "DisplayWidget.h" #include "DisplayWidget.h"
#include "GameList/GameListRefreshThread.h" #include "GameList/GameListRefreshThread.h"
#include "GameList/GameListWidget.h" #include "GameList/GameListWidget.h"
@ -291,6 +292,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() {
if (isShowingGameList()) if (isShowingGameList())
@ -1495,6 +1497,13 @@ void MainWindow::onToolsOpenDataDirectoryTriggered()
QtUtils::OpenURL(this, QUrl::fromLocalFile(path)); QtUtils::OpenURL(this, QUrl::fromLocalFile(path));
} }
void MainWindow::onToolsCoverDownloaderTriggered()
{
CoverDownloadDialog dlg(this);
connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers);
dlg.exec();
}
void MainWindow::updateTheme() void MainWindow::updateTheme()
{ {
updateApplicationTheme(); updateApplicationTheme();

View File

@ -148,6 +148,7 @@ private Q_SLOTS:
void onAboutActionTriggered(); void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered(); void onCheckForUpdatesActionTriggered();
void onToolsOpenDataDirectoryTriggered(); void onToolsOpenDataDirectoryTriggered();
void onToolsCoverDownloaderTriggered();
void updateTheme(); void updateTheme();
void onScreenshotActionTriggered(); void onScreenshotActionTriggered();
void onSaveGSDumpActionTriggered(); void onSaveGSDumpActionTriggered();

View File

@ -192,6 +192,7 @@
<addaction name="actionInputRecControllerLogs"/> <addaction name="actionInputRecControllerLogs"/>
</widget> </widget>
<addaction name="actionOpenDataDirectory"/> <addaction name="actionOpenDataDirectory"/>
<addaction name="actionCoverDownloader"/>
<addaction name="menuInput_Recording"/> <addaction name="menuInput_Recording"/>
</widget> </widget>
<addaction name="menuSystem"/> <addaction name="menuSystem"/>
@ -849,6 +850,11 @@
<string>Big Picture</string> <string>Big Picture</string>
</property> </property>
</action> </action>
<action name="actionCoverDownloader">
<property name="text">
<string>Cover Downloader...</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="resources/resources.qrc"/> <include location="resources/resources.qrc"/>

View File

@ -0,0 +1,247 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "common/Assertions.h"
#include "QtProgressCallback.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtWidgets/QMessageBox>
#include <array>
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("PCSX2"));
m_dialog.setMinimumSize(QSize(500, 0));
m_dialog.setModal(parent_widget != nullptr);
m_dialog.setAutoClose(false);
m_dialog.setAutoReset(false);
checkForDelayedShow();
}
QtModalProgressCallback::~QtModalProgressCallback() = default;
bool QtModalProgressCallback::IsCancelled() const
{
return m_dialog.wasCanceled();
}
void QtModalProgressCallback::SetCancellable(bool cancellable)
{
if (m_cancellable == cancellable)
return;
BaseProgressCallback::SetCancellable(cancellable);
m_dialog.setCancelButtonText(cancellable ? tr("Cancel") : QString());
}
void QtModalProgressCallback::SetTitle(const char* title)
{
m_dialog.setWindowTitle(QString::fromUtf8(title));
}
void QtModalProgressCallback::SetStatusText(const char* text)
{
BaseProgressCallback::SetStatusText(text);
checkForDelayedShow();
if (m_dialog.isVisible())
m_dialog.setLabelText(QString::fromUtf8(text));
}
void QtModalProgressCallback::SetProgressRange(u32 range)
{
BaseProgressCallback::SetProgressRange(range);
checkForDelayedShow();
if (m_dialog.isVisible())
m_dialog.setRange(0, m_progress_range);
}
void QtModalProgressCallback::SetProgressValue(u32 value)
{
BaseProgressCallback::SetProgressValue(value);
checkForDelayedShow();
if (m_dialog.isVisible() && static_cast<u32>(m_dialog.value()) != m_progress_range)
m_dialog.setValue(m_progress_value);
QCoreApplication::processEvents();
}
void QtModalProgressCallback::DisplayError(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayWarning(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayInformation(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::DisplayDebugMessage(const char* message)
{
qWarning() << message;
}
void QtModalProgressCallback::ModalError(const char* message)
{
QMessageBox::critical(&m_dialog, tr("Error"), QString::fromUtf8(message));
}
bool QtModalProgressCallback::ModalConfirmation(const char* message)
{
return (QMessageBox::question(&m_dialog, tr("Question"), QString::fromUtf8(message), QMessageBox::Yes,
QMessageBox::No) == QMessageBox::Yes);
}
void QtModalProgressCallback::ModalInformation(const char* message)
{
QMessageBox::information(&m_dialog, tr("Information"), QString::fromUtf8(message));
}
void QtModalProgressCallback::checkForDelayedShow()
{
if (m_dialog.isVisible())
return;
if (m_show_timer.GetTimeSeconds() >= m_show_delay)
{
m_dialog.setRange(0, m_progress_range);
m_dialog.setValue(m_progress_value);
m_dialog.show();
}
}
QtAsyncProgressThread::QtAsyncProgressThread(QWidget* parent)
: QThread()
{
// NOTE: We deliberately don't set the thread parent, because otherwise we can't move it.
}
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()
{
pxAssertRel(!isRunning(), "Async progress thread is not already running");
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

@ -0,0 +1,102 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2022 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "common/ProgressCallback.h"
#include "common/Timer.h"
#include <QtCore/QThread>
#include <QtCore/QSemaphore>
#include <QtWidgets/QProgressDialog>
#include <atomic>
class QtModalProgressCallback final : public QObject, public BaseProgressCallback
{
Q_OBJECT
public:
QtModalProgressCallback(QWidget* parent_widget, float show_delay = 0.0f);
~QtModalProgressCallback();
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;
private:
void checkForDelayedShow();
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;
};

View File

@ -135,6 +135,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="EarlyHardwareCheck.cpp" /> <ClCompile Include="EarlyHardwareCheck.cpp" />
<ClCompile Include="QtProgressCallback.cpp" />
<ClCompile Include="Settings\FolderSettingsWidget.cpp" /> <ClCompile Include="Settings\FolderSettingsWidget.cpp" />
<ClCompile Include="Tools\InputRecording\NewInputRecordingDlg.cpp" /> <ClCompile Include="Tools\InputRecording\NewInputRecordingDlg.cpp" />
<ClCompile Include="Settings\BIOSSettingsWidget.cpp" /> <ClCompile Include="Settings\BIOSSettingsWidget.cpp" />
@ -165,6 +166,7 @@
<ClCompile Include="GameList\GameListWidget.cpp" /> <ClCompile Include="GameList\GameListWidget.cpp" />
<ClCompile Include="AboutDialog.cpp" /> <ClCompile Include="AboutDialog.cpp" />
<ClCompile Include="AutoUpdaterDialog.cpp" /> <ClCompile Include="AutoUpdaterDialog.cpp" />
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="DisplayWidget.cpp" /> <ClCompile Include="DisplayWidget.cpp" />
<ClCompile Include="QtHost.cpp" /> <ClCompile Include="QtHost.cpp" />
<ClCompile Include="MainWindow.cpp" /> <ClCompile Include="MainWindow.cpp" />
@ -193,6 +195,7 @@
<QtMoc Include="Settings\DEV9DnsHostDialog.h" /> <QtMoc Include="Settings\DEV9DnsHostDialog.h" />
<QtMoc Include="Settings\DEV9SettingsWidget.h" /> <QtMoc Include="Settings\DEV9SettingsWidget.h" />
<QtMoc Include="Settings\DEV9UiCommon.h" /> <QtMoc Include="Settings\DEV9UiCommon.h" />
<QtMoc Include="QtProgressCallback.h" />
<ClInclude Include="Settings\ControllerSettingWidgetBinder.h" /> <ClInclude Include="Settings\ControllerSettingWidgetBinder.h" />
<QtMoc Include="Settings\FolderSettingsWidget.h" /> <QtMoc Include="Settings\FolderSettingsWidget.h" />
<ClInclude Include="Settings\HddCreateQt.h" /> <ClInclude Include="Settings\HddCreateQt.h" />
@ -210,6 +213,7 @@
<ClInclude Include="PrecompiledHeader.h" /> <ClInclude Include="PrecompiledHeader.h" />
<QtMoc Include="AboutDialog.h" /> <QtMoc Include="AboutDialog.h" />
<QtMoc Include="AutoUpdaterDialog.h" /> <QtMoc Include="AutoUpdaterDialog.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="MainWindow.h" /> <QtMoc Include="MainWindow.h" />
<QtMoc Include="DisplayWidget.h" /> <QtMoc Include="DisplayWidget.h" />
</ItemGroup> </ItemGroup>
@ -247,9 +251,11 @@
<ClCompile Include="$(IntDir)GameList\moc_GameListWidget.cpp" /> <ClCompile Include="$(IntDir)GameList\moc_GameListWidget.cpp" />
<ClCompile Include="$(IntDir)moc_AboutDialog.cpp" /> <ClCompile Include="$(IntDir)moc_AboutDialog.cpp" />
<ClCompile Include="$(IntDir)moc_AutoUpdaterDialog.cpp" /> <ClCompile Include="$(IntDir)moc_AutoUpdaterDialog.cpp" />
<ClCompile Include="$(IntDir)moc_CoverDownloadDialog.cpp" />
<ClCompile Include="$(IntDir)moc_DisplayWidget.cpp" /> <ClCompile Include="$(IntDir)moc_DisplayWidget.cpp" />
<ClCompile Include="$(IntDir)moc_MainWindow.cpp" /> <ClCompile Include="$(IntDir)moc_MainWindow.cpp" />
<ClCompile Include="$(IntDir)moc_QtHost.cpp" /> <ClCompile Include="$(IntDir)moc_QtHost.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)Tools\InputRecording\moc_NewInputRecordingDlg.cpp" /> <ClCompile Include="$(IntDir)Tools\InputRecording\moc_NewInputRecordingDlg.cpp" />
<ClCompile Include="$(IntDir)qrc_resources.cpp"> <ClCompile Include="$(IntDir)qrc_resources.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader> <PrecompiledHeader>NotUsing</PrecompiledHeader>
@ -328,6 +334,9 @@
<QtUi Include="AutoUpdaterDialog.ui"> <QtUi Include="AutoUpdaterDialog.ui">
<FileType>Document</FileType> <FileType>Document</FileType>
</QtUi> </QtUi>
<QtUi Include="CoverDownloadDialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="Tools\InputRecording\NewInputRecordingDlg.ui"> <QtUi Include="Tools\InputRecording\NewInputRecordingDlg.ui">
<FileType>Document</FileType> <FileType>Document</FileType>
</QtUi> </QtUi>

View File

@ -221,6 +221,14 @@
<ClCompile Include="$(IntDir)Settings\moc_FolderSettingsWidget.cpp"> <ClCompile Include="$(IntDir)Settings\moc_FolderSettingsWidget.cpp">
<Filter>moc</Filter> <Filter>moc</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="$(IntDir)moc_CoverDownloadDialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp">
<Filter>moc</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Manifest Include="..\pcsx2\windows\PCSX2.manifest"> <Manifest Include="..\pcsx2\windows\PCSX2.manifest">
@ -229,7 +237,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="PrecompiledHeader.h" /> <ClInclude Include="PrecompiledHeader.h" />
<ClInclude Include="QtHost.h" />
<ClInclude Include="QtUtils.h" /> <ClInclude Include="QtUtils.h" />
<ClInclude Include="SettingWidgetBinder.h" /> <ClInclude Include="SettingWidgetBinder.h" />
<ClInclude Include="Settings\HddCreateQt.h"> <ClInclude Include="Settings\HddCreateQt.h">
@ -325,6 +332,9 @@
<QtMoc Include="Settings\FolderSettingsWidget.h"> <QtMoc Include="Settings\FolderSettingsWidget.h">
<Filter>Settings</Filter> <Filter>Settings</Filter>
</QtMoc> </QtMoc>
<QtMoc Include="QtHost.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="QtProgressCallback.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<QtResource Include="resources\resources.qrc"> <QtResource Include="resources\resources.qrc">
@ -409,6 +419,7 @@
<QtUi Include="Settings\ControllerMacroEditWidget.ui"> <QtUi Include="Settings\ControllerMacroEditWidget.ui">
<Filter>Settings</Filter> <Filter>Settings</Filter>
</QtUi> </QtUi>
<QtUi Include="CoverDownloadDialog.ui" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Settings\FolderSettingsWidget.ui"> <None Include="Settings\FolderSettingsWidget.ui">

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20 5H4v14l9.292-9.294a1 1 0 0 1 1.414 0L20 15.01V5zM2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M20 5H4v14l9.292-9.294a1 1 0 0 1 1.414 0L20 15.01V5zM2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" fill="#ffffff"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -33,6 +33,7 @@
<file>icons/black/svg/gamepad-line.svg</file> <file>icons/black/svg/gamepad-line.svg</file>
<file>icons/black/svg/global-line.svg</file> <file>icons/black/svg/global-line.svg</file>
<file>icons/black/svg/hard-drive-2-line.svg</file> <file>icons/black/svg/hard-drive-2-line.svg</file>
<file>icons/black/svg/image-fill.svg</file>
<file>icons/black/svg/keyboard-line.svg</file> <file>icons/black/svg/keyboard-line.svg</file>
<file>icons/black/svg/layout-grid-line.svg</file> <file>icons/black/svg/layout-grid-line.svg</file>
<file>icons/black/svg/list-check.svg</file> <file>icons/black/svg/list-check.svg</file>
@ -87,6 +88,7 @@
<file>icons/white/svg/gamepad-line.svg</file> <file>icons/white/svg/gamepad-line.svg</file>
<file>icons/white/svg/global-line.svg</file> <file>icons/white/svg/global-line.svg</file>
<file>icons/white/svg/hard-drive-2-line.svg</file> <file>icons/white/svg/hard-drive-2-line.svg</file>
<file>icons/white/svg/image-fill.svg</file>
<file>icons/white/svg/keyboard-line.svg</file> <file>icons/white/svg/keyboard-line.svg</file>
<file>icons/white/svg/layout-grid-line.svg</file> <file>icons/white/svg/layout-grid-line.svg</file>
<file>icons/white/svg/list-check.svg</file> <file>icons/white/svg/list-check.svg</file>

View File

@ -20,6 +20,7 @@
#include "common/Assertions.h" #include "common/Assertions.h"
#include "common/Console.h" #include "common/Console.h"
#include "common/FileSystem.h" #include "common/FileSystem.h"
#include "common/HTTPDownloader.h"
#include "common/Path.h" #include "common/Path.h"
#include "common/ProgressCallback.h" #include "common/ProgressCallback.h"
#include "common/StringUtil.h" #include "common/StringUtil.h"
@ -778,7 +779,7 @@ std::string GameList::GetCoverImagePath(const std::string& path, const std::stri
return cover_path; return cover_path;
} }
std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename) std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial)
{ {
const char* extension = std::strrchr(new_filename, '.'); const char* extension = std::strrchr(new_filename, '.');
if (!extension) if (!extension)
@ -792,7 +793,132 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha
return existing_filename; return existing_filename;
} }
std::string cover_filename(entry->title + extension); std::string cover_filename(use_serial ? (entry->serial + extension) : (entry->title + extension));
Path::SanitizeFileName(&cover_filename); Path::SanitizeFileName(&cover_filename);
return Path::Combine(EmuFolders::Covers, cover_filename); return Path::Combine(EmuFolders::Covers, cover_filename);
} }
bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial, ProgressCallback* progress,
std::function<void(const Entry*, std::string)> save_callback)
{
if (!progress)
progress = ProgressCallback::NullProgressCallback;
bool has_title = false;
bool has_file_title = false;
bool has_serial = false;
for (const std::string& url_template : url_templates)
{
if (!has_title && url_template.find("${title}") != std::string::npos)
has_title = true;
if (!has_file_title && url_template.find("${filetitle}") != std::string::npos)
has_file_title = true;
if (!has_serial && url_template.find("${serial}") != std::string::npos)
has_serial = true;
}
if (!has_title && !has_file_title && !has_serial)
{
progress->DisplayError("URL template must contain at least one of ${title}, ${filetitle}, or ${serial}.");
return false;
}
std::vector<std::pair<std::string, std::string>> download_urls;
{
std::unique_lock lock(s_mutex);
for (const GameList::Entry& entry : m_entries)
{
const std::string existing_path(GetCoverImagePathForEntry(&entry));
if (!existing_path.empty())
continue;
for (const std::string& url_template : url_templates)
{
std::string url(url_template);
if (has_title)
StringUtil::ReplaceAll(&url, "${title}", Common::HTTPDownloader::URLEncode(entry.title));
if (has_file_title)
{
std::string display_name(FileSystem::GetDisplayNameFromPath(entry.path));
StringUtil::ReplaceAll(&url, "${filetitle}", Common::HTTPDownloader::URLEncode(Path::GetFileTitle(display_name)));
}
if (has_serial)
StringUtil::ReplaceAll(&url, "${serial}", Common::HTTPDownloader::URLEncode(entry.serial));
download_urls.emplace_back(entry.path, std::move(url));
}
}
}
if (download_urls.empty())
{
progress->DisplayError("No URLs to download enumerated.");
return false;
}
std::unique_ptr<Common::HTTPDownloader> downloader(Common::HTTPDownloader::Create());
if (!downloader)
{
progress->DisplayError("Failed to create HTTP downloader.");
return false;
}
progress->SetCancellable(true);
progress->SetProgressRange(static_cast<u32>(download_urls.size()));
for (auto& [entry_path, url] : download_urls)
{
if (progress->IsCancelled())
break;
// make sure it didn't get done already
{
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
{
progress->IncrementProgressValue();
continue;
}
progress->SetFormattedStatusText("Downloading cover for %s [%s]...", entry->title.c_str(), entry->serial.c_str());
}
// we could actually do a few in parallel here...
std::string filename(Common::HTTPDownloader::URLDecode(url));
downloader->CreateRequest(std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path),
filename = std::move(filename)](s32 status_code, std::string content_type, Common::HTTPDownloader::Request::Data data) {
if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty())
return;
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
return;
// prefer the content type from the response for the extension
// otherwise, if it's missing, and the request didn't have an extension.. fall back to jpegs.
std::string template_filename;
std::string content_type_extension(Common::HTTPDownloader::GetExtensionForContentType(content_type));
// don't treat the domain name as an extension..
const std::string::size_type last_slash = filename.find('/');
const std::string::size_type last_dot = filename.find('.');
if (!content_type_extension.empty())
template_filename = fmt::format("cover.{}", content_type_extension);
else if (last_slash != std::string::npos && last_dot != std::string::npos && last_dot > last_slash)
template_filename = Path::GetFileName(filename);
else
template_filename = "cover.jpg";
std::string write_path(GetNewCoverImagePathForEntry(entry, template_filename.c_str(), use_serial));
if (write_path.empty())
return;
if (FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()) && save_callback)
save_callback(entry, std::move(write_path));
});
downloader->WaitForAllRequests();
progress->IncrementProgressValue();
}
return true;
}

View File

@ -17,6 +17,7 @@
#include "GameDatabase.h" #include "GameDatabase.h"
#include "common/Pcsx2Defs.h" #include "common/Pcsx2Defs.h"
#include <ctime> #include <ctime>
#include <functional>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
@ -124,5 +125,10 @@ namespace GameList
std::string GetCoverImagePathForEntry(const Entry* entry); std::string GetCoverImagePathForEntry(const Entry* entry);
std::string GetCoverImagePath(const std::string& path, const std::string& code, const std::string& title); std::string GetCoverImagePath(const std::string& path, const std::string& code, const std::string& title);
std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename); std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial = false);
/// Downloads covers using the specified URL templates. By default, covers are saved by title, but this can be changed with
/// the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to.
bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial = false, ProgressCallback* progress = nullptr,
std::function<void(const Entry*, std::string)> save_callback = {});
} // namespace GameList } // namespace GameList