Просмотр исходного кода

applets/web: Implement the Qt web browser applet frontend

Morph 5 лет назад
Родитель
Сommit
93cb783853
4 измененных файлов с 600 добавлено и 5 удалено
  1. 331 2
      src/yuzu/applets/web_browser.cpp
  2. 154 1
      src/yuzu/applets/web_browser.h
  3. 108 2
      src/yuzu/main.cpp
  4. 7 0
      src/yuzu/main.h

+ 331 - 2
src/yuzu/applets/web_browser.cpp

@@ -2,10 +2,339 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include "core/hle/lock.h"
+#ifdef YUZU_USE_QT_WEB_ENGINE
+#include <QKeyEvent>
+
+#include <QWebEngineProfile>
+#include <QWebEngineScript>
+#include <QWebEngineScriptCollection>
+#include <QWebEngineSettings>
+#include <QWebEngineUrlScheme>
+#endif
+
+#include "common/file_util.h"
+#include "core/core.h"
+#include "core/frontend/input_interpreter.h"
 #include "yuzu/applets/web_browser.h"
+#include "yuzu/applets/web_browser_scripts.h"
 #include "yuzu/main.h"
+#include "yuzu/util/url_request_interceptor.h"
+
+#ifdef YUZU_USE_QT_WEB_ENGINE
+
+namespace {
+
+constexpr int HIDButtonToKey(HIDButton button) {
+    switch (button) {
+    case HIDButton::DLeft:
+    case HIDButton::LStickLeft:
+        return Qt::Key_Left;
+    case HIDButton::DUp:
+    case HIDButton::LStickUp:
+        return Qt::Key_Up;
+    case HIDButton::DRight:
+    case HIDButton::LStickRight:
+        return Qt::Key_Right;
+    case HIDButton::DDown:
+    case HIDButton::LStickDown:
+        return Qt::Key_Down;
+    default:
+        return 0;
+    }
+}
+
+} // Anonymous namespace
+
+QtNXWebEngineView::QtNXWebEngineView(QWidget* parent, Core::System& system)
+    : QWebEngineView(parent), url_interceptor(std::make_unique<UrlRequestInterceptor>()),
+      input_interpreter(std::make_unique<InputInterpreter>(system)) {
+    QWebEngineScript nx_font_css;
+    QWebEngineScript load_nx_font;
+    QWebEngineScript gamepad;
+    QWebEngineScript window_nx;
+
+    const QString fonts_dir = QString::fromStdString(Common::FS::SanitizePath(
+        fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir))));
+
+    nx_font_css.setName(QStringLiteral("nx_font_css.js"));
+    load_nx_font.setName(QStringLiteral("load_nx_font.js"));
+    gamepad.setName(QStringLiteral("gamepad_script.js"));
+    window_nx.setName(QStringLiteral("window_nx_script.js"));
+
+    nx_font_css.setSourceCode(
+        QString::fromStdString(NX_FONT_CSS)
+            .arg(fonts_dir + QStringLiteral("/FontStandard.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontChineseSimplified.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontExtendedChineseSimplified.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontChineseTraditional.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontKorean.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontNintendoExtended.ttf"))
+            .arg(fonts_dir + QStringLiteral("/FontNintendoExtended2.ttf")));
+    load_nx_font.setSourceCode(QString::fromStdString(LOAD_NX_FONT));
+    gamepad.setSourceCode(QString::fromStdString(GAMEPAD_SCRIPT));
+    window_nx.setSourceCode(QString::fromStdString(WINDOW_NX_SCRIPT));
+
+    nx_font_css.setInjectionPoint(QWebEngineScript::DocumentReady);
+    load_nx_font.setInjectionPoint(QWebEngineScript::Deferred);
+    gamepad.setInjectionPoint(QWebEngineScript::DocumentCreation);
+    window_nx.setInjectionPoint(QWebEngineScript::DocumentCreation);
+
+    nx_font_css.setWorldId(QWebEngineScript::MainWorld);
+    load_nx_font.setWorldId(QWebEngineScript::MainWorld);
+    gamepad.setWorldId(QWebEngineScript::MainWorld);
+    window_nx.setWorldId(QWebEngineScript::MainWorld);
+
+    nx_font_css.setRunsOnSubFrames(true);
+    load_nx_font.setRunsOnSubFrames(true);
+    gamepad.setRunsOnSubFrames(true);
+    window_nx.setRunsOnSubFrames(true);
+
+    auto* default_profile = QWebEngineProfile::defaultProfile();
+
+    default_profile->scripts()->insert(nx_font_css);
+    default_profile->scripts()->insert(load_nx_font);
+    default_profile->scripts()->insert(gamepad);
+    default_profile->scripts()->insert(window_nx);
+
+    default_profile->setRequestInterceptor(url_interceptor.get());
+
+    auto* global_settings = QWebEngineSettings::globalSettings();
+
+    global_settings->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
+    global_settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
+    global_settings->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true);
+    global_settings->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true);
+    global_settings->setAttribute(QWebEngineSettings::AllowWindowActivationFromJavaScript, true);
+    global_settings->setAttribute(QWebEngineSettings::ShowScrollBars, false);
+
+    connect(
+        url_interceptor.get(), &UrlRequestInterceptor::FrameChanged, url_interceptor.get(),
+        [this] {
+            std::this_thread::sleep_for(std::chrono::milliseconds(50));
+            page()->runJavaScript(QString::fromStdString(LOAD_NX_FONT));
+        },
+        Qt::QueuedConnection);
+
+    connect(
+        page(), &QWebEnginePage::windowCloseRequested, page(),
+        [this] {
+            if (page()->url() == url_interceptor->GetRequestedURL()) {
+                SetFinished(true);
+                SetExitReason(WebExitReason::WindowClosed);
+            }
+        },
+        Qt::QueuedConnection);
+}
+
+QtNXWebEngineView::~QtNXWebEngineView() {
+    SetFinished(true);
+    StopInputThread();
+}
+
+void QtNXWebEngineView::LoadLocalWebPage(std::string_view main_url,
+                                         std::string_view additional_args) {
+    SetUserAgent(UserAgent::WebApplet);
+    SetFinished(false);
+    SetExitReason(WebExitReason::EndButtonPressed);
+    SetLastURL("http://localhost/");
+    StartInputThread();
+
+    load(QUrl(QUrl::fromLocalFile(QString::fromStdString(std::string(main_url))).toString() +
+              QString::fromStdString(std::string(additional_args))));
+}
+
+void QtNXWebEngineView::SetUserAgent(UserAgent user_agent) {
+    const QString user_agent_str = [user_agent] {
+        switch (user_agent) {
+        case UserAgent::WebApplet:
+        default:
+            return QStringLiteral("WebApplet");
+        case UserAgent::ShopN:
+            return QStringLiteral("ShopN");
+        case UserAgent::LoginApplet:
+            return QStringLiteral("LoginApplet");
+        case UserAgent::ShareApplet:
+            return QStringLiteral("ShareApplet");
+        case UserAgent::LobbyApplet:
+            return QStringLiteral("LobbyApplet");
+        case UserAgent::WifiWebAuthApplet:
+            return QStringLiteral("WifiWebAuthApplet");
+        }
+    }();
+
+    QWebEngineProfile::defaultProfile()->setHttpUserAgent(
+        QStringLiteral("Mozilla/5.0 (Nintendo Switch; %1) AppleWebKit/606.4 "
+                       "(KHTML, like Gecko) NF/6.0.1.15.4 NintendoBrowser/5.1.0.20389")
+            .arg(user_agent_str));
+}
+
+bool QtNXWebEngineView::IsFinished() const {
+    return finished;
+}
+
+void QtNXWebEngineView::SetFinished(bool finished_) {
+    finished = finished_;
+}
+
+WebExitReason QtNXWebEngineView::GetExitReason() const {
+    return exit_reason;
+}
+
+void QtNXWebEngineView::SetExitReason(WebExitReason exit_reason_) {
+    exit_reason = exit_reason_;
+}
 
