From 4be896ea3e21ee5eff168a3694e7242f9e6d92f6 Mon Sep 17 00:00:00 2001 From: vadims <118171981+vadimstruts@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:54:44 +0100 Subject: [PATCH] Element Picker on Android (#26725) Element Picker https://github.com/brave/brave-browser/issues/33241 --------- Signed-off-by: Vadym Struts --- android/brave_java_sources.gni | 1 + .../org/chromium/base/BraveFeatureList.java | 1 + .../chrome/browser/app/BraveActivity.java | 11 + .../BraveCosmeticFiltersUtils.java | 48 +++ .../browser/shields/BraveShieldsHandler.java | 18 + .../layout/brave_shields_secondary_layout.xml | 10 + browser/about_flags.cc | 8 + .../cosmetic_filters_utils.cc | 38 ++ .../cosmetic_filters/cosmetic_filters_utils.h | 18 + .../cosmetic_filters_tab_helper.cc | 27 +- .../cosmetic_filters_tab_helper.h | 2 + browser/cosmetic_filters/sources.gni | 9 + .../android/strings/android_brave_strings.grd | 3 + build/android/BUILD.gn | 1 + build/android/config.gni | 1 + .../flags/android/chrome_feature_list.cc | 3 +- .../render_view_context_menu.cc | 4 + .../brave_shields/core/common/features.cc | 8 + .../brave_shields/core/common/features.h | 1 + .../common/cosmetic_filters.mojom | 3 + .../renderer/cosmetic_filters_js_handler.cc | 111 ++++- .../renderer/cosmetic_filters_js_handler.h | 19 +- .../resources/data/element_picker.html | 243 +++++++---- .../resources/data/element_picker.ts | 401 +++++++++++++++--- components/definitions/chromel.d.ts | 3 + 25 files changed, 838 insertions(+), 154 deletions(-) create mode 100644 android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java create mode 100644 browser/android/cosmetic_filters/cosmetic_filters_utils.cc create mode 100644 browser/android/cosmetic_filters/cosmetic_filters_utils.h diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index 225f330ca8b7..a746cbc842d5 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -96,6 +96,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/browsing_data/BraveClearBrowsingDataFragmentAdvanced.java", "../../brave/android/java/org/chromium/chrome/browser/compositor/layouts/BraveToolbarSwipeLayout.java", "../../brave/android/java/org/chromium/chrome/browser/contextmenu/BraveChromeContextMenuPopulator.java", + "../../brave/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java", "../../brave/android/java/org/chromium/chrome/browser/crash/BravePureJavaExceptionReporter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/AssetRatioServiceFactory.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/BlockchainRegistryFactory.java", diff --git a/android/java/org/chromium/base/BraveFeatureList.java b/android/java/org/chromium/base/BraveFeatureList.java index bce7846dc3af..69e4e9aebedb 100644 --- a/android/java/org/chromium/base/BraveFeatureList.java +++ b/android/java/org/chromium/base/BraveFeatureList.java @@ -35,4 +35,5 @@ public abstract class BraveFeatureList { public static final String BRAVE_DAY_ZERO_EXPERIMENT = "BraveDayZeroExperiment"; public static final String BRAVE_FALLBACK_DOH_PROVIDER = "BraveFallbackDoHProvider"; public static final String BRAVE_BLOCK_ALL_COOKIES_TOGGLE = "BlockAllCookiesToggle"; + public static final String BRAVE_SHIELDS_ELEMENT_PICKER = "BraveShieldsElementPicker"; } diff --git a/android/java/org/chromium/chrome/browser/app/BraveActivity.java b/android/java/org/chromium/chrome/browser/app/BraveActivity.java index fc15df811c65..b70dfb0460d3 100644 --- a/android/java/org/chromium/chrome/browser/app/BraveActivity.java +++ b/android/java/org/chromium/chrome/browser/app/BraveActivity.java @@ -184,6 +184,7 @@ import org.chromium.chrome.browser.share.ShareDelegate; import org.chromium.chrome.browser.share.ShareDelegate.ShareOrigin; import org.chromium.chrome.browser.shields.ContentFilteringFragment; +import org.chromium.chrome.browser.shields.CreateCustomFiltersFragment; import org.chromium.chrome.browser.site_settings.BraveWalletEthereumConnectedSites; import org.chromium.chrome.browser.speedreader.BraveSpeedReaderUtils; import org.chromium.chrome.browser.tab.Tab; @@ -1622,6 +1623,16 @@ public void openBraveContentFilteringSettings() { settingsLauncher.startSettings(this, ContentFilteringFragment.class); } + public int getBraveThemeBackgroundColor() { + return ContextUtils.getApplicationContext() + .getColor(R.color.toolbar_background_color_for_ntp); + } + + public void openBraveCreateCustomFiltersSettings() { + SettingsNavigation settingsLauncher = SettingsNavigationFactory.createSettingsNavigation(); + settingsLauncher.startSettings(this, CreateCustomFiltersFragment.class); + } + public void openBraveWalletSettings() { SettingsNavigation settingsLauncher = SettingsNavigationFactory.createSettingsNavigation(); settingsLauncher.startSettings(this, BraveWalletPreferences.class); diff --git a/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java b/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java new file mode 100644 index 000000000000..aa5c6a13eea5 --- /dev/null +++ b/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java @@ -0,0 +1,48 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser.cosmetic_filters; + +import org.jni_zero.CalledByNative; +import org.jni_zero.JNINamespace; +import org.jni_zero.NativeMethods; + +import org.chromium.base.Log; +import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.tab.Tab; + +@JNINamespace("cosmetic_filters") +public class BraveCosmeticFiltersUtils { + private static final String TAG = "CosmeticFiltersUtils"; + + public static boolean launchContentPickerForWebContent(Tab tab) { + return BraveCosmeticFiltersUtilsJni.get().launchContentPickerForWebContent(tab); + } + + @CalledByNative + public static void showCustomFilterSettings() { + try { + BraveActivity.getBraveActivity().openBraveCreateCustomFiltersSettings(); + } catch (BraveActivity.BraveActivityNotFoundException e) { + Log.e(TAG, "open create custom filter settings" + e); + } + } + + @CalledByNative + public static int getThemeBackgroundColor() { + int backgroundColor = 0; + try { + backgroundColor = BraveActivity.getBraveActivity().getBraveThemeBackgroundColor(); + } catch (BraveActivity.BraveActivityNotFoundException e) { + Log.e(TAG, "Get theme background color" + e); + } + return backgroundColor; + } + + @NativeMethods + interface Natives { + boolean launchContentPickerForWebContent(Tab tab); + } +} diff --git a/android/java/org/chromium/chrome/browser/shields/BraveShieldsHandler.java b/android/java/org/chromium/chrome/browser/shields/BraveShieldsHandler.java index e273a719d4cb..b93420fe9fb5 100644 --- a/android/java/org/chromium/chrome/browser/shields/BraveShieldsHandler.java +++ b/android/java/org/chromium/chrome/browser/shields/BraveShieldsHandler.java @@ -48,6 +48,7 @@ import org.chromium.chrome.browser.BraveRewardsNativeWorker; import org.chromium.chrome.browser.app.BraveActivity; import org.chromium.chrome.browser.brave_stats.BraveStatsUtil; +import org.chromium.chrome.browser.cosmetic_filters.BraveCosmeticFiltersUtils; import org.chromium.chrome.browser.customtabs.CustomTabActivity; import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.onboarding.OnboardingPrefManager; @@ -744,6 +745,23 @@ private void setUpSwitchLayouts() { } else { fingerprintingSwitchLayout.setVisibility(View.GONE); } + + TextView blockElementsText = + mSecondaryLayout.findViewById(R.id.brave_shields_block_element_text); + blockElementsText.setVisibility( + ChromeFeatureList.isEnabled(BraveFeatureList.BRAVE_SHIELDS_ELEMENT_PICKER) + ? View.VISIBLE + : View.GONE); + blockElementsText.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + hideBraveShieldsMenu(); + Tab currentActiveTab = mIconFetcher.getTab(); + BraveCosmeticFiltersUtils.launchContentPickerForWebContent( + currentActiveTab); + } + }); } private void setUpAboutLayout() { diff --git a/android/java/res/layout/brave_shields_secondary_layout.xml b/android/java/res/layout/brave_shields_secondary_layout.xml index ec54866e9346..e8289273da63 100644 --- a/android/java/res/layout/brave_shields_secondary_layout.xml +++ b/android/java/res/layout/brave_shields_secondary_layout.xml @@ -58,4 +58,14 @@ android:id="@+id/brave_shields_fingerprinting_switch_id" layout="@layout/brave_shields_switcher"/> + + diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 00a26143ff7b..e8bb97c7d2d0 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -687,6 +687,14 @@ kOsAll, \ FEATURE_VALUE_TYPE(brave_shields::features::kBlockAllCookiesToggle), \ }, \ + { \ + "block-element-feature", \ + "Enable Block Element feature", \ + "Allows to block selected HTML element on the page", \ + kOsAll, \ + FEATURE_VALUE_TYPE( \ + brave_shields::features::kBraveShieldsElementPicker), \ + }, \ { \ "brave-super-referral", \ "Enable Brave Super Referral", \ diff --git a/browser/android/cosmetic_filters/cosmetic_filters_utils.cc b/browser/android/cosmetic_filters/cosmetic_filters_utils.cc new file mode 100644 index 000000000000..e850457af0da --- /dev/null +++ b/browser/android/cosmetic_filters/cosmetic_filters_utils.cc @@ -0,0 +1,38 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/android/cosmetic_filters/cosmetic_filters_utils.h" + +#include "brave/browser/cosmetic_filters/cosmetic_filters_tab_helper.h" +#include "brave/build/android/jni_headers/BraveCosmeticFiltersUtils_jni.h" +#include "chrome/browser/android/tab_android.h" + +namespace cosmetic_filters { + +static jboolean JNI_BraveCosmeticFiltersUtils_LaunchContentPickerForWebContent( + JNIEnv* env, + const base::android::JavaParamRef& tab) { + TabAndroid* tab_android = TabAndroid::GetNativeTab(env, tab); + content::WebContents* web_contents = tab_android->web_contents(); + if (!web_contents) { + return false; + } + + CosmeticFiltersTabHelper::LaunchContentPicker(web_contents); + + return true; +} + +void ShowCustomFilterSettings() { + JNIEnv* env = base::android::AttachCurrentThread(); + Java_BraveCosmeticFiltersUtils_showCustomFilterSettings(env); +} + +int32_t GetThemeBackgroundColor() { + JNIEnv* env = base::android::AttachCurrentThread(); + return Java_BraveCosmeticFiltersUtils_getThemeBackgroundColor(env); +} + +} // namespace cosmetic_filters diff --git a/browser/android/cosmetic_filters/cosmetic_filters_utils.h b/browser/android/cosmetic_filters/cosmetic_filters_utils.h new file mode 100644 index 000000000000..1b86ab9a051d --- /dev/null +++ b/browser/android/cosmetic_filters/cosmetic_filters_utils.h @@ -0,0 +1,18 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_ANDROID_COSMETIC_FILTERS_COSMETIC_FILTERS_UTILS_H_ +#define BRAVE_BROWSER_ANDROID_COSMETIC_FILTERS_COSMETIC_FILTERS_UTILS_H_ + +#include "content/public/browser/web_contents.h" + +namespace cosmetic_filters { + +void ShowCustomFilterSettings(); +int GetThemeBackgroundColor(); + +} // namespace cosmetic_filters + +#endif // BRAVE_BROWSER_ANDROID_COSMETIC_FILTERS_COSMETIC_FILTERS_UTILS_H_ diff --git a/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc b/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc index 637f1d1d897a..ead61963ff4f 100644 --- a/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc +++ b/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc @@ -16,12 +16,21 @@ #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/ui/brave_pages.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/color/chrome_color_id.h" +#include "ui/color/color_provider.h" +#else +#include "brave/browser/android/cosmetic_filters/cosmetic_filters_utils.h" +#include "chrome/browser/flags/android/chrome_session_state.h" #endif // !BUILDFLAG(IS_ANDROID) namespace cosmetic_filters { namespace { + bool IsValidFilterText(std::string_view selector) { if (!base::IsStringUTF8(selector)) { return false; @@ -85,7 +94,23 @@ void CosmeticFiltersTabHelper::ManageCustomFilters() { brave::ShowBraveAdblock(browser); } #else // !BUILDFLAG(IS_ANDROID) - NOTIMPLEMENTED(); + ShowCustomFilterSettings(); +#endif // !BUILDFLAG(IS_ANDROID) +} + +void CosmeticFiltersTabHelper::GetElementPickerThemeInfo( + GetElementPickerThemeInfoCallback callback) { +#if !BUILDFLAG(IS_ANDROID) + auto& color_provider = GetWebContents().GetColorProvider(); + std::move(callback).Run( + GetWebContents().GetColorMode() == ui::ColorProviderKey::ColorMode::kDark, + color_provider.GetColor(kColorSidePanelBadgeBackground)); +#else // !BUILDFLAG(IS_ANDROID) + const auto dark_mode_state = chrome::android::GetDarkModeState(); + std::move(callback).Run( + dark_mode_state == chrome::android::DarkModeState::kDarkModeSystem || + dark_mode_state == chrome::android::DarkModeState::kDarkModeApp, + GetThemeBackgroundColor()); #endif // !BUILDFLAG(IS_ANDROID) } diff --git a/browser/cosmetic_filters/cosmetic_filters_tab_helper.h b/browser/cosmetic_filters/cosmetic_filters_tab_helper.h index 2a9a8b8da8a7..9b5e91d34881 100644 --- a/browser/cosmetic_filters/cosmetic_filters_tab_helper.h +++ b/browser/cosmetic_filters/cosmetic_filters_tab_helper.h @@ -32,6 +32,8 @@ class CosmeticFiltersTabHelper private: void AddSiteCosmeticFilter(const std::string& filter) override; void ManageCustomFilters() override; + void GetElementPickerThemeInfo( + GetElementPickerThemeInfoCallback callback) override; friend class content::WebContentsUserData; diff --git a/browser/cosmetic_filters/sources.gni b/browser/cosmetic_filters/sources.gni index 707a20806ee3..69b52254b8c4 100644 --- a/browser/cosmetic_filters/sources.gni +++ b/browser/cosmetic_filters/sources.gni @@ -15,3 +15,12 @@ brave_browser_cosmetic_filters_deps = [ "//chrome/browser/ui", "//third_party/blink/public/common", ] + +if (is_android) { + brave_browser_cosmetic_filters_sources += [ + "//brave/browser/android/cosmetic_filters/cosmetic_filters_utils.cc", + "//brave/browser/android/cosmetic_filters/cosmetic_filters_utils.h", + ] + + brave_browser_cosmetic_filters_deps += [ "//brave/build/android:jni_headers" ] +} diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index 4ff223021162..dada07f42a0e 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -1645,6 +1645,9 @@ Are you sure you want to do this? If this site appears broken, try Shields down. + + Block element + Report a broken site diff --git a/build/android/BUILD.gn b/build/android/BUILD.gn index 9f5afcd87b8e..3f98c4155052 100644 --- a/build/android/BUILD.gn +++ b/build/android/BUILD.gn @@ -217,6 +217,7 @@ generate_jni("jni_headers") { "//brave/android/java/org/chromium/chrome/browser/brave_leo/BraveLeoSettingsLauncherHelper.java", "//brave/android/java/org/chromium/chrome/browser/brave_leo/BraveLeoUtils.java", "//brave/android/java/org/chromium/chrome/browser/brave_news/BraveNewsControllerFactory.java", + "//brave/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/AssetRatioServiceFactory.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/BlockchainRegistryFactory.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/BraveWalletProviderDelegateImplHelper.java", diff --git a/build/android/config.gni b/build/android/config.gni index d7dc2351e519..b681f6121d5c 100644 --- a/build/android/config.gni +++ b/build/android/config.gni @@ -108,6 +108,7 @@ brave_jni_headers_sources = [ "//brave/android/java/org/chromium/chrome/browser/brave_leo/BraveLeoMojomHelper.java", "//brave/android/java/org/chromium/chrome/browser/brave_leo/BraveLeoUtils.java", "//brave/android/java/org/chromium/chrome/browser/brave_news/BraveNewsControllerFactory.java", + "//brave/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/AssetRatioServiceFactory.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/BlockchainRegistryFactory.java", "//brave/android/java/org/chromium/chrome/browser/crypto_wallet/BraveWalletProviderDelegateImplHelper.java", diff --git a/chromium_src/chrome/browser/flags/android/chrome_feature_list.cc b/chromium_src/chrome/browser/flags/android/chrome_feature_list.cc index e5ee35b74f18..09b2d9be3680 100644 --- a/chromium_src/chrome/browser/flags/android/chrome_feature_list.cc +++ b/chromium_src/chrome/browser/flags/android/chrome_feature_list.cc @@ -45,7 +45,8 @@ &net::features::kBraveForgetFirstPartyStorage, \ &brave_shields::features::kBraveShowStrictFingerprintingMode, \ &brave_shields::features::kBraveLocalhostAccessPermission, \ - &brave_shields::features::kBlockAllCookiesToggle + &brave_shields::features::kBlockAllCookiesToggle, \ + &brave_shields::features::kBraveShieldsElementPicker // clang-format on diff --git a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc index 320938ecb7e3..cd52c7117673 100644 --- a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc +++ b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc @@ -43,6 +43,7 @@ #include "brave/browser/tor/tor_profile_service_factory.h" #endif +#include "base/feature_list.h" #include "brave/browser/ai_chat/ai_chat_service_factory.h" #include "brave/browser/brave_browser_process.h" #include "brave/browser/misc_metrics/process_misc_metrics.h" @@ -55,6 +56,7 @@ #include "brave/components/ai_chat/core/common/features.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "brave/components/ai_chat/core/common/pref_names.h" +#include "brave/components/brave_shields/core/common/features.h" #include "components/grit/brave_components_strings.h" #if BUILDFLAG(ENABLE_AI_REWRITER) @@ -714,6 +716,8 @@ void BraveRenderViewContextMenu::AppendDeveloperItems() { const auto page_url = source_web_contents_->GetLastCommittedURL(); add_block_elements &= page_url.SchemeIsHTTPOrHTTPS(); + add_block_elements &= base::FeatureList::IsEnabled( + brave_shields::features::kBraveShieldsElementPicker); if (add_block_elements) { std::optional inspect_index = menu_model_.GetIndexOfCommandId(IDC_CONTENT_CONTEXT_INSPECTELEMENT); diff --git a/components/brave_shields/core/common/features.cc b/components/brave_shields/core/common/features.cc index 0a3b8ba4949d..b8e73812d2ca 100644 --- a/components/brave_shields/core/common/features.cc +++ b/components/brave_shields/core/common/features.cc @@ -133,6 +133,14 @@ BASE_FEATURE(kCosmeticFilteringSyncLoad, BASE_FEATURE(kBlockAllCookiesToggle, "BlockAllCookiesToggle", base::FEATURE_DISABLED_BY_DEFAULT); +// when enabled, allow to select and block HTML elements +BASE_FEATURE(kBraveShieldsElementPicker, + "BraveShieldsElementPicker", +#if BUILDFLAG(IS_ANDROID) + base::FEATURE_DISABLED_BY_DEFAULT); +#else + base::FEATURE_ENABLED_BY_DEFAULT); +#endif // Enables extra TRACE_EVENTs in content filter js. The feature is // primary designed for local debugging. diff --git a/components/brave_shields/core/common/features.h b/components/brave_shields/core/common/features.h index 5ebff99f4e56..f147d9f0f148 100644 --- a/components/brave_shields/core/common/features.h +++ b/components/brave_shields/core/common/features.h @@ -41,6 +41,7 @@ BASE_DECLARE_FEATURE(kCosmeticFilteringJsPerformance); BASE_DECLARE_FEATURE(kCosmeticFilteringSyncLoad); BASE_DECLARE_FEATURE(kBlockAllCookiesToggle); BASE_DECLARE_FEATURE(kCosmeticFilteringCustomScriptlets); +BASE_DECLARE_FEATURE(kBraveShieldsElementPicker); extern const base::FeatureParam kComponentUpdateCheckIntervalMins; extern const base::FeatureParam kCosmeticFilteringSubFrameFirstSelectorsPollingDelayMs; diff --git a/components/cosmetic_filters/common/cosmetic_filters.mojom b/components/cosmetic_filters/common/cosmetic_filters.mojom index a7d23f5c7944..4b13390f7b34 100644 --- a/components/cosmetic_filters/common/cosmetic_filters.mojom +++ b/components/cosmetic_filters/common/cosmetic_filters.mojom @@ -24,6 +24,9 @@ interface CosmeticFiltersHandler { // Opens the custom filter section in Shields settings . ManageCustomFilters(); + + GetElementPickerThemeInfo() => ( + bool is_dark_mode_enabled, int32 background_color); }; // An interface to render frame agent `CosmeticFiltersJSHandler` to control diff --git a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc index 6f4671da110a..8247b9d618d5 100644 --- a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc +++ b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc @@ -33,6 +33,7 @@ #include "third_party/blink/public/web/web_local_frame.h" #include "third_party/blink/public/web/web_script_source.h" #include "ui/base/resource/resource_bundle.h" +#include "v8/include/v8-primitive.h" #include "v8/include/v8.h" namespace { @@ -120,6 +121,9 @@ constexpr char kHideSelectorsInjectScript[] = }; })();)"; +constexpr char kIsDarkModeEnabledPropery[] = "isDarkModeEnabled"; +constexpr char kBackgroundColorPropery[] = "bgcolor"; + std::string LoadDataResource(const int id) { auto& resource_bundle = ui::ResourceBundle::GetSharedInstance(); if (resource_bundle.IsGzipped(id)) { @@ -192,6 +196,16 @@ MakeCosmeticFiltersHandler(content::RenderFrame* render_frame) { return handler; } +v8::Maybe CreateDataProperty(v8::Local context, + v8::Local object, + std::string_view name, + v8::Local value) { + const v8::Local name_str = + gin::StringToV8(context->GetIsolate(), name); + + return object->CreateDataProperty(context, name_str, value); +} + } // namespace namespace cosmetic_filters { @@ -235,7 +249,8 @@ CosmeticFiltersJSHandler::CosmeticFiltersJSHandler( const int32_t isolated_world_id) : render_frame_(render_frame), isolated_world_id_(isolated_world_id), - enabled_1st_party_cf_(false) { + enabled_1st_party_cf_(false), + v8_value_converter_(content::V8ValueConverter::Create()) { EnsureConnected(); render_frame_->GetAssociatedInterfaceRegistry() @@ -274,13 +289,89 @@ bool CosmeticFiltersJSHandler::OnIsFirstParty(const std::string& url_string) { void CosmeticFiltersJSHandler::OnAddSiteCosmeticFilter( const std::string& selector) { const auto host = url_.host(); - auto handler = MakeCosmeticFiltersHandler(render_frame_); - handler->AddSiteCosmeticFilter(selector); + GetElementPickerRemoteHandler()->AddSiteCosmeticFilter(selector); } void CosmeticFiltersJSHandler::OnManageCustomFilters() { - auto handler = MakeCosmeticFiltersHandler(render_frame_); - handler->ManageCustomFilters(); + GetElementPickerRemoteHandler()->ManageCustomFilters(); +} + +mojo::AssociatedRemote& +CosmeticFiltersJSHandler::GetElementPickerRemoteHandler() { + if (!element_picker_actions_handler_ || + !element_picker_actions_handler_.is_connected()) { + element_picker_actions_handler_ = MakeCosmeticFiltersHandler(render_frame_); + element_picker_actions_handler_.set_disconnect_handler(base::BindOnce( + &CosmeticFiltersJSHandler::OnElementPickerRemoteHandlerDisconnect, + weak_ptr_factory_.GetWeakPtr())); + } + return element_picker_actions_handler_; +} + +void CosmeticFiltersJSHandler::OnElementPickerRemoteHandlerDisconnect() { + element_picker_actions_handler_.reset(); +} + +v8::Local CosmeticFiltersJSHandler::GetCosmeticFilterThemeInfo( + v8::Isolate* isolate) { + v8::MaybeLocal resolver = + v8::Promise::Resolver::New(isolate->GetCurrentContext()); + + if (!resolver.IsEmpty()) { + auto promise_resolver = + std::make_unique>(); + promise_resolver->Reset(isolate, resolver.ToLocalChecked()); + auto context_old = std::make_unique>( + isolate, isolate->GetCurrentContext()); + + GetElementPickerRemoteHandler()->GetElementPickerThemeInfo(base::BindOnce( + &CosmeticFiltersJSHandler::OnGetCosmeticFilterThemeInfo, + weak_ptr_factory_.GetWeakPtr(), std::move(promise_resolver), isolate, + std::move(context_old))); + + return resolver.ToLocalChecked()->GetPromise(); + } + + return v8::Local(); +} + +void CosmeticFiltersJSHandler::OnGetCosmeticFilterThemeInfo( + std::unique_ptr> promise_resolver, + v8::Isolate* isolate, + std::unique_ptr> context_old, + bool is_dark_mode_enabled, + int32_t background_color) { + v8::HandleScope handle_scope(isolate); + v8::Local context = context_old->Get(isolate); + v8::Context::Scope context_scope(context); + v8::MicrotasksScope microtasks(isolate, context->GetMicrotaskQueue(), + v8::MicrotasksScope::kDoNotRunMicrotasks); + + v8::Local resolver = promise_resolver->Get(isolate); + v8::Local result; + v8::Local object = v8::Object::New(isolate); + CHECK(CreateDataProperty(context, object, kIsDarkModeEnabledPropery, + v8_value_converter_->ToV8Value( + base::Value(is_dark_mode_enabled), context)) + .ToChecked()); + CHECK(CreateDataProperty(context, object, kBackgroundColorPropery, + v8_value_converter_->ToV8Value( + base::Value(background_color), context)) + .ToChecked()); + result = object; + + std::ignore = resolver->Resolve(context, result); +} + +v8::Local CosmeticFiltersJSHandler::GetPlatform( + v8::Isolate* isolate) { + return gin::StringToV8(isolate, +#if BUILDFLAG(IS_ANDROID) + "android" +#else // !BUILDFLAG(IS_ANDROID) + "desktop" +#endif // !BUILDFLAG(IS_ANDROID) + ); } void CosmeticFiltersJSHandler::AddJavaScriptObjectToFrame( @@ -356,6 +447,16 @@ void CosmeticFiltersJSHandler::BindFunctionsToObject( base::BindRepeating(&CosmeticFiltersJSHandler::OnManageCustomFilters, base::Unretained(this))); + BindFunctionToObject( + isolate, javascript_object, "getElementPickerThemeInfo", + base::BindRepeating(&CosmeticFiltersJSHandler::GetCosmeticFilterThemeInfo, + base::Unretained(this))); + + BindFunctionToObject( + isolate, javascript_object, "getPlatform", + base::BindRepeating(&CosmeticFiltersJSHandler::GetPlatform, + base::Unretained(this), isolate)); + if (perf_tracker_) { BindFunctionToObject( isolate, javascript_object, "onHandleMutationsBegin", diff --git a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h index 5e218163511f..0e136cbd5420 100644 --- a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h +++ b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h @@ -16,10 +16,12 @@ #include "brave/components/cosmetic_filters/common/cosmetic_filters.mojom.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/v8_value_converter.h" #include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/associated_remote.h" #include "mojo/public/cpp/bindings/remote.h" #include "url/gurl.h" -#include "v8/include/v8.h" +#include "v8/include/v8-promise.h" namespace cosmetic_filters { @@ -58,6 +60,8 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { void Bind( mojo::PendingAssociatedReceiver receiver); + void OnElementPickerRemoteHandlerDisconnect(); + // CosmeticFiltersAgent overrides: void LaunchContentPicker() override; @@ -76,6 +80,14 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { bool OnIsFirstParty(const std::string& url_string); void OnAddSiteCosmeticFilter(const std::string& selector); void OnManageCustomFilters(); + v8::Local GetPlatform(v8::Isolate* isolate); + v8::Local GetCosmeticFilterThemeInfo(v8::Isolate* isolate); + void OnGetCosmeticFilterThemeInfo( + std::unique_ptr> promise_resolver, + v8::Isolate* isolate, + std::unique_ptr> context_old, + bool is_dark_mode_enabled, + int32_t background_color); int OnEventBegin(const std::string& event_name); void OnEventEnd(const std::string& event_name, int); @@ -83,6 +95,10 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { bool generichide_ = false; + mojo::AssociatedRemote& + GetElementPickerRemoteHandler(); + mojo::AssociatedRemote + element_picker_actions_handler_; raw_ptr render_frame_ = nullptr; mojo::Remote cosmetic_filters_resources_; mojo::AssociatedReceiver receiver_{this}; @@ -91,6 +107,7 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { std::vector exceptions_; GURL url_; std::optional resources_dict_; + std::unique_ptr v8_value_converter_; // True if the content_cosmetic.bundle.js has injected in the current frame. bool bundle_injected_ = false; diff --git a/components/cosmetic_filters/resources/data/element_picker.html b/components/cosmetic_filters/resources/data/element_picker.html index d7b6a5ef1eed..2cff0f5c9735 100644 --- a/components/cosmetic_filters/resources/data/element_picker.html +++ b/components/cosmetic_filters/resources/data/element_picker.html @@ -1,87 +1,161 @@ @@ -89,20 +163,25 @@ - + -
+
-
+
- - - +
+ +
+
+ + + +
diff --git a/components/cosmetic_filters/resources/data/element_picker.ts b/components/cosmetic_filters/resources/data/element_picker.ts index 30cc985d3225..01a2ca34ce98 100644 --- a/components/cosmetic_filters/resources/data/element_picker.ts +++ b/components/cosmetic_filters/resources/data/element_picker.ts @@ -6,23 +6,25 @@ const NSSVG = 'http://www.w3.org/2000/svg' let pickerDiv: HTMLDivElement | null let shadowRoot: ShadowRoot | null +let isAndroid: boolean | null const api = { cosmeticFilterCreate: (selector: string) => { cf_worker.addSiteCosmeticFilter(selector) - - const styleId = 'brave-content-picker-style' - let style = document.getElementById(styleId) - if (!style) { - style = document.createElement('style') - style.id = styleId - document.head.appendChild(style) - } - style.innerText += `${selector} {display: none !important;}` }, cosmeticFilterManage: () => { cf_worker.manageCustomFilters() }, + getElementPickerThemeInfo: (callback: ( + isDarkModeEnabled: boolean, bgcolor: number) => void) => { + cf_worker.getElementPickerThemeInfo().then( + (val:{isDarkModeEnabled: boolean; bgcolor: number}) => { + callback(val.isDarkModeEnabled, val.bgcolor) + }) + }, + getPlatform: ():string => { + return cf_worker.getPlatform() + } } // When the picker is activated, it eats all pointer events and takes up the @@ -284,7 +286,7 @@ const elementPickerOnKeydown = (event: KeyboardEvent): void => { } const elementPickerViewportChanged = () => { - recalculateAndSendTargets(targetedElems) + recalculateAndSendTargets(null) } const quitElementPicker = () => { @@ -337,11 +339,10 @@ const attachElementPicker = () => { pickerDiv.setAttribute('style', pickerCSSStyle) document.documentElement.appendChild(pickerDiv) - // Setup listeners to assist element picker document.addEventListener('keydown', elementPickerOnKeydown, true) - document.addEventListener('resize', elementPickerViewportChanged) - document.addEventListener('scroll', elementPickerViewportChanged) + window.addEventListener('resize', elementPickerViewportChanged) + window.addEventListener('scroll', elementPickerViewportChanged) return shadowRoot } @@ -353,6 +354,51 @@ interface TargetRect { height: number } +class Target { + element: Element + rectElem: Element + coord: TargetRect + + constructor (elem: Element) { + this.element = elem + this.coord = targetRectFromElement(this.element) + } + + forceRecalcCoords() { + this.coord = targetRectFromElement(this.element) + } +} + +class TargetsCollection { + targets: Target[] = [] + togglePicker: ((val: boolean) => void) | null = null + reset(elems: Element[]) { + this.targets.length = 0 + elems.forEach((elem: Element) => { + this.targets.push(new Target(elem)) + }); + } + + getXpathsForMarked(): string[] { + return this.targets.map((el) => getElementXpath(el.element)) + .filter((item): item is string => item !== null) + } + + forceRecalcCoords() { + this.targets.forEach(t => t.forceRecalcCoords()) + // for case when element no longer in the DOM + this.targets = this.targets.filter(item => + item.coord.height !== 0 && item.coord.width !== 0) + if(this.targets.length === 0 && this.togglePicker) { + this.togglePicker(false) + } + } + + size() { + return this.targets.length + } +} + const targetRectFromElement = (elem: Element): TargetRect => { const rect = elem.getBoundingClientRect() return { @@ -364,17 +410,104 @@ const targetRectFromElement = (elem: Element): TargetRect => { } let lastHoveredElem: HTMLElement | null = null -let targetedElems: Element[] = [] +const targetedElems = new TargetsCollection + +const recalculateAndSendTargets = (elems: Element[] | null) => { + if(elems) { + targetedElems.reset(elems) + } else { + targetedElems.forceRecalcCoords() + } + + highlightElements() +} -const recalculateAndSendTargets = (elems: Element[]) => { - targetedElems = elems - const coords = elems.map((e: Element) => targetRectFromElement(e)) - highlightElements(coords) +const hideByCssSelector = (selector: string) => { + const styleId = 'brave-content-picker-style' + let style = document.getElementById(styleId) + if (!style) { + style = document.createElement('style') + style.id = styleId + document.head.appendChild(style) + } + style.innerText += `${selector} {display: none !important;}` +} + +const hideByXPath = (xpath: string): void => { + const hideStyle: Partial = { + display: 'none' + } + const result = document.evaluate( + xpath, + document, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE, + null + ); + + let node: Node | null = result.iterateNext(); + + while (node) { + if (node instanceof HTMLElement) { + Object.assign(node.style, hideStyle); + } + node = result.iterateNext(); + } +} + +const getElementByXpath = (xpath: string) => { + const result = document.evaluate( + xpath, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null + ); + + // Return the single node found, or null if none + return result.singleNodeValue as Element | null; +} + +const getElementXpath = (element: Element | null): string => { + if (!element || !(element instanceof Element)) { + return '' + } + + const getXPathSegment = (el: Element): string => { + const tagName = el.tagName.toLowerCase(); + + if (!el.parentElement) { + return `/${tagName}` + } + + const siblings = Array.from(el.parentElement.children).filter( + (sibling) => sibling.tagName === el.tagName + ); + if (siblings.length === 1) { + return `/${tagName}` + } + + const index = siblings.indexOf(el) + 1 + return `/${tagName}[${index}]` + }; + + const segments: string[] = [] + + while (element) { + segments.unshift(getXPathSegment(element)) + element = element.parentElement! + } + + return segments.join('') } const onTargetSelected = (selected: Element | null, index: number): string => { if (lastHoveredElem === null) { return '' } + if (isAndroid) { + return getElementXpath(selected) + } + let elem: Element | null = selected const selectorBuilders = [] const specificityMasks = [ @@ -399,9 +532,13 @@ const onTargetSelected = (selected: Element | null, index: number): string => { let i = 0 for (; i < selectorBuilders.length; i++) { const b = selectorBuilders[i] - if ((mask & SpecificityFlags.Id) && b.hasId || - document.querySelectorAll(b.toString(mask)).length === 1) { - break + try { + if ((mask & SpecificityFlags.Id) && b.hasId || + document.querySelectorAll(b.toString(mask)).length === 1) { + break + } + } catch { + continue } } const selector = selectorBuilders @@ -420,11 +557,25 @@ const elementPickerHoverCoordsChanged = (x: number, y: number) => { } } +const getElementBySelector = (selector: string) => { + let elements: Element[] | null; + if (isAndroid) { + const singleElement = getElementByXpath(selector); + elements = singleElement ? [singleElement] : null; + } else { + const nodeList = document.querySelectorAll(selector); + elements = nodeList.length > 0 ? Array.from(nodeList) : null; + } + return elements +} + const elementPickerUserSelectedTarget = (specificity: number) => { if (lastHoveredElem instanceof HTMLElement) { const selector = onTargetSelected(lastHoveredElem, specificity) if (selector !== '') { - recalculateAndSendTargets(Array.from(document.querySelectorAll(selector))) + try { + recalculateAndSendTargets(getElementBySelector(selector)) + } catch {} } return { isValid: selector !== '', @@ -439,13 +590,20 @@ const elementPickerUserSelectedTarget = (specificity: number) => { const elementPickerUserModifiedRule = (selector: string) => { if (selector.length > 0) { - recalculateAndSendTargets(Array.from(document.querySelectorAll(selector))) + try { + recalculateAndSendTargets(Array.from(document.querySelectorAll(selector))) + } catch {} } } const launchElementPicker = (root: ShadowRoot) => { let hasSelectedTarget = false + const btnShowRulesBox = root.getElementById('btnShowRulesBox') + if (isAndroid && btnShowRulesBox) { + btnShowRulesBox.style.display = 'none' + } + root.addEventListener( 'keydown', (event: KeyboardEvent) => { @@ -459,17 +617,19 @@ const launchElementPicker = (root: ShadowRoot) => { ) const svg = root.getElementById('picker-ui')! - - svg.addEventListener( - 'mousemove', - (event) => { - if (!hasSelectedTarget) { - elementPickerHoverCoordsChanged(event.clientX, event.clientY) - } - event.stopPropagation() - }, - true, - ) + if(window.matchMedia("(pointer: fine)").matches && + window.matchMedia("(hover: hover)").matches) { + svg.addEventListener( + 'mousemove', + (event) => { + if (!hasSelectedTarget) { + elementPickerHoverCoordsChanged(event.clientX, event.clientY) + } + event.stopPropagation() + }, + true, + ) + } const rulesTextArea: HTMLInputElement = root.querySelector( '#rules-box > textarea', @@ -488,22 +648,80 @@ const launchElementPicker = (root: ShadowRoot) => { hasSelectedTarget = true togglePopup(true) }) - const section = root.querySelector('section')! + + const setDarkModeButtons = (isDarkModeEnabled: boolean) => { + const elements = root.querySelectorAll('.button'); + elements.forEach(element => { + if (element.classList.contains(isDarkModeEnabled ? 'light' : 'dark')) { + element.classList.remove(isDarkModeEnabled ? 'light' : 'dark'); + } + element.classList.add(isDarkModeEnabled ? 'dark' : 'light') + }); + } + + const enableButtons = (isDisabled: boolean) => { + const elements = root.querySelectorAll('.button'); + elements.forEach(element => { + if(isDisabled) { + element.classList.add('disabled') + } else { + element.classList.remove('disabled'); + } + }); + } + + const section = root.getElementById('main-section')! + if (!isAndroid) { + section.classList.add('desktop') + } const togglePopup = (show: boolean) => { - section.setAttribute('style', `opacity : ${show ? '1' : '0.2'}`) + enableButtons(!show) + if (show) { + createButton.textContent = "Block Element" + } else { + createButton.textContent = "Select element you want to block" + } + if (!isAndroid) { + section.style.setProperty('opacity', show ? '1' : '0.2') + } } + targetedElems.togglePicker = togglePopup const slider = root.getElementById('sliderSpecificity') as HTMLInputElement + if (isAndroid) { + const sc = root.getElementById('slider-container') as HTMLInputElement + sc.style.display = 'none' + } + + const retrieveTheme = () => { + api.getElementPickerThemeInfo( + (isDarkModeEnabled: boolean, bgcolor: number) => { + const colorHex = `#${(bgcolor & 0xFFFFFF).toString(16).padStart(6, '0')}` + section.style.setProperty('background-color', colorHex) + root.querySelectorAll('.secondary-button').forEach(e => + (e as HTMLElement).style.setProperty('background-color', colorHex)) + + setDarkModeButtons(isDarkModeEnabled) + }) + } + const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); + const handleColorSchemeChange = (event: MediaQueryListEvent) => { + retrieveTheme() + }; + prefersDarkScheme.addEventListener('change', handleColorSchemeChange); + retrieveTheme() const dispatchSelect = () => { const { isValid, selector } = elementPickerUserSelectedTarget( parseInt(slider.value), ) + + hasSelectedTarget = isValid + togglePopup(isValid) if (isValid) { - hasSelectedTarget = true - togglePopup(true) - // disable hovering new elements rulesTextArea.value = selector + } else { + slider.value = '4' } } @@ -511,23 +729,46 @@ const launchElementPicker = (root: ShadowRoot) => { dispatchSelect() }) - svg.addEventListener('click', () => { - if (hasSelectedTarget) { - // We are already previewing a target. We'll interpet another click - // as the user wanting back control of the UI. - hasSelectedTarget = false - slider.value = '5' - togglePopup(false) - return + const oneClickEventHandler = (event: MouseEvent | TouchEvent) => { + let elem: Element | null = null + if (event instanceof MouseEvent) { + elem = elementFromFrameCoords(event.clientX, event.clientY) + } else if (event instanceof TouchEvent){ + const touch = event.touches[0]; + elem = elementFromFrameCoords(touch.clientX, touch.clientY) + } + + if (elem) { + recalculateAndSendTargets([elem]) + lastHoveredElem = elem as HTMLElement } + dispatchSelect() - }) + } + + svg.addEventListener('click', oneClickEventHandler) const createButton = root.getElementById('btnCreate')! createButton.addEventListener('click', () => { + if (createButton.classList.contains('block-button-disabled')) { + return + } + if (isAndroid) { + const selectedXpaths = targetedElems.getXpathsForMarked() + if(selectedXpaths && selectedXpaths.length > 0) { + for(const expr of selectedXpaths) { + const rule = `:xpath(${expr})` + api.cosmeticFilterCreate(rule) + hideByXPath(expr) + } + quitElementPicker() + } + return + } const selector = rulesTextArea.value.trim() if (selector.length > 0) { api.cosmeticFilterCreate(selector) + hideByCssSelector(selector) quitElementPicker() } }) @@ -541,38 +782,70 @@ const launchElementPicker = (root: ShadowRoot) => { manageButton.addEventListener('click', () => { api.cosmeticFilterManage(); }) + + const toggleDisplay = (target: HTMLElement | null, + trigger: HTMLElement | null) => { + if(!target || !trigger){ + return + } + trigger.addEventListener('click', e => { + if (target.style.display !== 'block') { + target.style.display = 'block' + trigger.textContent = 'Hide rules' + } else { + target.style.display = 'none' + trigger.textContent = 'Show rules' + } + }) + } + const rulesBox = root.getElementById('rules-box')! + const showRulesButton = root.getElementById('btnShowRulesBox')! + toggleDisplay(rulesBox, showRulesButton) } -const highlightElements = (coords: TargetRect[]) => { +const highlightElements = () => { if (!shadowRoot) return const svg = shadowRoot.getElementById('picker-ui')! const svgMask = shadowRoot.getElementById('highlight-mask')! - // // Delete old element targeting rectangles and their corresponding masks - const oldMask = svg.querySelectorAll('.mask') - for (const old of oldMask) { - old.remove() - } + svg.querySelectorAll('.mask').forEach(el => el.remove()); - for (const rect of coords) { - // Add the mask to the SVG definition so the dark background is removed + const svgMaskFragment = document.createDocumentFragment(); + const svgFragment = document.createDocumentFragment(); + + const createMaskElement = (): SVGRectElement => { const mask = document.createElementNS(NSSVG, 'rect') mask.classList.add('mask') - mask.setAttribute('x', rect.x.toString()) - mask.setAttribute('y', rect.y.toString()) - mask.setAttribute('width', rect.width.toString()) - mask.setAttribute('height', rect.height.toString()) - svgMask.appendChild(mask) + mask.rx.baseVal.value = 10 + mask.setAttribute('px', '10px') + mask.setAttribute('stroke-linejoin', 'round') + return mask + } + + for (const target of targetedElems.targets) { + // Add the mask to the SVG definition so the dark background is removed + const mask = createMaskElement() + mask.x.baseVal.value = target.coord.x; + mask.y.baseVal.value = target.coord.y; + mask.width.baseVal.value = target.coord.width; + mask.height.baseVal.value = target.coord.height; + mask.rx.baseVal.value = 10; + svgMaskFragment.appendChild(mask) // Use the same element, but add the target class which turns the // target rectangle orange - const braveTargetingArea = mask.cloneNode() as SVGRectElement - braveTargetingArea.classList.add('target') - svg.appendChild(braveTargetingArea) + const targetingArea = mask.cloneNode(false) as SVGRectElement + targetingArea.classList.add('target') + target.rectElem = targetingArea + + svgFragment.appendChild(targetingArea) } + svgMask.appendChild(svgMaskFragment) + svg.appendChild(svgFragment) } const active = document.getElementById('brave-element-picker') if (!active) { + isAndroid = api.getPlatform() === 'android' launchElementPicker(attachElementPicker()) } diff --git a/components/definitions/chromel.d.ts b/components/definitions/chromel.d.ts index b8a7a7d88fd2..b947c37ac567 100644 --- a/components/definitions/chromel.d.ts +++ b/components/definitions/chromel.d.ts @@ -224,6 +224,9 @@ declare namespace chrome.windows { declare namespace cf_worker { const addSiteCosmeticFilter: (selector: string) => void const manageCustomFilters: () => void + const getElementPickerThemeInfo: () => + Promise<{isDarkModeEnabled: boolean; bgcolor: number}> + const getPlatform: () => string } declare namespace chrome.test {