FullscreenUI: Implement cover download support

Signed-off-by: SternXD <[email protected]>
This commit is contained in:
SternXD
2026-06-07 14:53:56 -04:00
committed by Ty
parent 6683f84da0
commit ee57382d6e
2 changed files with 396 additions and 11 deletions
+378 -11
View File
@@ -379,7 +379,7 @@ bool FullscreenUI::HasActiveWindow()
bool FullscreenUI::AreAnyDialogsOpen()
{
return (s_save_state_selector_open || s_about_window_open ||
return (s_save_state_selector_open || s_about_window_open || s_cover_downloader_open ||
s_input_binding_type != InputBindingInfo::Type::Unknown || ImGuiFullscreen::IsChoiceDialogOpen() ||
ImGuiFullscreen::IsFileSelectorOpen());
}
@@ -532,6 +532,7 @@ void FullscreenUI::Shutdown(bool clear_state)
{
CancelAllHddOperations();
CloseSaveStateSelector();
CloseCoverDownloaderWindow();
s_cover_image_map.clear();
s_game_list_sorted_entries = {};
s_game_list_directories_cache = {};
@@ -649,6 +650,9 @@ void FullscreenUI::Render()
if (s_about_window_open)
DrawAboutWindow();
if (s_cover_downloader_open)
DrawCoverDownloaderWindow();
if (s_achievements_login_open || ImGui::IsPopupOpen("RetroAchievements"))
DrawAchievementsLoginWindow();
@@ -2611,6 +2615,10 @@ void FullscreenUI::DrawGameListWindow()
s_current_main_window = MainWindowType::GameListSettings;
QueueResetFocus(FocusResetType::WindowChanged);
}
else if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack, false) || ImGui::IsKeyPressed(ImGuiKey_F4, false))
{
OpenCoverDownloaderWindow();
}
switch (s_game_list_view)
{
@@ -2643,6 +2651,7 @@ void FullscreenUI::DrawGameListWindow()
const auto glyphs = GetGamepadGlyphs();
SetFullscreenFooterText(std::array{
std::make_pair(glyphs.dpad, FSUI_VSTR("Select Game")),
std::make_pair(glyphs.select, FSUI_VSTR("Cover Downloader")),
std::make_pair(glyphs.start, FSUI_VSTR("Settings")),
std::make_pair(swapNorthWest ? glyphs.west : glyphs.north, FSUI_VSTR("Change View")),
std::make_pair(swapNorthWest ? glyphs.north : glyphs.west, FSUI_VSTR("Launch Options")),
@@ -2657,6 +2666,7 @@ void FullscreenUI::DrawGameListWindow()
std::make_pair(ICON_PF_F1, FSUI_VSTR("Change View")),
std::make_pair(ICON_PF_F2, FSUI_VSTR("Settings")),
std::make_pair(ICON_PF_F3, FSUI_VSTR("Launch Options")),
std::make_pair(ICON_PF_F4, FSUI_VSTR("Cover Downloader")),
std::make_pair(ICON_PF_ENTER, FSUI_VSTR("Start Game")),
std::make_pair(ICON_PF_ESC, FSUI_VSTR("Back")),
});
@@ -3228,16 +3238,6 @@ void FullscreenUI::DrawGameListSettingsWindow()
"FullscreenUIShowGameGridTitles", true);
}
MenuHeading(FSUI_CSTR("Cover Settings"));
{
DrawFolderSetting(bsi, FSUI_ICONSTR(ICON_FA_FOLDER, "Covers Directory"), "Folders", "Covers", EmuFolders::Covers);
if (MenuButton(
FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Download Covers"), FSUI_CSTR("Downloads covers from a user-specified URL template.")))
{
Host::OnCoverDownloaderOpenRequested();
}
}
MenuHeading(FSUI_CSTR("Operations"));
{
if (MenuButton(
@@ -3482,6 +3482,373 @@ void FullscreenUI::DrawAboutWindow()
ImGui::PopFont();
}
void FullscreenUI::OpenCoverDownloaderWindow()
{
s_cover_downloader_open = true;
s_cover_downloader_urls_buffer.fill('\0');
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_use_title_filenames = false;
s_cover_downloader_downloading = false;
s_cover_downloader_has_error = false;
}
QueueResetFocus(FocusResetType::PopupOpened);
}
void FullscreenUI::CloseCoverDownloaderWindow()
{
if (s_cover_downloader_thread)
{
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_downloading = false;
}
if (s_cover_downloader_thread->joinable())
{
s_cover_downloader_thread->join();
}
s_cover_downloader_thread.reset();
}
s_cover_downloader_open = false;
s_cover_downloader_urls_buffer.fill('\0');
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_use_title_filenames = false;
s_cover_downloader_has_error = false;
}
}
void FullscreenUI::CoverDownloaderThreadFunc(const std::vector<std::string>& urls)
{
class CoverDownloaderProgressCallback : public ProgressCallback
{
public:
CoverDownloaderProgressCallback() = default;
~CoverDownloaderProgressCallback() = default;
void PushState() override {}
void PopState() override {}
bool IsCancelled() const override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
return !s_cover_downloader_downloading;
}
bool IsCancellable() const override
{
return true;
}
void SetCancellable(bool cancellable) override
{
}
void SetTitle(const char* title) override
{
}
void SetStatusText(const char* text) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_status = text;
}
void SetProgressRange(u32 range) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_progress_max = range;
s_cover_downloader_progress_value = 0;
}
void SetProgressValue(u32 value) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_progress_value = value;
}
void IncrementProgressValue() override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_progress_value++;
}
void SetProgressState(ProgressState state) override
{
}
void DisplayError(const char* message) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_status = fmt::format(FSUI_FSTR("Error: {}"), message);
s_cover_downloader_has_error = true;
}
void DisplayWarning(const char* message) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_status = fmt::format(FSUI_FSTR("Warning: {}"), message);
s_cover_downloader_has_error = false;
}
void DisplayInformation(const char* message) override
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_status = message;
s_cover_downloader_has_error = false;
}
void DisplayDebugMessage(const char* message) override
{
}
void ModalError(const char* message) override
{
DisplayError(message);
}
bool ModalConfirmation(const char* message) override
{
return false;
}
void ModalInformation(const char* message) override
{
DisplayInformation(message);
}
};
CoverDownloaderProgressCallback callback;
bool use_title_filenames;
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
use_title_filenames = s_cover_downloader_use_title_filenames;
}
GameList::DownloadCovers(urls, !use_title_filenames, &callback);
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
if (s_cover_downloader_has_error && !s_cover_downloader_status.empty())
{
std::string error_message = s_cover_downloader_status;
MTGS::RunOnGSThread([error_message]() {
ShowToast(FSUI_STR("Download Failed"), error_message, 5.0f);
});
}
// We clear the cover image cache so the newly downloaded covers are picked up
MTGS::RunOnGSThread([]() {
s_cover_image_map.clear();
});
s_cover_downloader_downloading = false;
}
}
void FullscreenUI::DrawCoverDownloaderWindow()
{
if (!s_cover_downloader_open)
return;
ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f));
ImGui::SetNextWindowPos(
(ImGui::GetIO().DisplaySize - LayoutScale(0.0f, ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)) * 0.5f,
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Cover Downloader"));
ImGui::PushFont(g_large_font.first, g_large_font.second);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, UIPopupBackgroundColor);
bool is_open = !WantsToCloseMenu();
const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar;
if (ImGui::BeginPopupModal(FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Cover Downloader"), &is_open, flags))
{
ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
BeginMenuButtons();
ResetFocusHere();
ImGui::TextWrapped("%s", FSUI_CSTR("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."));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(8.0f));
ImGui::TextWrapped("%s", FSUI_CSTR("Enter one or more cover image URL templates below. Variables such as ${serial} and ${title} are supported. See the Qt Cover Downloader for more information."));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
// URLs input section
bool is_downloading;
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
is_downloading = s_cover_downloader_downloading;
}
{
ImGui::TextUnformatted(FSUI_CSTR("URLs:"));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(4.0f));
const float text_box_height = LayoutScale(55.0f);
ImGui::SetNextItemWidth(-1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, LayoutScale(8.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(12.0f, 10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (is_downloading)
ImGui::BeginDisabled();
ImGui::InputTextMultiline("##urls", s_cover_downloader_urls_buffer.data(), s_cover_downloader_urls_buffer.size(),
ImVec2(-1.0f, text_box_height), ImGuiInputTextFlags_AllowTabInput);
if (is_downloading)
ImGui::EndDisabled();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(3);
}
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
// Use Title File Names toggle
{
bool use_title;
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
use_title = s_cover_downloader_use_title_filenames;
}
if (ToggleButton(FSUI_ICONSTR(ICON_FA_FILE_SIGNATURE, "Use Title File Names"),
FSUI_CSTR("Saves covers using the game's title instead of serial number."),
&use_title, !is_downloading))
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_use_title_filenames = use_title;
}
}
// Progress display
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
if (s_cover_downloader_downloading)
{
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
if (!s_cover_downloader_status.empty())
ImGui::TextUnformatted(s_cover_downloader_status.c_str());
else
ImGui::TextUnformatted(FSUI_CSTR("Downloading covers..."));
if (s_cover_downloader_progress_max > 0)
{
const float progress = static_cast<float>(s_cover_downloader_progress_value) /
static_cast<float>(s_cover_downloader_progress_max);
const int percent = static_cast<int>(std::round(progress * 100.0f));
ImGui::ProgressBar(progress, ImVec2(-1.0f, LayoutScale(30.0f)), fmt::format("{}%", percent).c_str());
}
else
{
const float fraction = std::fmod(ImGui::GetTime(), 2.0f) * 0.5f;
ImGui::ProgressBar(fraction, ImVec2(-1.0f, LayoutScale(30.0f)));
}
}
}
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f));
const bool has_urls = (s_cover_downloader_urls_buffer[0] != '\0');
bool start_enabled;
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
start_enabled = !s_cover_downloader_downloading && has_urls;
}
if (is_downloading)
{
if (ActiveButton(FSUI_ICONSTR(ICON_FA_STOP, "Stop"), false))
{
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
s_cover_downloader_downloading = false;
}
if (s_cover_downloader_thread && s_cover_downloader_thread->joinable())
s_cover_downloader_thread->join();
s_cover_downloader_thread.reset();
}
}
else
{
if (ActiveButton(FSUI_ICONSTR(ICON_FA_PLAY, "Start"), false, start_enabled))
{
std::vector<std::string> urls;
std::string urls_text(s_cover_downloader_urls_buffer.data());
for (const auto& line : StringUtil::SplitString(urls_text, '\n', true))
{
if (!line.empty())
urls.push_back(std::string(line));
}
if (s_cover_downloader_thread && s_cover_downloader_thread->joinable())
{
s_cover_downloader_thread->join();
s_cover_downloader_thread.reset();
}
bool should_start_download = false;
{
std::lock_guard<std::mutex> lock(s_cover_downloader_mutex);
if (!s_cover_downloader_downloading)
{
s_cover_downloader_progress_max = 0;
s_cover_downloader_progress_value = 0;
s_cover_downloader_status.clear();
s_cover_downloader_has_error = false;
s_cover_downloader_downloading = true;
should_start_download = true;
}
}
if (should_start_download)
s_cover_downloader_thread = std::make_unique<std::thread>(CoverDownloaderThreadFunc, urls);
}
}
if (ActiveButton(FSUI_ICONSTR(ICON_FA_XMARK, "Close"), false))
{
is_open = false;
}
EndMenuButtons();
ImGui::PopStyleColor(1);
ImGui::EndPopup();
}
else
{
is_open = false;
}
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(4);
ImGui::PopFont();
if (!is_open)
{
ImGui::CloseCurrentPopup();
s_cover_downloader_open = false;
CloseCoverDownloaderWindow();
}
}
bool FullscreenUI::OpenAchievementsWindow()
{
if (!VMManager::HasValidVM() || !Achievements::IsActive())
+18
View File
@@ -9,6 +9,8 @@
#include "common/Timer.h"
#include "Input/InputManager.h"
#include <thread>
#define TR_CONTEXT "FullscreenUI"
template <size_t L>
@@ -215,6 +217,10 @@ namespace FullscreenUI
void CopyTextToClipboard(std::string title, const std::string_view text);
void DrawAboutWindow();
void OpenAboutWindow();
void DrawCoverDownloaderWindow();
void OpenCoverDownloaderWindow();
void CloseCoverDownloaderWindow();
void CoverDownloaderThreadFunc(const std::vector<std::string>& urls);
void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel);
void ApplyLayoutSettings(const SettingsInterface* bsi = nullptr);
@@ -239,6 +245,18 @@ namespace FullscreenUI
inline char s_achievements_login_password[256] = {};
inline Achievements::LoginRequestReason s_achievements_login_reason = Achievements::LoginRequestReason::UserInitiated;
// cover downloader dialog state
inline bool s_cover_downloader_open = false;
inline std::array<char, 4096> s_cover_downloader_urls_buffer = {};
inline bool s_cover_downloader_use_title_filenames = false;
inline bool s_cover_downloader_downloading = false;
inline std::unique_ptr<std::thread> s_cover_downloader_thread;
inline std::mutex s_cover_downloader_mutex;
inline std::string s_cover_downloader_status;
inline bool s_cover_downloader_has_error = false;
inline s32 s_cover_downloader_progress_max = 0;
inline s32 s_cover_downloader_progress_value = 0;
// local copies of the currently-running game
inline std::string s_current_game_title;
inline std::string s_current_game_subtitle;