-QtWebBrowser::QtWebBrowser(GMainWindow& main_window) {}
+const std::string& QtNXWebEngineView::GetLastURL() const {
+    return last_url;
+}
+
+void QtNXWebEngineView::SetLastURL(std::string last_url_) {
+    last_url = std::move(last_url_);
+}
+
+QString QtNXWebEngineView::GetCurrentURL() const {
+    return url_interceptor->GetRequestedURL().toString();
+}
+
+void QtNXWebEngineView::hide() {
+    SetFinished(true);
+    StopInputThread();
+
+    QWidget::hide();
+}
+
+template <HIDButton... T>
+void QtNXWebEngineView::HandleWindowFooterButtonPressedOnce() {
+    const auto f = [this](HIDButton button) {
+        if (input_interpreter->IsButtonPressedOnce(button)) {
+            page()->runJavaScript(
+                QStringLiteral("yuzu_key_callbacks[%1] == null;").arg(static_cast<u8>(button)),
+                [&](const QVariant& variant) {
+                    if (variant.toBool()) {
+                        switch (button) {
+                        case HIDButton::A:
+                            SendMultipleKeyPressEvents<Qt::Key_A, Qt::Key_Space, Qt::Key_Return>();
+                            break;
+                        case HIDButton::B:
+                            SendKeyPressEvent(Qt::Key_B);
+                            break;
+                        case HIDButton::X:
+                            SendKeyPressEvent(Qt::Key_X);
+                            break;
+                        case HIDButton::Y:
+                            SendKeyPressEvent(Qt::Key_Y);
+                            break;
+                        default:
+                            break;
+                        }
+                    }
+                });
+
+            page()->runJavaScript(
+                QStringLiteral("if (yuzu_key_callbacks[%1] != null) { yuzu_key_callbacks[%1](); }")
+                    .arg(static_cast<u8>(button)));
+        }
+    };
+
+    (f(T), ...);
+}
+
+template <HIDButton... T>
+void QtNXWebEngineView::HandleWindowKeyButtonPressedOnce() {
+    const auto f = [this](HIDButton button) {
+        if (input_interpreter->IsButtonPressedOnce(button)) {
+            SendKeyPressEvent(HIDButtonToKey(button));
+        }
+    };
+
+    (f(T), ...);
+}
+
+template <HIDButton... T>
+void QtNXWebEngineView::HandleWindowKeyButtonHold() {
+    const auto f = [this](HIDButton button) {
+        if (input_interpreter->IsButtonHeld(button)) {
+            SendKeyPressEvent(HIDButtonToKey(button));
+        }
+    };
+
+    (f(T), ...);
+}
+
+void QtNXWebEngineView::SendKeyPressEvent(int key) {
+    if (key == 0) {
+        return;
+    }
+
+    QCoreApplication::postEvent(focusProxy(),
+                                new QKeyEvent(QKeyEvent::KeyPress, key, Qt::NoModifier));
+    QCoreApplication::postEvent(focusProxy(),
+                                new QKeyEvent(QKeyEvent::KeyRelease, key, Qt::NoModifier));
+}
+
+void QtNXWebEngineView::StartInputThread() {
+    if (input_thread_running) {
+        return;
+    }
+
+    input_thread_running = true;
+    input_thread = std::thread(&QtNXWebEngineView::InputThread, this);
+}
+
+void QtNXWebEngineView::StopInputThread() {
+    input_thread_running = false;
+    if (input_thread.joinable()) {
+        input_thread.join();
+    }
+}
+
+void QtNXWebEngineView::InputThread() {
+    // Wait for 1 second before allowing any inputs to be processed.
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+
+    while (input_thread_running) {
+        input_interpreter->PollInput();
+
+        HandleWindowFooterButtonPressedOnce<HIDButton::A, HIDButton::B, HIDButton::X, HIDButton::Y,
+                                            HIDButton::L, HIDButton::R>();
+
+        HandleWindowKeyButtonPressedOnce<HIDButton::DLeft, HIDButton::DUp, HIDButton::DRight,
+                                         HIDButton::DDown, HIDButton::LStickLeft,
+                                         HIDButton::LStickUp, HIDButton::LStickRight,
+                                         HIDButton::LStickDown>();
+
+        HandleWindowKeyButtonHold<HIDButton::DLeft, HIDButton::DUp, HIDButton::DRight,
+                                  HIDButton::DDown, HIDButton::LStickLeft, HIDButton::LStickUp,
+                                  HIDButton::LStickRight, HIDButton::LStickDown>();
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(50));
+    }
+}
+
+#endif
+
+QtWebBrowser::QtWebBrowser(GMainWindow& main_window) {
+    connect(this, &QtWebBrowser::MainWindowOpenLocalWebPage, &main_window,
+            &GMainWindow::WebBrowserOpenLocalWebPage, Qt::QueuedConnection);
+    connect(&main_window, &GMainWindow::WebBrowserClosed, this,
+            &QtWebBrowser::MainWindowWebBrowserClosed, Qt::QueuedConnection);
+}
 
 QtWebBrowser::~QtWebBrowser() = default;
