From 223b04f0873e2a5a2931472575261307aa908e7d Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 28 Mar 2025 13:00:05 -0400 Subject: [PATCH] LibWebView: Add autoplay settings to about:settings The idea with the UI here is that it will serve as a generic component for all site settings, such as autoplay, notifications, etc. When the site settings dialog is opened, it is filled with the requested setting data, and messages sent to the browser process are based on the setting. This patch only implements the UI and persisted settings. It does not apply autoplay changes to the WebContent process. --- Base/res/ladybird/about-pages/settings.html | 341 +++++++++++++++++++- Base/res/ladybird/ladybird.css | 49 ++- Libraries/LibWebView/Settings.cpp | 89 +++++ Libraries/LibWebView/Settings.h | 16 + Libraries/LibWebView/WebUI/SettingsUI.cpp | 124 +++++++ Libraries/LibWebView/WebUI/SettingsUI.h | 5 + 6 files changed, 614 insertions(+), 10 deletions(-) diff --git a/Base/res/ladybird/about-pages/settings.html b/Base/res/ladybird/about-pages/settings.html index 5e981d14a4e..fb871caf27a 100644 --- a/Base/res/ladybird/about-pages/settings.html +++ b/Base/res/ladybird/about-pages/settings.html @@ -80,6 +80,18 @@ margin-bottom: 0; } + .permission-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + + .forcibly-enabled { + font-size: 14px; + opacity: 0.6; + } + label { display: block; margin-bottom: 8px; @@ -87,6 +99,7 @@ font-size: 14px; } + input[type="text"], input[type="url"], select { background-color: var(--input-background-color); @@ -95,14 +108,145 @@ border: 1px solid var(--border-color); } + input[type="text"].success, input[type="url"].success { border: 1px solid green; } + input[type="text"].error, input[type="url"].error { border: 1px solid red; } + dialog { + background-color: var(--card-background-color); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); + + width: 90%; + max-width: 500px; + + margin: auto; + padding: 0; + + border: none; + border-radius: 8px; + outline: none; + } + + dialog::backdrop { + /* FIXME: This doesn't work in Ladybird. */ + background-color: rgba(0, 0, 0, 0.5); + } + + dialog .dialog-header { + background-color: var(--card-header-background-color); + + display: flex; + justify-content: space-between; + align-items: center; + + padding: 15px 20px; + } + + dialog .dialog-body { + height: 450px; + + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + + display: flex; + flex-direction: column; + + padding: 15px 20px; + } + + dialog .dialog-body label { + font-size: 16px; + } + + dialog .dialog-body hr { + height: 1px; + background-color: var(--border-color); + border: none; + + margin: 8px 4px 10px 4px; + } + + dialog .dialog-footer { + display: flex; + justify-content: flex-end; + + padding: 15px 20px; + gap: 10px; + } + + dialog .dialog-title { + font-size: 18px; + font-weight: 500; + } + + dialog .dialog-close { + background: none; + border: none; + outline: none; + + padding: 0; + + font-size: 22px; + opacity: 0.8; + + cursor: pointer; + } + + dialog .dialog-close:hover { + opacity: 1; + } + + dialog .dialog-site-list { + outline: 1px solid var(--border-color); + border-radius: 4px; + + margin: 10px 0; + + font-size: 14px; + + overflow-y: auto; + flex-grow: 1; + } + + dialog .dialog-site-item { + padding: 10px; + + display: flex; + justify-content: space-between; + align-items: center; + } + + dialog .dialog-site-item:hover { + background-color: var(--card-header-background-color); + } + + dialog .dialog-site-item-placeholder { + padding: 10px; + opacity: 0.6; + } + + dialog .dialog-site-item .filter { + flex-grow: 1; + } + + dialog .dialog-add-site { + display: flex; + justify-content: flex-end; + + margin-top: auto; + gap: 10px; + } + + dialog .dialog-add-site input { + flex-grow: 1; + } + .button-container { display: flex; justify-content: flex-end; @@ -146,7 +290,7 @@ - + + + diff --git a/Base/res/ladybird/ladybird.css b/Base/res/ladybird/ladybird.css index 8beb7f7e708..d8a4a1b96fd 100644 --- a/Base/res/ladybird/ladybird.css +++ b/Base/res/ladybird/ladybird.css @@ -43,6 +43,9 @@ :root { --text-color: #2f3640; --toggle-background-color: #cccccc; + --secondary-button-color: #dddddd; + --secondary-button-hover: #e1e1e1; + --secondary-button-active: #e9e9e9; } } @@ -50,26 +53,35 @@ :root { --text-color: #e0e0e0; --toggle-background-color: #555555; + --secondary-button-color: #555555; + --secondary-button-hover: #505050; + --secondary-button-active: #444444; } } * { color-scheme: light dark; font-family: sans-serif; + + margin: 0; + padding: 0; } body { color: var(--text-color); } +.hidden { + display: none; + opacity: 0; +} + /** * Generic form controls. */ -button.primary-button { - color: white; - background-color: var(--violet-100); - +button.primary-button, +button.secondary-button { font-size: 14px; border: none; @@ -79,14 +91,39 @@ button.primary-button { cursor: pointer; } +button.primary-button:hover, +button.secondary-button:hover { + filter: unset; +} + +button.primary-button:active, +button.secondary-button:active { + outline: none; +} + +button.primary-button { + color: white; + background-color: var(--violet-100); +} + button.primary-button:hover { background-color: var(--violet-500); - filter: unset; } button.primary-button:active { background-color: var(--violet-900); - outline: none; +} + +button.secondary-button { + background-color: var(--secondary-button-color); +} + +button.secondary-button:hover { + background-color: var(--secondary-button-hover); +} + +button.secondary-button:active { + background-color: var(--secondary-button-active); } input[type="search"], diff --git a/Libraries/LibWebView/Settings.cpp b/Libraries/LibWebView/Settings.cpp index ce43406f2a7..579a56be45b 100644 --- a/Libraries/LibWebView/Settings.cpp +++ b/Libraries/LibWebView/Settings.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -19,9 +20,15 @@ namespace WebView { static constexpr auto new_tab_page_url_key = "newTabPageURL"sv; + static constexpr auto search_engine_key = "searchEngine"sv; static constexpr auto search_engine_name_key = "name"sv; +static constexpr auto site_setting_enabled_globally_key = "enabledGlobally"sv; +static constexpr auto site_setting_site_filters_key = "siteFilters"sv; + +static constexpr auto autoplay_key = "autoplay"sv; + static ErrorOr read_settings_file(StringView settings_path) { auto settings_file = Core::File::open(settings_path, Core::File::OpenMode::Read); @@ -74,6 +81,26 @@ Settings Settings::create(Badge) settings.m_search_engine = find_search_engine_by_name(*search_engine_name); } + auto load_site_setting = [&](SiteSetting& site_setting, StringView key) { + auto saved_settings = settings_json.value().get_object(key); + if (!saved_settings.has_value()) + return; + + if (auto enabled_globally = saved_settings->get_bool(site_setting_enabled_globally_key); enabled_globally.has_value()) + site_setting.enabled_globally = *enabled_globally; + + if (auto site_filters = saved_settings->get_array(site_setting_site_filters_key); site_filters.has_value()) { + site_setting.site_filters.clear(); + + site_filters->for_each([&](auto const& site_filter) { + if (site_filter.is_string()) + site_setting.site_filters.set(site_filter.as_string()); + }); + } + }; + + load_site_setting(settings.m_autoplay, autoplay_key); + return settings; } @@ -95,6 +122,22 @@ JsonValue Settings::serialize_json() const settings.set(search_engine_key, move(search_engine)); } + auto save_site_setting = [&](SiteSetting const& site_setting, StringView key) { + JsonArray site_filters; + site_filters.ensure_capacity(site_setting.site_filters.size()); + + for (auto const& site_filter : site_setting.site_filters) + site_filters.must_append(site_filter); + + JsonObject setting; + setting.set("enabledGlobally"sv, site_setting.enabled_globally); + setting.set("siteFilters"sv, move(site_filters)); + + settings.set(key, move(setting)); + }; + + save_site_setting(m_autoplay, autoplay_key); + return settings; } @@ -102,6 +145,7 @@ void Settings::restore_defaults() { m_new_tab_page_url = URL::about_newtab(); m_search_engine.clear(); + m_autoplay = SiteSetting {}; persist_settings(); @@ -131,6 +175,46 @@ void Settings::set_search_engine(Optional search_engine_name) observer.search_engine_changed(); } +void Settings::set_autoplay_enabled_globally(bool enabled_globally) +{ + m_autoplay.enabled_globally = enabled_globally; + persist_settings(); + + for (auto& observer : m_observers) + observer.autoplay_settings_changed(); +} + +void Settings::add_autoplay_site_filter(String const& site_filter) +{ + auto trimmed_site_filter = MUST(site_filter.trim_whitespace()); + if (trimmed_site_filter.is_empty()) + return; + + m_autoplay.site_filters.set(move(trimmed_site_filter)); + persist_settings(); + + for (auto& observer : m_observers) + observer.autoplay_settings_changed(); +} + +void Settings::remove_autoplay_site_filter(String const& site_filter) +{ + m_autoplay.site_filters.remove(site_filter); + persist_settings(); + + for (auto& observer : m_observers) + observer.autoplay_settings_changed(); +} + +void Settings::remove_all_autoplay_site_filters() +{ + m_autoplay.site_filters.clear(); + persist_settings(); + + for (auto& observer : m_observers) + observer.autoplay_settings_changed(); +} + void Settings::persist_settings() { auto settings = serialize_json(); @@ -162,4 +246,9 @@ SettingsObserver::~SettingsObserver() Settings::remove_observer({}, *this); } +SiteSetting::SiteSetting() +{ + site_filters.set("file://"_string); +} + } diff --git a/Libraries/LibWebView/Settings.h b/Libraries/LibWebView/Settings.h index c05b1dcb689..79e1ec20457 100644 --- a/Libraries/LibWebView/Settings.h +++ b/Libraries/LibWebView/Settings.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -15,6 +16,13 @@ namespace WebView { +struct SiteSetting { + SiteSetting(); + + bool enabled_globally { false }; + OrderedHashTable site_filters; +}; + class SettingsObserver { public: SettingsObserver(); @@ -22,6 +30,7 @@ public: virtual void new_tab_page_url_changed() { } virtual void search_engine_changed() { } + virtual void autoplay_settings_changed() { } }; class Settings { @@ -38,6 +47,12 @@ public: Optional const& search_engine() const { return m_search_engine; } void set_search_engine(Optional search_engine_name); + SiteSetting const& autoplay_settings() const { return m_autoplay; } + void set_autoplay_enabled_globally(bool); + void add_autoplay_site_filter(String const&); + void remove_autoplay_site_filter(String const&); + void remove_all_autoplay_site_filters(); + static void add_observer(Badge, SettingsObserver&); static void remove_observer(Badge, SettingsObserver&); @@ -50,6 +65,7 @@ private: URL::URL m_new_tab_page_url; Optional m_search_engine; + SiteSetting m_autoplay; Vector m_observers; }; diff --git a/Libraries/LibWebView/WebUI/SettingsUI.cpp b/Libraries/LibWebView/WebUI/SettingsUI.cpp index c49cf717b08..828603de458 100644 --- a/Libraries/LibWebView/WebUI/SettingsUI.cpp +++ b/Libraries/LibWebView/WebUI/SettingsUI.cpp @@ -29,6 +29,21 @@ void SettingsUI::register_interfaces() register_interface("setSearchEngine"sv, [this](auto const& data) { set_search_engine(data); }); + register_interface("loadForciblyEnabledSiteSettings"sv, [this](auto const&) { + load_forcibly_enabled_site_settings(); + }); + register_interface("setSiteSettingEnabledGlobally"sv, [this](auto const& data) { + set_site_setting_enabled_globally(data); + }); + register_interface("addSiteSettingFilter"sv, [this](auto const& data) { + add_site_setting_filter(data); + }); + register_interface("removeSiteSettingFilter"sv, [this](auto const& data) { + remove_site_setting_filter(data); + }); + register_interface("removeAllSiteSettingFilters"sv, [this](auto const& data) { + remove_all_site_setting_filters(data); + }); } void SettingsUI::load_current_settings() @@ -72,4 +87,113 @@ void SettingsUI::set_search_engine(JsonValue const& search_engine) WebView::Application::settings().set_search_engine(search_engine.as_string()); } +enum class SiteSettingType { + Autoplay, +}; + +static constexpr StringView site_setting_type_to_string(SiteSettingType setting) +{ + switch (setting) { + case SiteSettingType::Autoplay: + return "autoplay"sv; + } + VERIFY_NOT_REACHED(); +} + +static Optional site_setting_type(JsonValue const& settings) +{ + if (!settings.is_object()) + return {}; + + auto setting_type = settings.as_object().get_string("setting"sv); + if (!setting_type.has_value()) + return {}; + + if (*setting_type == "autoplay"sv) + return SiteSettingType::Autoplay; + return {}; +} + +void SettingsUI::load_forcibly_enabled_site_settings() +{ + JsonArray site_settings; + + if (Application::web_content_options().enable_autoplay == EnableAutoplay::Yes) + site_settings.must_append(site_setting_type_to_string(SiteSettingType::Autoplay)); + + async_send_message("forciblyEnableSiteSettings"sv, move(site_settings)); +} + +void SettingsUI::set_site_setting_enabled_globally(JsonValue const& site_setting) +{ + auto setting = site_setting_type(site_setting); + if (!setting.has_value()) + return; + + auto enabled = site_setting.as_object().get_bool("enabled"sv); + if (!enabled.has_value()) + return; + + switch (*setting) { + case SiteSettingType::Autoplay: + WebView::Application::settings().set_autoplay_enabled_globally(*enabled); + break; + } + + load_current_settings(); +} + +void SettingsUI::add_site_setting_filter(JsonValue const& site_setting) +{ + auto setting = site_setting_type(site_setting); + if (!setting.has_value()) + return; + + auto filter = site_setting.as_object().get_string("filter"sv); + if (!filter.has_value()) + return; + + switch (*setting) { + case SiteSettingType::Autoplay: + WebView::Application::settings().add_autoplay_site_filter(*filter); + break; + } + + load_current_settings(); +} + +void SettingsUI::remove_site_setting_filter(JsonValue const& site_setting) +{ + auto setting = site_setting_type(site_setting); + if (!setting.has_value()) + return; + + auto filter = site_setting.as_object().get_string("filter"sv); + if (!filter.has_value()) + return; + + switch (*setting) { + case SiteSettingType::Autoplay: + WebView::Application::settings().remove_autoplay_site_filter(*filter); + break; + } + + load_current_settings(); +} + +void SettingsUI::remove_all_site_setting_filters(JsonValue const& site_setting) +{ + auto setting = site_setting_type(site_setting); + if (!setting.has_value()) + return; + + switch (*setting) { + case SiteSettingType::Autoplay: + WebView::Application::settings().remove_all_autoplay_site_filters(); + break; + } + + load_current_settings(); +} + } diff --git a/Libraries/LibWebView/WebUI/SettingsUI.h b/Libraries/LibWebView/WebUI/SettingsUI.h index 262d600ce8a..d0253cc7187 100644 --- a/Libraries/LibWebView/WebUI/SettingsUI.h +++ b/Libraries/LibWebView/WebUI/SettingsUI.h @@ -21,6 +21,11 @@ private: void set_new_tab_page_url(JsonValue const&); void load_available_search_engines(); void set_search_engine(JsonValue const&); + void load_forcibly_enabled_site_settings(); + void set_site_setting_enabled_globally(JsonValue const&); + void add_site_setting_filter(JsonValue const&); + void remove_site_setting_filter(JsonValue const&); + void remove_all_site_setting_filters(JsonValue const&); }; }