diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index b991537f60..7f447e7a39 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -372,16 +372,6 @@ void Host::OnAchievementsRefreshed() // noop } -void Host::OnCoverDownloaderOpenRequested() -{ - // noop -} - -void Host::OnCreateMemoryCardOpenRequested() -{ - // noop -} - bool Host::InBatchMode() { return false; diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 13d55c3cd0..b1e6e04701 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -444,8 +444,6 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread) connect(thread, &EmuThread::onCaptureStopped, this, &MainWindow::onCaptureStopped); connect(thread, &EmuThread::onAchievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); connect(thread, &EmuThread::onAchievementsHardcoreModeChanged, this, &MainWindow::onAchievementsHardcoreModeChanged); - connect(thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); - connect(thread, &EmuThread::onCreateMemoryCardOpenRequested, this, &MainWindow::onCreateMemoryCardOpenRequested); connect(m_ui.actionReset, &QAction::triggered, this, &MainWindow::requestReset); connect(m_ui.actionPause, &QAction::toggled, thread, &EmuThread::setVMPaused); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 902c400597..0004a9d4d6 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -1130,16 +1130,6 @@ void Host::OnAchievementsHardcoreModeChanged(bool enabled) emit g_emu_thread->onAchievementsHardcoreModeChanged(enabled); } -void Host::OnCoverDownloaderOpenRequested() -{ - emit g_emu_thread->onCoverDownloaderOpenRequested(); -} - -void Host::OnCreateMemoryCardOpenRequested() -{ - emit g_emu_thread->onCreateMemoryCardOpenRequested(); -} - bool Host::ShouldPreferHostFileSelector() { #ifdef __linux__ diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index 0785f4d811..b72c77d428 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -164,10 +164,6 @@ Q_SIGNALS: /// Called when hardcore mode is enabled or disabled. void onAchievementsHardcoreModeChanged(bool enabled); - /// Big Picture UI requests. - void onCoverDownloaderOpenRequested(); - void onCreateMemoryCardOpenRequested(); - /// Called when video capture starts/stops. void onCaptureStarted(const QString& filename); void onCaptureStopped(); diff --git a/pcsx2/GameList.cpp b/pcsx2/GameList.cpp index cc242dcd6c..1ba18fd76c 100644 --- a/pcsx2/GameList.cpp +++ b/pcsx2/GameList.cpp @@ -1355,6 +1355,12 @@ bool GameList::DownloadCovers(const std::vector& url_templates, boo return false; } + if (!FileSystem::CreateDirectoryPath(EmuFolders::Covers.c_str(), false)) + { + progress->DisplayError(fmt::format("Failed to create covers directory: {}", EmuFolders::Covers).c_str()); + return false; + } + std::vector> download_urls; { std::unique_lock lock(s_mutex); diff --git a/pcsx2/ImGui/FullscreenUI.cpp b/pcsx2/ImGui/FullscreenUI.cpp index 3d3416c127..de603fc866 100644 --- a/pcsx2/ImGui/FullscreenUI.cpp +++ b/pcsx2/ImGui/FullscreenUI.cpp @@ -439,6 +439,10 @@ namespace FullscreenUI static void CopyTextToClipboard(std::string title, const std::string_view text); static void DrawAboutWindow(); static void OpenAboutWindow(); + static void DrawCoverDownloaderWindow(); + static void OpenCoverDownloaderWindow(); + static void CloseCoverDownloaderWindow(); + static void CoverDownloaderThreadFunc(const std::vector& urls); static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel); static void ApplyLayoutSettings(const SettingsInterface* bsi = nullptr); @@ -462,6 +466,18 @@ namespace FullscreenUI static char s_achievements_login_password[256] = {}; static Achievements::LoginRequestReason s_achievements_login_reason = Achievements::LoginRequestReason::UserInitiated; + // cover downloader dialog state + static bool s_cover_downloader_open = false; + static std::array s_cover_downloader_urls_buffer = {}; + static bool s_cover_downloader_use_title_filenames = false; + static bool s_cover_downloader_downloading = false; + static std::unique_ptr s_cover_downloader_thread; + static std::mutex s_cover_downloader_mutex; + static std::string s_cover_downloader_status; + static bool s_cover_downloader_has_error = false; + static s32 s_cover_downloader_progress_max = 0; + static s32 s_cover_downloader_progress_value = 0; + // local copies of the currently-running game static std::string s_current_game_title; static std::string s_current_game_subtitle; @@ -1028,7 +1044,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()); } @@ -1181,6 +1197,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 = {}; @@ -1298,6 +1315,9 @@ void FullscreenUI::Render() if (s_about_window_open) DrawAboutWindow(); + if (s_cover_downloader_open) + DrawCoverDownloaderWindow(); + if (s_achievements_login_open) DrawAchievementsLoginWindow(); @@ -7865,6 +7885,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) { @@ -7896,6 +7920,7 @@ void FullscreenUI::DrawGameListWindow() const bool swapNorthWest = ImGuiManager::IsGamepadNorthWestSwapped(); SetFullscreenFooterText(std::array{ std::make_pair(ICON_PF_DPAD, FSUI_VSTR("Select Game")), + std::make_pair(ICON_PF_SELECT_SHARE, FSUI_VSTR("Cover Downloader")), std::make_pair(ICON_PF_START, FSUI_VSTR("Settings")), std::make_pair(swapNorthWest ? ICON_PF_BUTTON_SQUARE : ICON_PF_BUTTON_TRIANGLE, FSUI_VSTR("Change View")), std::make_pair(swapNorthWest ? ICON_PF_BUTTON_TRIANGLE : ICON_PF_BUTTON_SQUARE, FSUI_VSTR("Launch Options")), @@ -7910,6 +7935,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")), }); @@ -8466,16 +8492,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( @@ -8716,6 +8732,374 @@ void FullscreenUI::DrawAboutWindow() ImGui::PopFont(); } +void FullscreenUI::OpenCoverDownloaderWindow() +{ + s_cover_downloader_open = true; + s_cover_downloader_urls_buffer.fill('\0'); + { + std::lock_guard 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 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 lock(s_cover_downloader_mutex); + s_cover_downloader_use_title_filenames = false; + s_cover_downloader_has_error = false; + } +} + +void FullscreenUI::CoverDownloaderThreadFunc(const std::vector& urls) +{ + class CoverDownloaderProgressCallback : public ProgressCallback + { + public: + CoverDownloaderProgressCallback() = default; + ~CoverDownloaderProgressCallback() = default; + + void PushState() override {} + void PopState() override {} + + bool IsCancelled() const override + { + std::lock_guard 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 lock(s_cover_downloader_mutex); + s_cover_downloader_status = text; + } + + void SetProgressRange(u32 range) override + { + std::lock_guard 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 lock(s_cover_downloader_mutex); + s_cover_downloader_progress_value = value; + } + + void IncrementProgressValue() override + { + std::lock_guard lock(s_cover_downloader_mutex); + s_cover_downloader_progress_value++; + } + + void SetProgressState(ProgressState state) override + { + } + + void DisplayError(const char* message) override + { + std::lock_guard 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 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 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 lock(s_cover_downloader_mutex); + use_title_filenames = s_cover_downloader_use_title_filenames; + } + GameList::DownloadCovers(urls, !use_title_filenames, &callback); + + { + std::lock_guard 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() +{ + ImGui::SetNextWindowSize(LayoutScale(800.0f, 640.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 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_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + + const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar; + + if (ImGui::BeginPopupModal(FSUI_ICONSTR(ICON_FA_DOWNLOAD, "Cover Downloader"), &s_cover_downloader_open, flags)) + { + BeginMenuButtons(); + + 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("In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:")); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(6.0f)); + ImGui::Text("${title}: %s", FSUI_CSTR("Title of the game.")); + ImGui::Text("${filetitle}: %s", FSUI_CSTR("Name component of the game's filename.")); + ImGui::Text("${serial}: %s", FSUI_CSTR("Serial of the game.")); + ImGui::TextWrapped("%s https://www.example-not-a-real-domain.com/covers/${serial}.jpg", FSUI_CSTR("Example:")); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + // URLs input section + { + ImGui::TextUnformatted(FSUI_CSTR("URLs:")); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(4.0f)); + + const float text_box_height = LayoutScale(55.0f); + ImGui::SetNextItemWidth(-1.0f); + ImGuiInputTextFlags text_flags = ImGuiInputTextFlags_AllowTabInput; + bool is_downloading; + { + std::lock_guard lock(s_cover_downloader_mutex); + is_downloading = s_cover_downloader_downloading; + } + if (!is_downloading) + { + ImGui::InputTextMultiline("##urls", s_cover_downloader_urls_buffer.data(), s_cover_downloader_urls_buffer.size(), ImVec2(-1.0f, text_box_height), text_flags); + } + else + { + ImGui::BeginDisabled(); + ImGui::InputTextMultiline("##urls", s_cover_downloader_urls_buffer.data(), s_cover_downloader_urls_buffer.size(), ImVec2(-1.0f, text_box_height), text_flags); + ImGui::EndDisabled(); + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + bool use_title; + { + std::lock_guard lock(s_cover_downloader_mutex); + use_title = s_cover_downloader_use_title_filenames; + } + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::Checkbox(FSUI_CSTR("Use Title File Names"), &use_title)) + { + std::lock_guard lock(s_cover_downloader_mutex); + s_cover_downloader_use_title_filenames = use_title; + } + const bool is_hovering_checkbox = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay); + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + const bool is_hovering_indicator = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay); + if (is_hovering_checkbox || is_hovering_indicator) + { + if (ImGui::BeginTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(FSUI_CSTR( + "By default, the downloaded covers will be saved with the game's serial to ensure covers do not break with GameDB changes " + "and that titles with multiple regions do not conflict. If this is not desired, you can check the \"Use Title File Names\" box above.")); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + // Progress display + { + std::lock_guard lock(s_cover_downloader_mutex); + + if (s_cover_downloader_downloading) + { + if (!s_cover_downloader_status.empty()) + { + ImGui::TextUnformatted(s_cover_downloader_status.c_str()); + } + else + { + ImGui::TextUnformatted(FSUI_CSTR("Downloading covers...")); + } + + // Show progress bar if we have progress info + if (s_cover_downloader_progress_max > 0) + { + const float progress = static_cast(s_cover_downloader_progress_value) / static_cast(s_cover_downloader_progress_max); + const int percent = static_cast(std::round(progress * 100.0f)); + ImGui::ProgressBar(progress, ImVec2(-1.0f, LayoutScale(30.0f)), fmt::format("{}%", percent).c_str()); + } + else + { + // Indeterminate progress bar + 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(4.0f)); + } + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(12.0f)); + + const bool has_urls = (s_cover_downloader_urls_buffer[0] != '\0'); + bool start_enabled; + { + std::lock_guard lock(s_cover_downloader_mutex); + start_enabled = !s_cover_downloader_downloading && has_urls; + } + + { + std::unique_lock lock(s_cover_downloader_mutex); + const bool is_downloading = s_cover_downloader_downloading; + lock.unlock(); + + if (is_downloading) + { + if (ActiveButton(FSUI_ICONSTR(ICON_FA_STOP, "Stop"), false)) + { + { + std::lock_guard lock2(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 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)); + } + + // Start download in a background thread + // Clean up any existing thread before creating a new one + 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 lock(s_cover_downloader_mutex); + if (!s_cover_downloader_downloading) + { + // Reset progress values for new download + 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(CoverDownloaderThreadFunc, urls); + } + } + } + } + + if (ActiveButton(FSUI_ICONSTR(ICON_FA_XMARK, "Close"), false) || WantsToCloseMenu()) + { + ImGui::CloseCurrentPopup(); + s_cover_downloader_open = false; + CloseCoverDownloaderWindow(); + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(4); + ImGui::PopFont(); +} + bool FullscreenUI::OpenAchievementsWindow() { if (!VMManager::HasValidVM() || !Achievements::IsActive()) diff --git a/tests/ctest/core/StubHost.cpp b/tests/ctest/core/StubHost.cpp index bcf741a90f..34858c703f 100644 --- a/tests/ctest/core/StubHost.cpp +++ b/tests/ctest/core/StubHost.cpp @@ -238,14 +238,6 @@ void Host::OnAchievementsHardcoreModeChanged(bool enabled) { } -void Host::OnCoverDownloaderOpenRequested() -{ -} - -void Host::OnCreateMemoryCardOpenRequested() -{ -} - bool Host::LocaleCircleConfirm() { return false;