+
+void QtWebBrowser::OpenLocalWebPage(
+    std::string_view local_url, std::function<void(WebExitReason, std::string)> callback) const {
+    this->callback = std::move(callback);
+
+    const auto index = local_url.find('?');
+
+    if (index == std::string::npos) {
+        emit MainWindowOpenLocalWebPage(local_url, "");
+    } else {
+        emit MainWindowOpenLocalWebPage(local_url.substr(0, index), local_url.substr(index));
+    }
+}
+
+void QtWebBrowser::MainWindowWebBrowserClosed(WebExitReason exit_reason, std::string last_url) {
+    callback(exit_reason, last_url);
+}

+ 154 - 1
src/yuzu/applets/web_browser.h

@@ -4,6 +4,10 @@
 
 #pragma once
 
+#include <atomic>
+#include <memory>
+#include <thread>
+
 #include <QObject>
 
 #ifdef YUZU_USE_QT_WEB_ENGINE
@@ -12,12 +16,161 @@
 
 #include "core/frontend/applets/web_browser.h"
 
+enum class HIDButton : u8;
+
+class InputInterpreter;
 class GMainWindow;
+class UrlRequestInterceptor;
+
+namespace Core {
+class System;
+}
+
+#ifdef YUZU_USE_QT_WEB_ENGINE
+
+enum class UserAgent {
+    WebApplet,
+    ShopN,
+    LoginApplet,
+    ShareApplet,
+    LobbyApplet,
+    WifiWebAuthApplet,
+};
+
+class QtNXWebEngineView : public QWebEngineView {
+    Q_OBJECT
+
+public:
+    explicit QtNXWebEngineView(QWidget* parent, Core::System& system);
+    ~QtNXWebEngineView() override;
+
+    /**
+     * Loads a HTML document that exists locally. Cannot be used to load external websites.
+     *
+     * @param main_url The url to the file.
+     * @param additional_args Additional arguments appended to the main url.
+     */
+    void LoadLocalWebPage(std::string_view main_url, std::string_view additional_args);
+
+    /**
+     * Sets the background color of the web page.
+     *
+     * @param color The color to set.
+     */
+    void SetBackgroundColor(QColor color);
+
+    /**
+     * Sets the user agent of the web browser.
+     *
+     * @param user_agent The user agent enum.
+     */
+    void SetUserAgent(UserAgent user_agent);
+
+    [[nodiscard]] bool IsFinished() const;
+    void SetFinished(bool finished_);
+
+    [[nodiscard]] WebExitReason GetExitReason() const;
+    void SetExitReason(WebExitReason exit_reason_);
+
+    [[nodiscard]] const std::string& GetLastURL() const;
+    void SetLastURL(std::string last_url_);
+
+    /**
+     * This gets the current URL that has been requested by the webpage.
+     * This only applies to the main frame. Sub frames and other resources are ignored.
+     *
+     * @return Currently requested URL
+     */
+    [[nodiscard]] QString GetCurrentURL() const;
+
+public slots:
+    void hide();
+
+private:
+    /**
+     * Handles button presses to execute functions assigned in yuzu_key_callbacks.
+     * yuzu_key_callbacks contains specialized functions for the buttons in the window footer
+     * that can be overriden by games to achieve desired functionality.
+     *
+     * @tparam HIDButton The list of buttons contained in yuzu_key_callbacks
+     */
+    template <HIDButton... T>
+    void HandleWindowFooterButtonPressedOnce();
+
+    /**
+     * Handles button presses and converts them into keyboard input.
+     * This should only be used to convert D-Pad or Analog Stick input into arrow keys.
+     *
+     * @tparam HIDButton The list of buttons that can be converted into keyboard input.
+     */
+    template <HIDButton... T>
+    void HandleWindowKeyButtonPressedOnce();
+
+    /**
+     * Handles button holds and converts them into keyboard input.
+     * This should only be used to convert D-Pad or Analog Stick input into arrow keys.
+     *
+     * @tparam HIDButton The list of buttons that can be converted into keyboard input.
+     */
+    template <HIDButton... T>
+    void HandleWindowKeyButtonHold();
+
+    /**
+     * Sends a key press event to QWebEngineView.
+     *
+     * @param key Qt key code.
+     */
+    void SendKeyPressEvent(int key);
+
+    /**
+     * Sends multiple key press events to QWebEngineView.
+     *
+     * @tparam int Qt key code.
+     */
+    template <int... T>
+    void SendMultipleKeyPressEvents() {
+        (SendKeyPressEvent(T), ...);
+    }
+
+    void StartInputThread();
+    void StopInputThread();
+
+    /// The thread where input is being polled and processed.
+    void InputThread();
+
+    std::unique_ptr<UrlRequestInterceptor> url_interceptor;
+
+    std::unique_ptr<InputInterpreter> input_interpreter;
+
+    std::thread input_thread;
+
+    std::atomic<bool> input_thread_running{};
+
+    std::atomic<bool> finished{};
+
+    WebExitReason exit_reason{WebExitReason::EndButtonPressed};
+
+    std::string last_url{"http://localhost/"};
+};
+
+#endif
 
 class QtWebBrowser final : public QObject, public Core::Frontend::WebBrowserApplet {
     Q_OBJECT
 
 public:
-    explicit QtWebBrowser(GMainWindow& main_window);
+    explicit QtWebBrowser(GMainWindow& parent);
     ~QtWebBrowser() override;
+
+    void OpenLocalWebPage(std::string_view local_url,
+                          std::function<void(WebExitReason, std::string)> callback) const override;
+
+signals:
+    void MainWindowOpenLocalWebPage(std::string_view main_url,
+                                    std::string_view additional_args) const;
+
+private:
+    void MainWindowWebBrowserClosed(WebExitReason exit_reason, std::string last_url);
+
+    mutable std::function<void(WebExitReason, std::string)> callback;
 };

