diff --git a/android/java/org/chromium/chrome/browser/app/BraveActivity.java b/android/java/org/chromium/chrome/browser/app/BraveActivity.java index 022e510939b7..73cc46156fb7 100644 --- a/android/java/org/chromium/chrome/browser/app/BraveActivity.java +++ b/android/java/org/chromium/chrome/browser/app/BraveActivity.java @@ -1621,6 +1621,11 @@ 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); diff --git a/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java b/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java index ec9d79808939..aa5c6a13eea5 100644 --- a/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java +++ b/android/java/org/chromium/chrome/browser/cosmetic_filters/BraveCosmeticFiltersUtils.java @@ -30,6 +30,17 @@ public static void showCustomFilterSettings() { } } + @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/browser/android/cosmetic_filters/cosmetic_filters_utils.cc b/browser/android/cosmetic_filters/cosmetic_filters_utils.cc index 346a3caba270..e850457af0da 100644 --- a/browser/android/cosmetic_filters/cosmetic_filters_utils.cc +++ b/browser/android/cosmetic_filters/cosmetic_filters_utils.cc @@ -30,4 +30,9 @@ void ShowCustomFilterSettings() { 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 index 2e999348cc0d..636839565e2a 100644 --- a/browser/android/cosmetic_filters/cosmetic_filters_utils.h +++ b/browser/android/cosmetic_filters/cosmetic_filters_utils.h @@ -11,7 +11,7 @@ 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 9892b62324c7..226ba9e6ef5a 100644 --- a/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc +++ b/browser/cosmetic_filters/cosmetic_filters_tab_helper.cc @@ -16,7 +16,13 @@ #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/ui/brave_pages.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/themes/theme_service_factory.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" #endif // !BUILDFLAG(IS_ANDROID) @@ -24,6 +30,7 @@ namespace cosmetic_filters { namespace { + bool IsValidFilterText(std::string_view selector) { if (!base::IsStringUTF8(selector)) { return false; @@ -91,6 +98,17 @@ void CosmeticFiltersTabHelper::ManageCustomFilters() { #endif // !BUILDFLAG(IS_ANDROID) } +void CosmeticFiltersTabHelper::GetElementPickerThemeInfo( + GetElementPickerThemeInfoCallback callback) { +#if !BUILDFLAG(IS_ANDROID) + auto& color_provider = GetWebContents().GetColorProvider(); + std::move(callback).Run( + color_provider.GetColor(kColorSidePanelBadgeBackground)); +#else // !BUILDFLAG(IS_ANDROID) + std::move(callback).Run(GetThemeBackgroundColor()); +#endif // !BUILDFLAG(IS_ANDROID) +} + CosmeticFiltersTabHelper::CosmeticFiltersTabHelper( content::WebContents* web_contents) : content::WebContentsUserData(*web_contents), 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/components/cosmetic_filters/common/cosmetic_filters.mojom b/components/cosmetic_filters/common/cosmetic_filters.mojom index a7d23f5c7944..69bcf398d2a2 100644 --- a/components/cosmetic_filters/common/cosmetic_filters.mojom +++ b/components/cosmetic_filters/common/cosmetic_filters.mojom @@ -24,6 +24,8 @@ interface CosmeticFiltersHandler { // Opens the custom filter section in Shields settings . ManageCustomFilters(); + + GetElementPickerThemeInfo() => (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 2ee307644740..fc9453364ad4 100644 --- a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc +++ b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.cc @@ -119,6 +119,11 @@ constexpr char kHideSelectorsInjectScript[] = ...document.adoptedStyleSheets]; }; })();)"; +constexpr char kSePickerThemeInfoScript[] = + R"((function() { + const CC = window.content_cosmetic + CC.setTheme(%d) + })();)"; std::string LoadDataResource(const int id) { auto& resource_bundle = ui::ResourceBundle::GetSharedInstance(); @@ -274,13 +279,36 @@ 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); + GetRemoteHandler()->AddSiteCosmeticFilter(selector); } void CosmeticFiltersJSHandler::OnManageCustomFilters() { - auto handler = MakeCosmeticFiltersHandler(render_frame_); - handler->ManageCustomFilters(); + GetRemoteHandler()->ManageCustomFilters(); +} + +mojo::AssociatedRemote& +CosmeticFiltersJSHandler::GetRemoteHandler() { + if (!handler_ || !handler_.is_bound() || !handler_.is_connected()) { + handler_ = MakeCosmeticFiltersHandler(render_frame_); + } + return handler_; +} + +void CosmeticFiltersJSHandler::GetCosmeticFilterThemeInfo() { + GetRemoteHandler()->GetElementPickerThemeInfo( + base::BindOnce(&CosmeticFiltersJSHandler::OnGetCosmeticFilterThemeInfo, + weak_ptr_factory_.GetWeakPtr())); +} + +void CosmeticFiltersJSHandler::OnGetCosmeticFilterThemeInfo( + int32_t background_color) { + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + std::string new_selectors_script = + base::StringPrintf(kSePickerThemeInfoScript, background_color); + web_frame->ExecuteScriptInIsolatedWorld( + isolated_world_id_, + blink::WebScriptSource(blink::WebString::FromUTF8(new_selectors_script)), + blink::BackForwardCacheAware::kAllow); } void CosmeticFiltersJSHandler::AddJavaScriptObjectToFrame( @@ -356,6 +384,11 @@ void CosmeticFiltersJSHandler::BindFunctionsToObject( base::BindRepeating(&CosmeticFiltersJSHandler::OnManageCustomFilters, base::Unretained(this))); + BindFunctionToObject( + isolate, javascript_object, "getElementPickerThemeInfo", + base::BindRepeating(&CosmeticFiltersJSHandler::GetCosmeticFilterThemeInfo, + base::Unretained(this))); + 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..ad504b3c1607 100644 --- a/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h +++ b/components/cosmetic_filters/renderer/cosmetic_filters_js_handler.h @@ -17,6 +17,7 @@ #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.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" @@ -76,6 +77,8 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { bool OnIsFirstParty(const std::string& url_string); void OnAddSiteCosmeticFilter(const std::string& selector); void OnManageCustomFilters(); + void GetCosmeticFilterThemeInfo(); + void OnGetCosmeticFilterThemeInfo(int32_t background_color); int OnEventBegin(const std::string& event_name); void OnEventEnd(const std::string& event_name, int); @@ -83,6 +86,10 @@ class CosmeticFiltersJSHandler : public mojom::CosmeticFiltersAgent { bool generichide_ = false; + mojo::AssociatedRemote& + GetRemoteHandler(); + mojo::AssociatedRemote + handler_; raw_ptr render_frame_ = nullptr; mojo::Remote cosmetic_filters_resources_; mojo::AssociatedReceiver receiver_{this}; diff --git a/components/cosmetic_filters/resources/data/android_element_picker.html b/components/cosmetic_filters/resources/data/android_element_picker.html new file mode 100644 index 000000000000..3c894f29cfee --- /dev/null +++ b/components/cosmetic_filters/resources/data/android_element_picker.html @@ -0,0 +1,153 @@ + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+ +
+
+ + + +
+
+
+
+ \ No newline at end of file diff --git a/components/cosmetic_filters/resources/data/element_picker.html b/components/cosmetic_filters/resources/data/element_picker.html index f7057abe29e6..c457d6319e89 100644 --- a/components/cosmetic_filters/resources/data/element_picker.html +++ b/components/cosmetic_filters/resources/data/element_picker.html @@ -57,6 +57,13 @@ justify-content: space-between; flex-direction: column; } + + .buttons-line { + display: flex; + justify-content: space-between; + flex-direction: row; + padding-bottom: 6px; + } #main-buttons { display: flex; @@ -93,11 +100,6 @@ padding-bottom: 4px; } .target { - stroke: #FF6200; - stroke-width: 2px; - fill: rgba(255, 130, 63, 0.25); - } - .marked-target { stroke: #3d39c8; stroke-width: 2px; fill: rgba(121, 116, 224, 0.25); @@ -111,7 +113,7 @@ -
+
@@ -120,12 +122,14 @@
-
- - - -
- +
+ +
+
+ + + +
diff --git a/components/cosmetic_filters/resources/data/element_picker.ts b/components/cosmetic_filters/resources/data/element_picker.ts index 34564d5c5a76..08124e9aea26 100644 --- a/components/cosmetic_filters/resources/data/element_picker.ts +++ b/components/cosmetic_filters/resources/data/element_picker.ts @@ -6,6 +6,7 @@ const NSSVG = 'http://www.w3.org/2000/svg' let pickerDiv: HTMLDivElement | null let shadowRoot: ShadowRoot | null +const isAndroid = /(android)/i.test(navigator.userAgent) const api = { cosmeticFilterCreate: (selector: string) => { @@ -14,6 +15,9 @@ const api = { cosmeticFilterManage: () => { cf_worker.manageCustomFilters() }, + getElementPickerThemeInfo: () => { + cf_worker.getElementPickerThemeInfo() + } } // When the picker is activated, it eats all pointer events and takes up the @@ -275,7 +279,6 @@ const elementPickerOnKeydown = (event: KeyboardEvent): void => { } const elementPickerViewportChanged = () => { - endLongPress() recalculateAndSendTargets(null) } @@ -300,7 +303,9 @@ const attachElementPicker = () => { // Will be resolved by webpack to the file content. // It's a trusted content so it's safe to use innerHTML. // eslint-disable-next-line no-unsanitized/property - shadowRoot.innerHTML = require('./element_picker.html') + shadowRoot.innerHTML = isAndroid ? + require('./android_element_picker.html') : + require('./element_picker.html') const pickerCSSStyle: string = [ 'background: transparent', @@ -329,11 +334,10 @@ const attachElementPicker = () => { pickerDiv.setAttribute('style', pickerCSSStyle) document.documentElement.appendChild(pickerDiv) - // Setup listeners to assist element picker document.addEventListener('keydown', elementPickerOnKeydown, true) window.addEventListener('resize', elementPickerViewportChanged) - document.addEventListener('scroll', elementPickerViewportChanged) + window.addEventListener('scroll', elementPickerViewportChanged) return shadowRoot } @@ -348,12 +352,10 @@ interface TargetRect { class Target { element: Element rectElem: Element - isMarked: boolean coord: TargetRect constructor (elem: Element) { this.element = elem - this.isMarked = false this.coord = targetRectFromElement(this.element) } @@ -364,6 +366,7 @@ class Target { class TargetsCollection { targets: Target[] = [] + togglePicker: ((val: boolean) => void) | null = null reset(elems: Element[]) { this.targets.length = 0 elems.forEach((elem: Element) => { @@ -371,23 +374,19 @@ class TargetsCollection { }); } - toggleElementMark(rectElem: Element): boolean | null { - const targetToMark = this.targets.find(t => t.rectElem === rectElem) - if (targetToMark) { - targetToMark.isMarked = !targetToMark.isMarked - return targetToMark.isMarked - } - return null - } - getXpathsForMarked(): string[] { - const markedTargets = this.targets.filter(t => t.isMarked) - return markedTargets.map((el) => getElementXpath(el.element)) + 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() { @@ -405,14 +404,9 @@ const targetRectFromElement = (elem: Element): TargetRect => { } } -let pressTimer: ReturnType; let lastHoveredElem: HTMLElement | null = null const targetedElems = new TargetsCollection -const endLongPress = () => { - clearTimeout(pressTimer); // Clear the timer if the user releases early -} - const recalculateAndSendTargets = (elems: Element[] | null) => { if(elems) { targetedElems.reset(elems) @@ -456,9 +450,22 @@ const hideByXPath = (xpath: string): void => { } } -const getElementXpath = (element: Element): string | null => { - if (!(element instanceof Element)) { - return null +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 => { @@ -492,6 +499,10 @@ const getElementXpath = (element: Element): string | null => { 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 = [ @@ -541,13 +552,24 @@ 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 !== '') { try { - recalculateAndSendTargets( - Array.from(document.querySelectorAll(selector))) + recalculateAndSendTargets(getElementBySelector(selector)) } catch {} } return { @@ -569,15 +591,7 @@ const elementPickerUserModifiedRule = (selector: string) => { } } -const isSelectedElementChecked = (elem: HTMLElement) => { - if (!elem) { - return false - } - return elem.classList.contains('marked-target') -} - const launchElementPicker = (root: ShadowRoot) => { - const isAndroid = /(android)/i.test(navigator.userAgent) let hasSelectedTarget = false const btnShowRulesBox = root.getElementById('btnShowRulesBox') @@ -631,12 +645,39 @@ const launchElementPicker = (root: ShadowRoot) => { togglePopup(true) }) - const section = root.querySelector('section')! + const section = root.getElementById('main-section')! const togglePopup = (show: boolean) => { + if(isAndroid) { + if (show) { + createButton.classList.remove('block-button-disabled') + createButton.textContent = "Block Element" + } else { + createButton.classList.add('block-button-disabled') + createButton.textContent = "Select element you want to block" + } + return + } section.style.setProperty('opacity', show ? '1' : '0.2') } + targetedElems.togglePicker = togglePopup const slider = root.getElementById('sliderSpecificity') as HTMLInputElement + if (isAndroid) { + slider.style.display = 'none' + } + + const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); + const handleColorSchemeChange = () => { + api.getElementPickerThemeInfo() + }; + prefersDarkScheme.addEventListener('change', handleColorSchemeChange); + window.content_cosmetic.setTheme = (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)) + } + handleColorSchemeChange() const dispatchSelect = () => { const { isValid, selector } = elementPickerUserSelectedTarget( @@ -656,7 +697,7 @@ const launchElementPicker = (root: ShadowRoot) => { dispatchSelect() }) - const doubleOrLongClickEventHandler = (event: MouseEvent | TouchEvent) => { + const oneClickEventHandler = (event: MouseEvent | TouchEvent) => { let elem: Element | null = null if (event instanceof MouseEvent) { elem = elementFromFrameCoords(event.clientX, event.clientY) @@ -673,63 +714,25 @@ const launchElementPicker = (root: ShadowRoot) => { dispatchSelect() } - const oneClickEventHandler = (sourceEvent: MouseEvent) => { - const clickedElement = sourceEvent.target as HTMLElement - if (!clickedElement || !hasSelectedTarget) { - return - } - - if (targetedElems.toggleElementMark(clickedElement) === null) { - hasSelectedTarget = false - slider.value = '4' - togglePopup(false) - targetedElems.targets.length = 0 - highlightElements() - return - } - - if (!isSelectedElementChecked(clickedElement)) { - clickedElement.classList.add('marked-target') - clickedElement.classList.remove('target') - } else { - clickedElement.classList.remove('marked-target') - clickedElement.classList.add('target') - } - } - - const startLongPress = (event: MouseEvent | TouchEvent) => { - pressTimer = setTimeout(() => { - doubleOrLongClickEventHandler(event) - }, 800); - } - - if (isAndroid) { - svg.addEventListener('mousedown', startLongPress) - svg.addEventListener('mouseup', endLongPress) - svg.addEventListener('mouseleave', endLongPress) - - svg.addEventListener('touchstart', startLongPress) - svg.addEventListener('touchend', endLongPress) - svg.addEventListener('touchcancel', endLongPress) - } else { - svg.addEventListener('dblclick', doubleOrLongClickEventHandler) - } - svg.addEventListener('click', oneClickEventHandler) const createButton = root.getElementById('btnCreate')! createButton.addEventListener('click', () => { - const selectedXpaths = targetedElems.getXpathsForMarked() - if(selectedXpaths && selectedXpaths.length > 0) { - for(const expr of selectedXpaths) { - const rule = `:xpath(${expr})` - api.cosmeticFilterCreate(rule) - hideByXPath(expr) + 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() } - quitElementPicker() return } - const selector = rulesTextArea.value.trim() if (selector.length > 0) { api.cosmeticFilterCreate(selector) @@ -746,7 +749,6 @@ const launchElementPicker = (root: ShadowRoot) => { const manageButton = root.getElementById('btnManage')! manageButton.addEventListener('click', () => { api.cosmeticFilterManage(); - quitElementPicker() }) const toggleDisplay = (target: HTMLElement | null, @@ -801,7 +803,7 @@ const highlightElements = () => { // Use the same element, but add the target class which turns the // target rectangle orange const targetingArea = mask.cloneNode(false) as SVGRectElement - targetingArea.classList.add(target.isMarked ? 'marked-target' : 'target') + targetingArea.classList.add('target') target.rectElem = targetingArea svgFragment.appendChild(targetingArea) diff --git a/components/definitions/chromel.d.ts b/components/definitions/chromel.d.ts index b8a7a7d88fd2..3839fe57d0b8 100644 --- a/components/definitions/chromel.d.ts +++ b/components/definitions/chromel.d.ts @@ -224,6 +224,7 @@ declare namespace chrome.windows { declare namespace cf_worker { const addSiteCosmeticFilter: (selector: string) => void const manageCustomFilters: () => void + const getElementPickerThemeInfo: () => void } declare namespace chrome.test { diff --git a/components/definitions/global.d.ts b/components/definitions/global.d.ts index 851b700d3827..49b2a333ebba 100644 --- a/components/definitions/global.d.ts +++ b/components/definitions/global.d.ts @@ -72,7 +72,8 @@ declare global { fetchNewClassIdRulesThrottlingMs : number | undefined tryScheduleQueuePump: (() => void) proceduralActionFilters?: ProceduralActionFilter[] - hasProceduralActions: boolean + hasProceduralActions: boolean, + setTheme: (bgcolor: number) => void } } }