+ 108 - 2
src/yuzu/main.cpp

@@ -28,8 +28,6 @@
 #include "core/hle/service/am/applet_ae.h"
 #include "core/hle/service/am/applet_oe.h"
 #include "core/hle/service/am/applets/applets.h"
-#include "core/hle/service/hid/controllers/npad.h"
-#include "core/hle/service/hid/hid.h"
 
 // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows
 // defines.
@@ -182,6 +180,30 @@ static void InitializeLogging() {
 #endif
 }
 
+static void RemoveCachedContents() {
+    const auto offline_fonts = Common::FS::SanitizePath(
+        fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
+        Common::FS::DirectorySeparator::PlatformDefault);
+
+    const auto offline_manual = Common::FS::SanitizePath(
+        fmt::format("{}/offline_web_applet_manual",
+                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
+        Common::FS::DirectorySeparator::PlatformDefault);
+    const auto offline_legal_information = Common::FS::SanitizePath(
+        fmt::format("{}/offline_web_applet_legal_information",
+                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
+        Common::FS::DirectorySeparator::PlatformDefault);
+    const auto offline_system_data = Common::FS::SanitizePath(
+        fmt::format("{}/offline_web_applet_system_data",
+                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
+        Common::FS::DirectorySeparator::PlatformDefault);
+
+    Common::FS::DeleteDirRecursively(offline_fonts);
+    Common::FS::DeleteDirRecursively(offline_manual);
+    Common::FS::DeleteDirRecursively(offline_legal_information);
+    Common::FS::DeleteDirRecursively(offline_system_data);
+}
+
 GMainWindow::GMainWindow()
     : input_subsystem{std::make_shared<InputCommon::InputSubsystem>()},
       config{std::make_unique<Config>()}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()},
@@ -250,6 +272,9 @@ GMainWindow::GMainWindow()
         FileSys::ContentProviderUnionSlot::FrontendManual, provider.get());
     Core::System::GetInstance().GetFileSystemController().CreateFactories(*vfs);
 
+    // Remove cached contents generated during the previous session
+    RemoveCachedContents();
+
     // Gen keys if necessary
     OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning);
 
@@ -341,6 +366,86 @@ void GMainWindow::SoftwareKeyboardInvokeCheckDialog(std::u16string error_message
     emit SoftwareKeyboardFinishedCheckDialog();
 }
 
+void GMainWindow::WebBrowserOpenLocalWebPage(std::string_view main_url,
+                                             std::string_view additional_args) {
+#ifdef YUZU_USE_QT_WEB_ENGINE
+
+    QtNXWebEngineView web_browser_view(this, Core::System::GetInstance());
+
+    web_browser_view.LoadLocalWebPage(main_url, additional_args);
+
+    ui.action_Pause->setEnabled(false);
+    ui.action_Restart->setEnabled(false);
+    ui.action_Stop->setEnabled(false);
+
+    if (render_window->IsLoadingComplete()) {
+        render_window->hide();
+    }
+
+    const auto& layout = render_window->GetFramebufferLayout();
+    web_browser_view.resize(layout.screen.GetWidth(), layout.screen.GetHeight());
+    web_browser_view.move(layout.screen.left, layout.screen.top + menuBar()->height());
+    web_browser_view.setZoomFactor(static_cast<qreal>(layout.screen.GetWidth()) /
+                                   static_cast<qreal>(Layout::ScreenUndocked::Width));
+
+    web_browser_view.setFocus();
+    web_browser_view.show();
+
+    bool exit_check = false;
+
+    while (!web_browser_view.IsFinished()) {
+        QCoreApplication::processEvents();
+
+        if (!exit_check) {
+            web_browser_view.page()->runJavaScript(
+                QStringLiteral("end_applet;"), [&](const QVariant& variant) {
+                    exit_check = false;
+                    if (variant.toBool()) {
+                        web_browser_view.SetFinished(true);
+                        web_browser_view.SetExitReason(WebExitReason::EndButtonPressed);
+                    }
+                });
+
+            exit_check = true;
+        }
+
+        if (web_browser_view.GetCurrentURL().contains(QStringLiteral("localhost"))) {
+            if (!web_browser_view.IsFinished()) {
+                web_browser_view.SetFinished(true);
+                web_browser_view.SetExitReason(WebExitReason::CallbackURL);
+            }
+
+            web_browser_view.SetLastURL(web_browser_view.GetCurrentURL().toStdString());
+        }
+
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+
+    const auto exit_reason = web_browser_view.GetExitReason();
+    const auto last_url = web_browser_view.GetLastURL();
+
+    web_browser_view.hide();
+
+    render_window->setFocus();
+
+    if (render_window->IsLoadingComplete()) {
+        render_window->show();
+    }
+
+    ui.action_Pause->setEnabled(true);
+    ui.action_Restart->setEnabled(true);
+    ui.action_Stop->setEnabled(true);
+
+    emit WebBrowserClosed(exit_reason, last_url);
+
+#else
+
+    // Utilize the same fallback as the default web browser applet.
+    emit WebBrowserClosed(WebExitReason::WindowClosed, "http://localhost");
+
+#endif
+}
+
 void GMainWindow::InitializeWidgets() {
 #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
     ui.action_Report_Compatibility->setVisible(true);
@@ -1948,6 +2053,7 @@ void GMainWindow::OnStartGame() {
     qRegisterMetaType<std::string>("std::string");
     qRegisterMetaType<std::optional<std::u16string>>("std::optional<std::u16string>");
     qRegisterMetaType<std::string_view>("std::string_view");
+    qRegisterMetaType<Service::AM::Applets::WebExitReason>("Service::AM::Applets::WebExitReason");
 
     connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError);
 

+ 7 - 0
src/yuzu/main.h

@@ -55,6 +55,10 @@ namespace InputCommon {
 class InputSubsystem;
 }
 
+namespace Service::AM::Applets {
+enum class WebExitReason : u32;
+}
+
 enum class EmulatedDirectoryTarget {
     NAND,
     SDMC,
@@ -126,6 +130,8 @@ signals:
     void SoftwareKeyboardFinishedText(std::optional<std::u16string> text);
     void SoftwareKeyboardFinishedCheckDialog();
 
+    void WebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, std::string last_url);
+
 public slots:
     void OnLoadComplete();
     void OnExecuteProgram(std::size_t program_index);
@@ -135,6 +141,7 @@ public slots:
     void ProfileSelectorSelectProfile();
     void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters);
     void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message);
+    void WebBrowserOpenLocalWebPage(std::string_view main_url, std::string_view additional_args);
     void OnAppFocusStateChanged(Qt::ApplicationState state);
 
 private: