feat: Add Arc's boosts implementation, b=no-bug, c=boosts (#11071)

Co-authored-by: Mr. M <mr.m@tuta.com>
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
This commit is contained in:
fen4flo
2026-03-02 18:19:40 +01:00
committed by GitHub
parent 32b355595c
commit d459ad932d
69 changed files with 7762 additions and 21 deletions

View File

@@ -20,3 +20,5 @@ files:
translation: browser/browser/preferences/zen-preferences.ftl
- source: en-US/browser/browser/zen-folders.ftl
translation: browser/browser/zen-folders.ftl
- source: en-US/browser/browser/zen-boosts.ftl
translation: browser/browser/zen-boosts.ftl

View File

@@ -0,0 +1,58 @@
# 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 http://mozilla.org/MPL/2.0/.
zen-boost-edit-rename =
.label = Rename Boost
zen-boost-edit-shuffle =
.label = Shuffle Vibes
zen-boost-edit-reset =
.label = Reset All Edits
zen-boost-edit-delete =
.label = Delete Boost
zen-boost-size = Size
zen-boost-zap = Zap
zen-boost-code = Code
zen-boost-back = Back
zen-boost-shuffle =
.tooltiptext = Shuffle Boost Settings
zen-boost-invert =
.tooltiptext = Smart Invert Colors
zen-boost-controls =
.tooltiptext = Advanced Color Controls
zen-boost-disable =
.tooltiptext = Disable Color Adjustments
zen-boost-text-case-toggle =
.tooltiptext = Toggle Text Case
zen-boost-css-picker =
.tooltiptext = Pick Selector
zen-boost-css-inspector =
.tooltiptext = Open Inspector
zen-bootst-color-contrast = Contrast
zen-bootst-color-brightness = Brightness
zen-bootst-color-original-saturation = Original Saturation
zen-add-zap-helper = Click elements on the page to <b>Zap</b> them
zen-remove-zap-helper = ← Click to Unzap
zen-select-this = Insert selector for this
zen-select-related = Insert selector for related
zen-select-cancel = Cancel
zen-zap-this = Zap this
zen-zap-related = Zap all related elements
zen-zap-cancel = Cancel
zen-zap-done = Done
zen-unzap-tooltip =
{
$elementCount ->
[0] No elements zapped
[1] { $elementCount } element zapped
*[other] { $elementCount } elements zapped
}
zen-boost-save =
.tooltiptext = Export Boost
zen-boost-load =
.tooltiptext = Import Boost
zen-panel-ui-boosts-exported-message = Boost exported!
zen-site-data-boosts = Boosts
zen-site-data-create-boost =
.tooltiptext = Create new boost
zen-boost-rename-boost-prompt = Rename Boost?

9
prefs/zen/boosts.yaml Normal file
View File

@@ -0,0 +1,9 @@
# 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 http://mozilla.org/MPL/2.0/.
- name: zen.boosts.enabled
value: "@IS_TWILIGHT@"
- name: zen.boosts.dissolve-on-zap
value: true

View File

@@ -18,4 +18,5 @@
#include ../../../zen/images/jar.inc.mn
#include ../../../zen/vendor/jar.inc.mn
#include ../../../zen/fonts/jar.inc.mn
#include ../../../zen/boosts/jar.inc.mn
#include ../../../zen/live-folders/jar.inc.mn

View File

@@ -9,5 +9,6 @@
<link rel="localization" href="browser/zen-menubar.ftl"/>
<link rel="localization" href="browser/zen-vertical-tabs.ftl"/>
<link rel="localization" href="browser/zen-folders.ftl"/>
<link rel="localization" href="browser/zen-boosts.ftl"/>
<link rel="localization" href="browser/zen-live-folders.ftl"/>
</linkset>

View File

@@ -75,6 +75,18 @@
data-l10n-id="unified-extensions-manage-extensions"
hidden="true" />
</vbox>
<vbox class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="zen-site-data-boosts" flex="1" />
#if 0
<!-- This can later be used to open Zen Library on the Boosts tab to display a list of all boosts -->
<label data-l10n-id="zen-generic-more" id="zen-site-data-boosts-more" hidden="true" />
#endif
</hbox>
<vbox id="zen-site-data-boost-list">
<!-- settings will be inserted here -->
</vbox>
</vbox>
<vbox class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="zen-site-data-settings" flex="1" />
@@ -87,6 +99,8 @@
<hbox id="zen-site-data-footer">
<toolbarbutton id="zen-site-data-security-info"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-boost" data-l10n-id="zen-site-data-create-boost"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-actions"
class="subviewbutton zen-interactive-button"
closemenu="none"

View File

@@ -8,7 +8,8 @@
#zen-welcome-start-button,
.zen-toast button,
.zen-current-workspace-indicator-chevron,
.pinned-tabs-container-separator toolbarbutton {
.pinned-tabs-container-separator toolbarbutton,
.zen-permission-popup-boost-editor-button {
-moz-context-properties: fill, fill-opacity !important;
fill: currentColor !important;
}
@@ -39,10 +40,21 @@
.close-icon,
.zen-glance-sidebar-close,
.zen-theme-picker-custom-list-item-remove,
#appMenu-quit-button2 {
#appMenu-quit-button2,
#zen-boost-delete {
list-style-image: url("close.svg") !important;
}
#PanelUI-zen-emojis-picker-none,
#zen-emojis-picker-none {
list-style-image: url("trash.svg");
}
#PanelUI-zen-gradient-generator-color-remove,
#zen-gradient-generator-color-remove {
list-style-image: url("unpin.svg") !important;
}
#PanelUI-zen-emojis-picker-none {
list-style-image: url("trash.svg");
}
@@ -90,7 +102,8 @@
#appMenu-zoom-controls,
#PanelUI-zen-gradient-generator-color-add,
#zen-site-data-new-addon-button {
#zen-site-data-new-addon-button,
.zen-create-new-boost {
list-style-image: url("plus.svg") !important;
}
@@ -115,7 +128,8 @@
}
.zen-current-workspace-indicator-chevron,
#PanelUI-zen-gradient-generator-color-page-right {
#PanelUI-zen-gradient-generator-color-page-right,
.zen-permission-popup-boost-editor-button {
list-style-image: url("arrow-right.svg");
}
@@ -410,7 +424,8 @@
}
.panel-header > .subviewbutton-back,
#PanelUI-zen-gradient-generator-color-page-left {
#PanelUI-zen-gradient-generator-color-page-left,
#zen-boost-back {
list-style-image: url("arrow-left.svg") !important;
}
@@ -502,6 +517,27 @@
&:is([open], [starred]) image {
list-style-image: url("permissions-fill.svg");
}
&[boosting] image {
color: var(--color-accent-primary);
list-style-image: url("permissions-fill.svg");
}
position: relative;
}
@media not (prefers-reduced-motion: reduce) {
#zen-site-data-icon-button[boosting]::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
opacity: 1;
color: var(--color-accent-primary);
background: url("chrome://browser/content/zen-images/boost-indicator.svg") no-repeat;
transform: translateX(-20%);
z-index: 0;
pointer-events: none;
}
}
.geo-icon {
@@ -876,6 +912,31 @@
fill: currentColor;
}
#zen-site-data-boost {
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
border-radius: 6px;
appearance: none;
padding: 6px 10px 6px 10px;
position: relative;
list-style-image: url("paintbrush.svg");
& .toolbarbutton-text {
display: none;
}
@media -moz-pref("zen.boosts.enabled", false) {
display: none;
}
}
#zen-site-data-boost[boosting],
.boost-brush {
list-style-image: url("paintbrush-fill.svg");
}
#zen-site-data-security-info {
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
@@ -935,3 +996,79 @@
list-style-image: url("link.svg");
fill-opacity: 0.65;
}
#zen-boost-text-case-toggle {
list-style-image: url("text-title-case.svg");
}
#zen-boost-text-case-toggle[case-mode="uppercase"] {
list-style-image: url("text-uppercase.svg");
}
#zen-boost-text-case-toggle[case-mode="lowercase"] {
list-style-image: url("text-lowercase.svg");
}
#zen-boost-code {
list-style-image: url("brackets-curly.svg");
}
#zen-boost-zap {
list-style-image: url("bolt.svg");
}
#zen-boost-controls {
list-style-image: url("sliders.svg");
}
#zen-boost-invert {
list-style-image: url("lightbulb.svg");
}
#zen-boost-disable {
list-style-image: url("block.svg");
}
#zen-boost-close {
list-style-image: url("close.svg");
@media (-moz-platform: macos) {
list-style-image: url("close-filled-round.svg");
}
@media (-moz-platform: windows) {
list-style-image: none;
list-style: none;
display: inline-flex;
&::before {
font-family: "Segoe Fluent Icons", "Segoe MDL2 Assets";
content: "\e8bb";
font-size: 12px;
}
}
}
#zen-boost-magic-theme {
list-style-image: url("sparkles.svg");
}
#zen-boost-shuffle {
list-style-image: url("arrow-rotate-anticlockwise.svg");
}
#zen-boost-css-picker {
list-style-image: url("eyedropper.svg");
}
#zen-boost-css-inspector {
list-style-image: url("hammer.svg");
}
#zen-boost-save {
list-style-image: url("downloads.svg");
}
#zen-boost-load {
list-style-image: url("open.svg");
}

View File

@@ -7,20 +7,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -44,6 +51,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -53,12 +61,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -78,6 +88,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -110,13 +122,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -125,6 +143,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)
@@ -136,20 +155,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -173,6 +199,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -182,12 +209,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -207,6 +236,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -239,13 +270,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -254,6 +291,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)
@@ -265,20 +303,27 @@
* skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg)
* skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg)
* skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg)
* skin/classic/browser/zen-icons/arrow-rotate-anticlockwise.svg (../shared/zen-icons/nucleo/arrow-rotate-anticlockwise.svg)
* skin/classic/browser/zen-icons/arrow-up.svg (../shared/zen-icons/nucleo/arrow-up.svg)
* skin/classic/browser/zen-icons/autoplay-media-blocked.svg (../shared/zen-icons/nucleo/autoplay-media-blocked.svg)
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
* skin/classic/browser/zen-icons/camera.svg (../shared/zen-icons/nucleo/camera.svg)
* skin/classic/browser/zen-icons/canvas-blocked.svg (../shared/zen-icons/nucleo/canvas-blocked.svg)
* skin/classic/browser/zen-icons/canvas.svg (../shared/zen-icons/nucleo/canvas.svg)
* skin/classic/browser/zen-icons/chevron.svg (../shared/zen-icons/nucleo/chevron.svg)
* skin/classic/browser/zen-icons/close-filled-round.svg (../shared/zen-icons/nucleo/close-filled-round.svg)
* skin/classic/browser/zen-icons/close.svg (../shared/zen-icons/nucleo/close.svg)
* skin/classic/browser/zen-icons/container-tab.svg (../shared/zen-icons/nucleo/container-tab.svg)
* skin/classic/browser/zen-icons/cookies-fill.svg (../shared/zen-icons/nucleo/cookies-fill.svg)
@@ -302,6 +347,7 @@
* skin/classic/browser/zen-icons/extension-blocked.svg (../shared/zen-icons/nucleo/extension-blocked.svg)
* skin/classic/browser/zen-icons/extension-fill.svg (../shared/zen-icons/nucleo/extension-fill.svg)
* skin/classic/browser/zen-icons/extension.svg (../shared/zen-icons/nucleo/extension.svg)
* skin/classic/browser/zen-icons/eyedropper.svg (../shared/zen-icons/nucleo/eyedropper.svg)
* skin/classic/browser/zen-icons/face-sun.svg (../shared/zen-icons/nucleo/face-sun.svg)
* skin/classic/browser/zen-icons/folder.svg (../shared/zen-icons/nucleo/folder.svg)
* skin/classic/browser/zen-icons/forget.svg (../shared/zen-icons/nucleo/forget.svg)
@@ -311,12 +357,14 @@
* skin/classic/browser/zen-icons/geo-blocked.svg (../shared/zen-icons/nucleo/geo-blocked.svg)
* skin/classic/browser/zen-icons/geo-fill.svg (../shared/zen-icons/nucleo/geo-fill.svg)
* skin/classic/browser/zen-icons/geo.svg (../shared/zen-icons/nucleo/geo.svg)
* skin/classic/browser/zen-icons/hammer.svg (../shared/zen-icons/nucleo/hammer.svg)
* skin/classic/browser/zen-icons/heart-circle-fill.svg (../shared/zen-icons/nucleo/heart-circle-fill.svg)
* skin/classic/browser/zen-icons/help.svg (../shared/zen-icons/nucleo/help.svg)
* skin/classic/browser/zen-icons/history.svg (../shared/zen-icons/nucleo/history.svg)
* skin/classic/browser/zen-icons/home.svg (../shared/zen-icons/nucleo/home.svg)
* skin/classic/browser/zen-icons/info.svg (../shared/zen-icons/nucleo/info.svg)
* skin/classic/browser/zen-icons/library.svg (../shared/zen-icons/nucleo/library.svg)
* skin/classic/browser/zen-icons/lightbulb.svg (../shared/zen-icons/nucleo/lightbulb.svg)
* skin/classic/browser/zen-icons/link.svg (../shared/zen-icons/nucleo/link.svg)
* skin/classic/browser/zen-icons/mail.svg (../shared/zen-icons/nucleo/mail.svg)
* skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/nucleo/manage.svg)
@@ -336,6 +384,8 @@
* skin/classic/browser/zen-icons/new-tab-image.svg (../shared/zen-icons/nucleo/new-tab-image.svg)
* skin/classic/browser/zen-icons/open.svg (../shared/zen-icons/nucleo/open.svg)
* skin/classic/browser/zen-icons/page-portrait.svg (../shared/zen-icons/nucleo/page-portrait.svg)
* skin/classic/browser/zen-icons/paintbrush-fill.svg (../shared/zen-icons/nucleo/paintbrush-fill.svg)
* skin/classic/browser/zen-icons/paintbrush.svg (../shared/zen-icons/nucleo/paintbrush.svg)
* skin/classic/browser/zen-icons/palette.svg (../shared/zen-icons/nucleo/palette.svg)
* skin/classic/browser/zen-icons/passwords.svg (../shared/zen-icons/nucleo/passwords.svg)
* skin/classic/browser/zen-icons/permissions-fill.svg (../shared/zen-icons/nucleo/permissions-fill.svg)
@@ -368,13 +418,19 @@
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
* skin/classic/browser/zen-icons/split.svg (../shared/zen-icons/nucleo/split.svg)
* skin/classic/browser/zen-icons/square-wand-sparkle.svg (../shared/zen-icons/nucleo/square-wand-sparkle.svg)
* skin/classic/browser/zen-icons/tab-audio-blocked-small.svg (../shared/zen-icons/nucleo/tab-audio-blocked-small.svg)
* skin/classic/browser/zen-icons/tab-audio-muted-small.svg (../shared/zen-icons/nucleo/tab-audio-muted-small.svg)
* skin/classic/browser/zen-icons/tab-audio-playing-small.svg (../shared/zen-icons/nucleo/tab-audio-playing-small.svg)
* skin/classic/browser/zen-icons/tab.svg (../shared/zen-icons/nucleo/tab.svg)
* skin/classic/browser/zen-icons/text-lowercase.svg (../shared/zen-icons/nucleo/text-lowercase.svg)
* skin/classic/browser/zen-icons/text-size.svg (../shared/zen-icons/nucleo/text-size.svg)
* skin/classic/browser/zen-icons/text-title-case.svg (../shared/zen-icons/nucleo/text-title-case.svg)
* skin/classic/browser/zen-icons/text-uppercase.svg (../shared/zen-icons/nucleo/text-uppercase.svg)
* skin/classic/browser/zen-icons/tool-profiler.svg (../shared/zen-icons/nucleo/tool-profiler.svg)
* skin/classic/browser/zen-icons/tracking-protection-fill.svg (../shared/zen-icons/nucleo/tracking-protection-fill.svg)
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
@@ -383,6 +439,7 @@
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
* skin/classic/browser/zen-icons/wand-sparkle.svg (../shared/zen-icons/nucleo/wand-sparkle.svg)
* skin/classic/browser/zen-icons/window.svg (../shared/zen-icons/nucleo/window.svg)
* skin/classic/browser/zen-icons/xr-blocked.svg (../shared/zen-icons/nucleo/xr-blocked.svg)
* skin/classic/browser/zen-icons/xr-fill.svg (../shared/zen-icons/nucleo/xr-fill.svg)

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="polyline1"
d="M 1.88,3.305 2.288,6.25 5.232,5.843 M 3,13.071 c 1.304,1.919 3.505,3.179 6,3.179 4.004,0 7.25,-3.246 7.25,-7.25 0,-4.004 -3.246,-7.25 -7.25,-7.25 -3.031,0 -5.627,1.86 -6.71,4.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="circle1"
d="M 16.25,9 A 7.25,7.25 0 0 1 9,16.25 7.25,7.25 0 0 1 1.75,9 7.25,7.25 0 0 1 9,1.75 7.25,7.25 0 0 1 16.25,9 Z M 14.127,3.8729999 3.8729999,14.127" />
</g>
</svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 18 18"><g stroke-linecap="round" stroke-width="1.5" fill="none" stroke="context-fill" stroke-opacity="context-fill-opacity" stroke-linejoin="round" class="nc-icon-wrapper"><polyline points="14 6.25 16.25 4 14 1.75" data-color="color-2"></polyline><path d="M6.596,2.75h-1.846c-1.105,0-2,.896-2,2V13.25c0,1.104,.895,2,2,2H13.25c1.105,0,2-.896,2-2v-4.846"></path><polyline points="11 6.25 8.75 4 11 1.75" data-color="color-2"></polyline></g></svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 14.7505,7.25 H 9.49905 L 9.81065,1.9868 C 9.82325,1.7732 9.55085,1.6734 9.42255,1.8446 l -6.3729,8.5055 C 2.92615,10.5149 3.04376,10.75 3.24976,10.75 h 5.25139 l -0.3116,5.2632 c -0.0126,0.2136 0.2598,0.3134 0.3881,0.1422 L 14.9506,7.6499 C 15.0741,7.4851 14.9565,7.25 14.7505,7.25 Z" />
</g>
</svg>

View File

@@ -0,0 +1,28 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 14.7 16.72"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g3">
<path
id="circle3"
class="cls-1"
d="M 3.4,14.17 A 0.86000001,0.86000001 0 0 1 2.54,15.03 0.86000001,0.86000001 0 0 1 1.6799999,14.17 0.86000001,0.86000001 0 0 1 2.54,13.31 0.86000001,0.86000001 0 0 1 3.4,14.17 Z M 10.53,11.6 C 11.76,10.89 13.88,8.39 14.12,7.61 14.46,6.52 13.63,5.97 12.86,5.52 12.22,5.13 10.95,6.95 10.25,7.72 m -0.01,0 C 10.79,7.17 11.75,5.7 11.75,5.18 11.75,2.67 6.38,-0.99 6.8,1.16 7.09,2.63 5.72,5.24 5.17,6.11 M 5.08,6.35 C 4.76,6.03 4.24,6.02 3.91,6.33 L 3.22,6.98 C 2.88,7.3 2.87,7.85 3.2,8.18 l 1.69,1.69 -3.54,2.65 c -1.02,0.73 -1.15,2.2 -0.27,3.09 v 0 c 0.87,0.88 2.31,0.79 3.06,-0.18 l 2.71,-3.63 1.6,1.6 c 0.33,0.33 0.85,0.33 1.18,0 l 0.64,-0.63 c 0.33,-0.33 0.33,-0.86 0,-1.19 L 5.06,6.33 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="m 11.75,15.25 h 1 c 1.105,0 2,-0.895 2,-2 V 10.625 C 14.75,9.728 15.478,9 16.375,9 15.478,9 14.75,8.272 14.75,7.375 V 4.75 c 0,-1.105 -0.895,-2 -2,-2 h -1 m -5.5,12.5 h -1 c -1.105,0 -2,-0.895 -2,-2 V 10.625 C 3.25,9.728 2.522,9 1.625,9 2.522,9 3.25,8.272 3.25,7.375 V 4.75 c 0,-1.105 0.895,-2 2,-2 h 1" />
</g>
</svg>

View File

@@ -0,0 +1,28 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="20px"
viewBox="0 -960 960 960"
width="20px"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="context-fill"
stroke="none"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="m 480,-424 116,116 q 11,11 28,11 17,0 28,-11 11,-11 11,-28 0,-17 -11,-28 L 536,-480 652,-596 q 11,-11 11,-28 0,-17 -11,-28 -11,-11 -28,-11 -17,0 -28,11 L 480,-536 364,-652 q -11,-11 -28,-11 -17,0 -28,11 -11,11 -11,28 0,17 11,28 l 116,116 -116,116 q -11,11 -11,28 0,17 11,28 11,11 28,11 17,0 28,-11 z m 0,344 Q 397,-80 324,-111.5 251,-143 197,-197 143,-251 111.5,-324 80,-397 80,-480 q 0,-83 31.5,-156 31.5,-73 85.5,-127 54,-54 127,-85.5 73,-31.5 156,-31.5 83,0 156,31.5 73,31.5 127,85.5 54,54 85.5,127 31.5,73 31.5,156 0,83 -31.5,156 -31.5,73 -85.5,127 -54,54 -127,85.5 Q 563,-80 480,-80 Z" />
</g>
</svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" stroke="context-fill" stroke-opacity="context-fill-opacity"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m9.235 5.735-6.107 6.107a2.142 2.142 0 1 0 3.03 3.03l6.107-6.107M3.128 14.872 1.75 16.25"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m12.265 8.765 2.857-2.857a2.142 2.142 0 1 0-3.03-3.03L9.235 5.735M7.75 4.25l6 6"/></svg>

View File

@@ -0,0 +1,5 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" stroke="context-fill" stroke-opacity="context-fill-opacity"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.529 8.721-6.563 6.563a1.59 1.59 0 0 1-2.25 0h0a1.59 1.59 0 0 1 0-2.25l6.563-6.563"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m16.207 8.944-1.521 1.521a1 1 0 0 1-1.414 0l-7.53-7.531L6.676 2l4.191.493a1 1 0 0 1 .59.286l4.75 4.75a1 1 0 0 1 0 1.414Z"/></svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 14,6.75 C 14,3.637 11.154,1.188 7.922,1.863 5.99,2.266 4.447,3.856 4.088,5.796 3.654,8.14 4.859,10.255 6.75,11.211 v 3.039 c 0,1.105 0.895,2 2,2 h 0.5 c 1.105,0 2,-0.895 2,-2 V 11.211 C 12.88,10.387 14,8.701 14,6.75 Z m -7.25,6.5 h 4.5" />
</g>
</svg>

View File

@@ -0,0 +1,27 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="16px"
height="16px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="M 8.97,9.291 C 8.301,8.62 7.416,8.25 6.476,8.25 H 6.47 C 5.534,8.252 4.653,8.622 3.99,9.293 c -0.952,0.962 -1.119,1.933 -1.252,2.712 -0.149,0.869 -0.247,1.442 -1.347,2.043 -0.266,0.145 -0.418,0.435 -0.387,0.736 0.032,0.301 0.241,0.554 0.53,0.641 1.058,0.318 2.185,0.58 3.306,0.58 1.438,0 2.867,-0.43 4.13,-1.724 1.371,-1.375 1.371,-3.614 0,-4.989 z m 7.348,-7.36 c -0.79,-0.789 -2.166,-0.79 -2.957,0 L 8.216,7.076 c 0.673,0.25 1.294,0.631 1.817,1.156 0.525,0.527 0.897,1.145 1.14,1.802 l 5.146,-5.146 c 0.815,-0.815 0.815,-2.142 0,-2.958 z" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="16px"
height="16px"
viewBox="0 0 18 18"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="path2"
d="m 1.75,14.706 c 2.703,0.812 4.896,0.88 6.689,-0.955 C 9.52,12.666 9.52,10.906 8.439,9.82 7.358,8.734 5.613,8.718 4.523,9.82 2.75,11.612 4.298,13.314 1.75,14.706 Z M 6.956,9.044 13.534,2.466 c 0.621,-0.621 1.629,-0.621 2.25,0 v 0 c 0.621,0.621 0.621,1.629 0,2.25 l -6.578,6.578" />
</g>
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g3" />
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg8"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g8">
<path
id="circle8"
d="M 8.5,14.25 A 1.75,1.75 0 0 1 6.75,16 1.75,1.75 0 0 1 5,14.25 1.75,1.75 0 0 1 6.75,12.5 1.75,1.75 0 0 1 8.5,14.25 Z m 0,-10.5 A 1.75,1.75 0 0 1 6.75,5.5 1.75,1.75 0 0 1 5,3.75 1.75,1.75 0 0 1 6.75,2 1.75,1.75 0 0 1 8.5,3.75 Z M 12.75,9 A 1.75,1.75 0 0 1 11,10.75 1.75,1.75 0 0 1 9.25,9 1.75,1.75 0 0 1 11,7.25 1.75,1.75 0 0 1 12.75,9 Z m 3.5,5.25 h -5 M 5,14.25 H 1.75 m 14.5,-10.5 h -5 M 5,3.75 H 1.75 M 1.75,9 H 9 m 6.25,0 h 1" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg7"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g7">
<path
id="path7"
d="m 6.25,9.75 2,2 m -1.74799,-9 H 4.75 c -1.105,0 -2,0.896 -2,2 V 9.00732 M 8.992,15.25 h 4.258 c 1.105,0 2,-0.896 2,-2 V 10.616 M 7.95696,8.04302 2.04396,13.956 c -0.39,0.39 -0.39,1.023 0,1.414 l 0.586,0.585 c 0.39,0.391 1.024,0.391 1.414,0 l 5.913,-5.912 c 0.39004,-0.39108 0.39004,-1.02402 0,-1.41502 l -0.586,-0.58496 c -0.39,-0.3911 -1.023,-0.3911 -1.414,0 z M 15.25,2.5 C 15.664,2.5 16,2.164 16,1.75 16,1.336 15.664,1 15.25,1 14.836,1 14.5,1.336 14.5,1.75 c 0,0.414 0.336,0.75 0.75,0.75 z M 16.659,5.98996 15.396,5.56894 14.975,4.30606 c -0.137,-0.408 -0.812,-0.408 -0.949,0 l -0.421,1.26288 -1.263,0.42102 c -0.204,0.068 -0.342,0.259 -0.342,0.474 0,0.215 0.138,0.406 0.342,0.474 l 1.263,0.42102 0.421,1.263 c 0.068,0.204 0.26,0.34198 0.475,0.34198 0.215,0 0.406,-0.13798 0.475,-0.34198 l 0.421,-1.263 1.263,-0.42102 c 0.204,-0.068 0.342,-0.259 0.342,-0.474 0,-0.215 -0.139,-0.406 -0.343,-0.474 z M 11.744,2.49201 10.798,2.17701 10.482,1.22999 c -0.102,-0.306002 -0.609,-0.306002 -0.711,0 L 9.45499,2.17701 8.509,2.49201 c -0.153,0.051 -0.25699,0.19402 -0.25699,0.35602 0,0.162 0.10399,0.30496 0.25699,0.35596 l 0.94599,0.315 0.31601,0.94702 c 0.051,0.153 0.19401,0.25598 0.355,0.25598 0.161,0 0.305,-0.10398 0.355,-0.25598 l 0.316,-0.94702 0.946,-0.315 C 11.896,3.15299 12,3.01003 12,2.84803 12,2.68603 11.897,2.54301 11.744,2.49201 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g3">
<path
id="path3"
d="m 14.75,6.75 v 6.303 c 0,1.214 -0.984,2.197 -2.197,2.197 v 0 c -0.728,0 -1.373,-0.354 -1.773,-0.899 M 12.45,6.75 h 0.204999 C 13.81563,6.75 14.75,7.68437 14.75,8.845 v 0.81 c 0,1.16063 -0.93437,2.095 -2.095001,2.095 H 12.45 c -1.16063,0 -2.095,-0.93437 -2.095,-2.095 v -0.81 c 0,-1.16063 0.93437,-2.095 2.095,-2.095 z M 7.25,9.5 c 0,1.518 -1.232,2.75 -2.75,2.75 H 4.3386 C 3.5993,12.25 3,11.6507 3,10.9114 v 0 C 3,10.2678 3.458,9.7153 4.0904,9.596 L 6.3677,9.1961 C 6.9062,9.0945 7.2576,8.581 7.1689,8.0499 M 3.234,7.234 C 3.72,6.711 4.412,6.5 5.115,6.5 v 0 c 1.179,0 2.134,0.956 2.134,2.134 v 3.616" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2">
<path
id="line2"
d="M 1.38,11.75 H 5.7160001 M 6.346,13.25 3.616,6.75 H 3.48 l -2.73,6.5 m 9.49,-2.5 h 6.040001 M 17.25,13.25 13.565,3.75 h -0.61 l -3.685,9.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="m 15.75,6.75 v 6.303 c 0,1.214 -0.984,2.197 -2.197,2.197 v 0 c -0.728,0 -1.373,-0.354 -1.773,-0.899 M 13.45,6.75 h 0.204999 C 14.81563,6.75 15.75,7.68437 15.75,8.845 v 0.81 c 0,1.16063 -0.93437,2.095 -2.095001,2.095 H 13.45 c -1.16063,0 -2.095,-0.93437 -2.095,-2.095 v -0.81 c 0,-1.16063 0.93437,-2.095 2.095,-2.095 z M 2.5899999,10.25 H 8.2279997 M 9.068,12.25 5.498,3.75 H 5.32 l -3.57,8.5" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g1">
<path
id="path1"
d="M 16.53,6.048 C 16.201,5.615 15.361,4.75 13.905,4.75 c -2.136,0 -3.655,1.522 -3.655,4.304 0,2.444 1.492,4.196 3.439,4.196 1.125,0 3.266,-0.173 3.266,-3.958 H 13.797 M 1.84,11.25 H 7.4780002 M 8.318,13.25 4.748,4.75 H 4.57 L 1,13.25" />
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# 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 http://mozilla.org/MPL/2.0/.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
x="0px"
y="0px"
width="18px"
height="18px"
viewBox="0 0 18 18"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5" />
<g
stroke-linecap="round"
stroke-width="1.5"
fill="none"
stroke="context-fill"
stroke-opacity="context-fill-opacity"
stroke-linejoin="round"
class="nc-icon-wrapper"
id="g2" />
<g
fill="context-fill"
fill-opacity="context-fill-opacity"
class="nc-icon-wrapper"
id="g5">
<path
id="path5"
d="M 9.25,2.5 C 9.664,2.5 10,2.16 10,1.75 10,1.34 9.664,1 9.25,1 8.836,1 8.5,1.34 8.5,1.75 8.5,2.16 8.836,2.5 9.25,2.5 Z m 7.408,9.49 -1.263,-0.42 -0.421,-1.2599 c -0.137,-0.41004 -0.812,-0.41004 -0.949,0 l -0.421,1.2599 -1.263,0.42 c -0.204,0.0701 -0.342,0.26 -0.342,0.47 0,0.2201 0.138,0.4099 0.342,0.4799 l 1.263,0.4201 0.421,1.26 c 0.068,0.21 0.26,0.34 0.475,0.34 0.215,0 0.406,-0.13 0.475,-0.34 l 0.421,-1.26 1.263,-0.4201 c 0.204,-0.0699 0.342,-0.2598 0.342,-0.4799 0,-0.21 -0.139,-0.3999 -0.343,-0.47 z M 7.24297,3.48999 6.29697,3.18006 5.98097,2.22998 c -0.102,-0.3099 -0.609,-0.3099 -0.711,0 L 4.95396,3.18006 4.00797,3.48999 c -0.153,0.05 -0.25699,0.19999 -0.25699,0.35999 0,0.16 0.10399,0.29997 0.25699,0.34997 L 4.95396,4.52014 5.26997,5.4701 c 0.051,0.15 0.194,0.25 0.355,0.25 0.161,0 0.305,-0.1 0.355,-0.25 l 0.316,-0.94996 0.946,-0.32019 c 0.153,-0.0499 0.25699,-0.18987 0.25699,-0.34997 0,-0.1601 -0.10299,-0.30989 -0.25599,-0.35999 z m 3.14403,1.87 2.25,2.25 M 12.5717,2.92528 2.91893,12.583 c -0.3899,0.39 -0.3903,1.0221 -0.0011,1.4127 l 1.08521,1.0892 c 0.391,0.39 1.02399,0.39 1.41499,0 L 15.0701,5.42687 c 0.3898,-0.3901 0.3903,-1.02212 0.0011,-1.41272 L 13.9874,2.92638 C 13.597,2.53458 12.9627,2.53408 12.5717,2.92528 Z" />
</g>
</svg>

View File

@@ -0,0 +1,31 @@
diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h
index 42e3b22a366db0a8c470b458696d81e4bdbd537c..5d82dae436f502fa087f94f6baa7a280736d0b3e 100644
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -264,6 +264,8 @@ struct EmbedderColorSchemes {
FIELD(IsInBFCache, bool) \
FIELD(HasRestoreData, bool) \
FIELD(SessionStoreEpoch, uint32_t) \
+ FIELD(ZenBoostsData, nscolor) \
+ FIELD(IsZenBoostsInverted, bool) \
/* Whether we can execute scripts in this BrowsingContext. Has no effect \
* unless scripts are also allowed in the parent WindowContext. */ \
FIELD(AllowJavascript, bool) \
@@ -665,6 +667,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
bool FullscreenAllowed() const;
+ auto ZenBoostsData() const { return GetZenBoostsData(); }
+ auto IsZenBoostsInverted() const { return GetIsZenBoostsInverted(); }
float FullZoom() const { return GetFullZoom(); }
float TextZoom() const { return GetTextZoom(); }
@@ -1233,6 +1237,8 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {
}
void DidSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aOldValue);
+ void DidSet(FieldIndex<IDX_ZenBoostsData>, nscolor aOldValue);
+ void DidSet(FieldIndex<IDX_IsZenBoostsInverted>, bool aOldValue);
using CanSetResult = syncedcontext::CanSetResult;

View File

@@ -0,0 +1,14 @@
diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl
index 3bb17157c1dba1ca75ad9cfee2c21fcafb2dd0ee..a43f8aa1e4c808418f1cc7ccc69ced12c9ebfef0 100644
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -169,6 +169,9 @@ interface BrowsingContext {
[SetterThrows] attribute float textZoom;
+ [SetterThrows] attribute long zenBoostsData;
+ [SetterThrows] attribute boolean isZenBoostsInverted;
+
// Override the dots-per-CSS-pixel scaling factor in this BrowsingContext
// and all of its descendants. May only be set on the top BC, and should
// only be set from the parent process.

View File

@@ -0,0 +1,21 @@
diff --git a/gfx/layers/AnimationInfo.cpp b/gfx/layers/AnimationInfo.cpp
index dd67af463d383dd98da80ce44f41b395e4a9a7cf..9da3849630a80fea2b254659a218e5ed38b44bc0 100644
--- a/gfx/layers/AnimationInfo.cpp
+++ b/gfx/layers/AnimationInfo.cpp
@@ -16,6 +16,7 @@
#include "mozilla/MotionPathUtils.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsIContent.h"
#include "nsLayoutUtils.h"
#include "nsRefreshDriver.h"
@@ -344,7 +345,7 @@ static void SetAnimatable(NonCustomCSSPropertyId aProperty,
// resolve currentColor at this moment.
nscolor foreground =
aFrame->Style()->GetVisitedDependentColor(&nsStyleText::mColor);
- aAnimatable = aAnimationValue.GetColor(foreground);
+ aAnimatable = zen::nsZenBoostsBackend::FilterColorFromPresContext(aAnimationValue.GetColor(foreground), aFrame->PresContext());
break;
}
case eCSSProperty_opacity:

View File

@@ -0,0 +1,21 @@
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
index dd4df91b14137c2aa1ffd978faf1f4c91e234f8a..99976e9416d8a5b814bbb3a5421899f450ceeee5 100644
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -136,6 +136,7 @@
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/WebRenderUserData.h"
#include "mozilla/layout/ScrollAnchorContainer.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsAnimationManager.h"
#include "nsAutoLayoutPhase.h"
#include "nsCOMArray.h"
@@ -5546,7 +5547,7 @@ nscolor PresShell::GetDefaultBackgroundColorToDraw() const {
if (!mPresContext) {
return NS_RGB(255, 255, 255);
}
- return mPresContext->DefaultBackgroundColor();
+ return zen::nsZenBoostsBackend::FilterColorFromPresContext(mPresContext->DefaultBackgroundColor(), mPresContext);
}
void PresShell::UpdateCanvasBackground() {

View File

@@ -0,0 +1,33 @@
diff --git a/layout/generic/ViewportFrame.cpp b/layout/generic/ViewportFrame.cpp
index ea018fff9f549cd8e07b20f4c8073391a6fdf40d..acb9428cf2276ed04064484600024d9e2c7e2bb7 100644
--- a/layout/generic/ViewportFrame.cpp
+++ b/layout/generic/ViewportFrame.cpp
@@ -24,6 +24,7 @@
#include "nsLayoutUtils.h"
#include "nsPlaceholderFrame.h"
#include "nsSubDocumentFrame.h"
+#include "mozilla/nsZenBoostsBackend.h"
using namespace mozilla;
@@ -299,15 +300,20 @@ ViewportFrame::BuildDisplayListForViewTransitionsAndNACTopLayer(
}
}
+ bool isAnonContent = false;
if (dom::Element* container = doc->GetCustomContentContainer()) {
if (nsIFrame* frame = container->GetPrimaryFrame()) {
MOZ_ASSERT(frame->StyleDisplay()->mTopLayer != StyleTopLayer::None,
"ua.css should ensure this");
MOZ_ASSERT(frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW));
+ isAnonContent = frame->ContentIsRootOfNativeAnonymousSubtree();
BuildDisplayListForTopLayerFrame(aBuilder, frame, &topLayerList);
}
}
+ if (auto zenBackend = zen::nsZenBoostsBackend::GetInstance()) {
+ zenBackend->mCurrentFrameIsAnonymousContent = isAnonContent;
+ }
return MaybeWrapTopLayerList(
aBuilder, uint16_t(TopLayerIndex::ViewTransitionsAndAnonymousContent),
topLayerList);

View File

@@ -0,0 +1,23 @@
diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp
index 3f9aea06257f162ded7d7cf8b32fc6597383cf27..099e825c251d4edf99b41f59160e276f5325217d 100644
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -82,6 +82,7 @@
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/WebRenderMessages.h"
#include "mozilla/layers/WebRenderScrollData.h"
+#include "mozilla/nsZenBoostsBackend.h"
#include "nsCSSProps.h"
#include "nsCSSRendering.h"
#include "nsCSSRenderingGradients.h"
@@ -1212,6 +1213,10 @@ void nsDisplayListBuilder::EnterPresShell(const nsIFrame* aReferenceFrame,
docShell->GetWindowDraggingAllowed(&mWindowDraggingAllowed);
}
+ if (auto zenBackend = zen::nsZenBoostsBackend::GetInstance(); zenBackend && !mIsInChromePresContext) {
+ zenBackend->onPresShellEntered(pc->Document());
+ }
+
state->mTouchEventPrefEnabledDoc = dom::TouchEvent::PrefEnabled(docShell);
if (auto* vt = pc->Document()->GetActiveViewTransition()) {

View File

@@ -0,0 +1,30 @@
diff --git a/layout/style/StyleColor.cpp b/layout/style/StyleColor.cpp
index 5fd5f7efba9bcb2febdc9a6b8f8812df673513d8..200e9bf82aacbf38a50f5b99671e403de5b4f61b 100644
--- a/layout/style/StyleColor.cpp
+++ b/layout/style/StyleColor.cpp
@@ -10,6 +10,7 @@
#include "mozilla/dom/BindingDeclarations.h"
#include "nsIFrame.h"
#include "nsStyleStruct.h"
+#include "mozilla/nsZenBoostsBackend.h"
namespace mozilla {
@@ -23,6 +24,8 @@ bool StyleColor::MaybeTransparent() const {
template <>
StyleAbsoluteColor StyleColor::ResolveColor(
const StyleAbsoluteColor& aForegroundColor) const {
+ auto ResolveColorInner = [this,
+ &aForegroundColor]() -> StyleAbsoluteColor {
if (IsAbsolute()) {
return AsAbsolute();
}
@@ -32,6 +35,8 @@ StyleAbsoluteColor StyleColor::ResolveColor(
}
return Servo_ResolveColor(this, &aForegroundColor);
+ };
+ return zen::nsZenBoostsBackend::ResolveStyleColor(ResolveColorInner());
}
template <>

View File

@@ -0,0 +1,119 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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 http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = XPCOMUtils.declareLazy({
styleSheetService: {
service: "@mozilla.org/content/style-sheet-service;1",
iid: Ci.nsIStyleSheetService,
},
});
const AGENT_SHEET = Ci.nsIStyleSheetService.AGENT_SHEET;
export class nsZenBoostStyles {
#stylesCache = new Map();
/**
* Retrieves the CSS style string for a given boost configuration.
* Caches styles to optimize performance.
*
* @param {object} boostData - The boost configuration data.
* @returns {string} The generated CSS style string.
*/
getStyleForBoost(boostData, domain) {
if (this.#stylesCache.has(domain)) {
return this.#stylesCache.get(domain);
}
const rawStyle = this.#generateStyleString(boostData);
if (!rawStyle) {
return null;
}
const styleUri = this.#convertStyleToDataUri(rawStyle);
this.#cacheStyle(styleUri, domain);
return this.getStyleForBoost(boostData, domain);
}
invalidateStyleForDomain(domain) {
if (this.#stylesCache.has(domain)) {
const { uri } = this.#stylesCache.get(domain);
lazy.styleSheetService.unregisterSheet(uri, AGENT_SHEET);
this.#stylesCache.delete(domain);
}
}
/**
* Generates a CSS style string based on the boost configuration.
*
* @param {object} boostData - The boost configuration data.
* @returns {string} The generated CSS style string.
* @private
*/
#generateStyleString(boostData) {
let style = ``;
const fontFamily =
boostData.fontFamily != "" ? `font-family: ${boostData.fontFamily} !important;` : ``;
const fontCase = `text-transform: ${boostData.textCaseOverride} !important;`;
let zapBlocks = "";
if (boostData.zapSelectors) {
for (const selector of boostData.zapSelectors) {
zapBlocks += `html > body ${selector}:not([zen-zap-unhide]){ display: none !important; }\n`;
}
if (zapBlocks != "") {
style += `/* Zen-Zaps */\n`;
style += `${zapBlocks}\n`;
}
}
if (fontCase != "" || fontFamily != "") {
style += `/* Text Format */\n`;
style += `body :is(p, h1, h2, h3, h4, h5, a, span, textarea, input) {\n`;
style += `${fontFamily}\n`;
style += `${fontCase}\n`;
style += `}\n`;
}
if (boostData.customCSS != "") {
style += `/* USER CSS */\n`;
style += `${boostData.customCSS || ""}\n`;
}
return style;
}
/**
* Converts a raw CSS style string into a data URI.
*
* @param {string} rawStyle - The raw CSS style string.
* @returns {string} The data URI representing the CSS style.
* @private
*/
#convertStyleToDataUri(rawStyle) {
const encodedStyle = encodeURIComponent(rawStyle);
return Services.io.newURI(`data:text/css;charset=utf-8,${encodedStyle}`);
}
/**
* Prefetches the style from the data URI and caches it.
*
* @param {string} styleUri - The data URI of the CSS style.
* @param {string} domain - The domain associated with the boost.
* @returns {string} The cached style sheet URI.
* @private
*/
#cacheStyle(styleUri, domain) {
this.#stylesCache.set(domain, {
uuid: Services.uuid.generateUUID().toString(),
uri: styleUri,
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,754 @@
/* 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 http://mozilla.org/MPL/2.0/. */
import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
import { nsZenBoostStyles } from "resource:///modules/zen/boosts/ZenBoostStyles.sys.mjs";
class nsZenBoostsManager {
registeredDomains = new Map(); // <domain, { boosts: <id, boostEntry>, activeBoostID: null }>
#stylesManager = new nsZenBoostStyles();
#saveFilename = "zen-boosts.jsonlz4";
#file = null;
constructor() {
this.#readBoostsFromStore(this.notify);
}
/**
* @returns {object} New domain entry with empty boost map and active boost id
*/
#createDomainEntry() {
return {
boostEntries: new Map(), // <id, boostEntry>
activeBoostId: null,
};
}
/**
* Will get or create a domain entry for the given domain
*
* @param {string} domain - The domain of which the data will be fetched
* @returns {object} The domain entry
*/
#getOrCreateDomainEntry(domain) {
if (!this.registeredDomains.has(domain)) {
this.registeredDomains.set(domain, this.#createDomainEntry());
}
return this.registeredDomains.get(domain);
}
/**
* Will get a domain entry for the given domain
*
* @param {string} domain - The domain of which the data will be fetched
* @returns {object|null} The domain entry
*/
#getDomainEntry(domain) {
if (!this.registeredDomains.has(domain)) {
return null;
}
return this.registeredDomains.get(domain);
}
/**
* Will delete the domain entry for a domain
*
* @param {string} domain - The given domain
*/
#deleteDomainEntry(domain) {
if (this.registeredDomains.has(domain)) {
this.registeredDomains.delete(domain);
}
}
/**
* Gets the active boost id for a given domain
*
* @param {string} domain - The target domain
* @returns {string | null} Will return the active boost id or null
*/
getActiveBoostId(domain) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
return domainEntry.activeBoostId;
}
return null;
}
/**
* Deletes a boost for the specified domain and persists the change to disk.
*
* @param {object} boost - The targeted boost.
*/
deleteBoost(boost) {
const { domain, id } = boost;
if (this.registeredDomains.has(domain)) {
let domainEntry = this.#getOrCreateDomainEntry(domain);
if (domainEntry.boostEntries.has(id)) {
domainEntry.boostEntries.delete(id);
}
if (domainEntry.activeBoostId == id) {
domainEntry.activeBoostId = null;
}
if (domainEntry.boostEntries.size === 0) {
this.#deleteDomainEntry(domain);
}
}
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify(true);
this.#writeToDisk(this.registeredDomains);
}
/**
* @returns {object} Returns a new empty boost entry
*/
getEmptyBoostEntry() {
return {
boostData: {
boostName: "My Boost",
dotAngleDeg: 0,
dotPos: { x: null, y: null },
dotDistance: 0,
brightness: 0.5,
saturation: 0.5,
contrast: 0.25,
fontFamily: "",
enableColorBoost: false,
smartInvert: false,
// Choses theme based on Zen's workspace theme
autoTheme: false,
textCaseOverride: "none",
zapSelectors: [],
customCSS: "",
changeWasMade: false,
},
};
}
/**
* Will create a new boost
*
* @param {string} domain - The domain which will be affected by the boost
* @returns {object|null} The created boost with { id, domain, boostEntry: { boostData } } or null
*/
createNewBoost(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
return null;
}
const id = crypto.randomUUID();
const boostEntry = this.getEmptyBoostEntry(domain);
const domainEntry = this.#getOrCreateDomainEntry(domain);
domainEntry.boostEntries.set(id, boostEntry);
const boost = { id, domain, boostEntry };
return boost;
}
/**
* Loads the boost configuration for the specified domain from storage.
*
* @param {string} domain - The domain for which to load the boost
* @returns {object[] | null} All boosts for the domain or null
*/
loadBoostsFromStore(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
const boosts = [];
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
domainEntry.boostEntries.forEach((value, key) => {
const boost = { id: key, domain, boostEntry: value };
boosts.push(boost);
});
return boosts;
}
return null;
}
/**
* Loads the boost for the specified domain and id from storage.
* If no boost is present, a new one will be created.
*
* @param {string} domain - The domain of the boost
* @param {string} id - The id of the boost
* @returns {object} Returns the boost with { id, domain, boostEntry: { boostData } }
*/
loadBoostFromStore(domain, id) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
if (!id) {
console.error("[ZenBoostsManager] ID expected but got null.");
}
const domainEntry = this.#getOrCreateDomainEntry(domain);
if (domainEntry.boostEntries.has(id)) {
const boostEntry = domainEntry.boostEntries.get(id);
return { id, domain, boostEntry };
}
const boost = this.createNewBoost(domain);
return boost;
}
/**
* Loads the active boost for the specified domain from storage.
*
* @param {string} domain - The domain of the boost
* @returns {object | null} Returns the boost with { id, domain, boostEntry: { boostData } } or null
*/
loadActiveBoostFromStore(domain) {
if (!domain) {
console.error("[ZenBoostsManager] Domain expected but got null.");
}
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.size === 0) {
return this.createNewBoost(domain);
}
if (domainEntry.boostEntries.has(domainEntry.activeBoostId)) {
const boostEntry = domainEntry.boostEntries.get(domainEntry.activeBoostId);
return { id: domainEntry.activeBoostId, domain, boostEntry };
}
}
return null;
}
/**
* Adds the zap selector to the selectors list and updates the website.
*
* @param {string} selector - Selector which will hide the elements
* @param {string} domain - Domain of the target boost
*/
addZapSelectorToActive(selector, domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
if (!boostData.zapSelectors) {
boostData.zapSelectors = [];
}
if (!boostData.zapSelectors.includes(selector)) {
boostData.zapSelectors.push(selector);
}
this.updateBoost(boost);
}
/**
* Removes the zap selector to the selectors list and updates the website.
*
* @param {string} selector - Selector which will no longer hide the elements
* @param {string} domain - Domain of the target boost
*/
removeZapSelectorToActive(selector, domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
if (boostData.zapSelectors && boostData.zapSelectors.includes(selector)) {
const i = boostData.zapSelectors.indexOf(selector);
if (i !== -1) {
boostData.zapSelectors.splice(i, 1);
}
}
this.updateBoost(boost);
}
/**
* Makes the boost at the domain with the id active
*
* @param {string} domain The target domain
* @param {string} id The target boost id
*/
makeBoostActiveForDomain(domain, id) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.has(id)) {
domainEntry.activeBoostId = id;
}
}
Services.obs.notifyObservers(null, "zen-boosts-active-change", { id });
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
/**
* Toggles the boost activeness at the domain with the id active
*
* @param {string} domain The target domain
* @param {string} id The target boost id
*/
toggleBoostActiveForDomain(domain, id) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
if (domainEntry.boostEntries.has(id)) {
if (domainEntry.activeBoostId === id) {
domainEntry.activeBoostId = null;
Services.obs.notifyObservers(null, "zen-boosts-active-change", { id: null });
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify(true);
} else {
domainEntry.activeBoostId = id;
Services.obs.notifyObservers(null, "zen-boosts-active-change", { id });
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
}
}
}
/**
* Clears all zap selectors from a boost
*
* @param {string} domain - Target boost domain
*/
clearZapSelectorsForActive(domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
console.error("[ZenBoostsManager] Active boost is null");
return;
}
const { boostData } = boost.boostEntry;
boostData.zapSelectors = [];
this.updateBoost(boost);
}
/**
* Updates the boost for a domain in memory and notifies observers of the change.
*
* @param {object} boost - The target boost
*/
updateBoost(boost) {
const { domain, id, boostEntry } = boost;
const domainEntry = this.#getOrCreateDomainEntry(domain);
domainEntry.boostEntries.set(id, boostEntry);
this.#stylesManager.invalidateStyleForDomain(domain);
this.notify();
}
/**
* Notifies all observers that boost data has been updated.
* This triggers a 'zen-boosts-update' notification event.
*/
notify(unloadStyles = false) {
Services.obs.notifyObservers(null, "zen-boosts-update", { unloadStyles });
}
/**
* Saves a boost configuration to persistent storage and notifies observers.
*
* @param {object | null} boost - The boost data object to save. If null, only saves existing boosts.
*/
saveBoostToStore(boost) {
if (boost) {
this.updateBoost(boost);
}
this.#writeToDisk(this.registeredDomains);
this.notify();
}
/**
* Reads all boosts from persistent storage and updates the registered boosts map.
*
* @param {Function} done - Callback function to execute after reading is complete.
* @private
*/
#readBoostsFromStore(done) {
this.#readFromDisk().then((data) => {
this.registeredDomains = data;
done();
});
}
/**
* Gets the file path where boost data is stored in the user's profile directory.
*
* @returns {string} The full path to the boost storage file.
* @private
*/
get #storePath() {
const profilePath = PathUtils.profileDir;
return PathUtils.join(profilePath, this.#saveFilename);
}
/**
* Gets the directory path where user css is stored in the user's profile directory.
*
* @returns {string} The full path to the boost userCSS directory.
* @private
*/
get #cssPath() {
const profilePath = PathUtils.profileDir;
return PathUtils.join(profilePath, "zen-boosts");
}
/**
* Reads boost data from disk, decompresses it, and converts it to a Map.
*
* @returns {Promise<Map>} A promise that resolves to a Map of domain to boost data.
* @private
*/
async #readFromDisk() {
this.#file = new JSONFile({
path: this.#storePath,
compression: "lz4",
});
await this.#file.load();
const raw = this.#file.data ?? {};
const map = new Map();
for (const [domain, entry] of Object.entries(raw)) {
const boostsMap = new Map();
for (const [id, boostEntry] of Object.entries(entry.boostEntries ?? {})) {
// Reuinite the user css with the boost data if any exists
const userCSS = await this.#readBoostCSS(id);
if (userCSS) {
boostEntry.boostData.customCSS = userCSS;
}
boostsMap.set(id, boostEntry);
}
map.set(domain, {
activeBoostId: entry.activeBoostId ?? null,
boostEntries: boostsMap,
});
}
return map;
}
/**
* Reads the user CSS of a boost with the given id from the dedicated folder.
* Returns null if the file doesn't exist.
*
* @param {string} id - The id of the boost
* @returns {string|null} Returns the user CSS or null
*/
async #readBoostCSS(id) {
const fileName = `${id}.css`;
const directoryPath = this.#cssPath;
const savePath = PathUtils.join(directoryPath, fileName);
await IOUtils.makeDirectory(directoryPath, { createAncestors: true });
if (await IOUtils.exists(savePath)) {
const css = await IOUtils.readUTF8(savePath);
return css;
}
return null;
}
/**
* Writes boost data to disk by converting the Map to JSON and compressing it.
*
* @param {Map} map - The Map of domain to boost data to write to disk.
* @private
*/
#writeToDisk(map) {
const obj = {};
for (const [domain, entry] of map) {
const boostsObj = {};
for (const [id, boostEntry] of entry.boostEntries) {
// Split the user css from the boost data
boostsObj[id] = structuredClone(boostEntry);
delete boostsObj[id].boostData.customCSS;
this.#writeBoostCSS(id, boostEntry.boostData.customCSS);
}
obj[domain] = {
activeBoostId: entry.activeBoostId ?? null,
boostEntries: boostsObj,
};
}
this.#file.data = obj;
this.#file.saveSoon();
}
/**
* Writes the user CSS of a boost with the given id to a dedicated folder.
*
* @param {string} id - The id of the boost
* @param {string} css - The user CSS
*/
async #writeBoostCSS(id, css) {
const fileName = `${id}.css`;
const directoryPath = this.#cssPath;
const savePath = PathUtils.join(directoryPath, fileName);
await IOUtils.makeDirectory(directoryPath, { createAncestors: true });
await IOUtils.writeUTF8(savePath, css);
}
/**
* Checks if any boost is registered and active for the specified domain.
*
* @param {string} domain - The domain to check for any registered and active boost.
* @returns {boolean} True if a boost exists for the domain and is active, false otherwise.
*/
registeredBoostForDomain(domain) {
const domainEntry = this.#getDomainEntry(domain);
if (domainEntry) {
return domainEntry.boostEntries.has(domainEntry.activeBoostId);
}
return false;
}
/**
* Determines if a boost can be created for the given URI.
* Only HTTP and HTTPS schemes are supported for boosting.
*
* @param {nsIURI} uri - The URI to check for boost eligibility.
* @returns {boolean} True if the URI scheme is http or https, false otherwise.
*/
canBoostSite(uri) {
if (!uri || !uri.schemeIs) {
return false;
}
return uri.schemeIs("http") || uri.schemeIs("https");
}
/**
* Gets from cache or creates and caches a new style sheet for the given boost data.
*
* @param {string} domain - The domain of the boosts.
* @returns {nsIStyleSheet} The style sheet corresponding to the boost data.
*/
getStyleSheetForBoost(domain) {
const boost = this.loadActiveBoostFromStore(domain);
if (!boost) {
return null;
}
const { boostData } = boost.boostEntry;
return this.#stylesManager.getStyleForBoost(boostData, domain);
}
/**
* Opens the boost editor in a new popup window.
*
* @param {Window} parentWindow - The parent browser window
* @param {Boost} boost - The boost which will be edited
* @param {nsIURI} domainUri - The boost which will be edited
* @returns {Window|null} The instanced editor window
*/
openBoostWindow(parentWindow, boost, domainUri) {
if (!this.canBoostSite(domainUri)) {
console.error("[ZenBoostsManager] Cannot open editor for boost with invalid domain.");
return null;
}
const domain = boost.domain;
const { availLeft, availWidth } = parentWindow.screen;
const screenX = parentWindow.screenX;
const screenY = parentWindow.screenY;
const width = parentWindow.outerWidth;
const height = parentWindow.outerHeight;
const editorWidth = 185;
const editorHeight = 565;
const pad = 20;
let left = screenX + width + pad;
if (this.#areTabsOnRightSide()) {
left = screenX - (editorWidth + pad);
}
let top = screenY + height / 2 - editorHeight / 2;
if (left + editorWidth > availLeft + availWidth || left < availLeft) {
left = screenX + width - (editorWidth + pad);
if (this.#areTabsOnRightSide()) {
left = screenX + pad;
}
}
const editor = Services.ww.openWindow(
parentWindow,
"chrome://browser/content/zen-components/windows/zen-boost-editor.xhtml",
null,
`left=${left},top=${top},chrome,alwaysontop,resizable=no,minimizable=no,dependent,dialog=yes`,
null
);
// Close the editor if the tab is switched
parentWindow.gBrowser.tabContainer.addEventListener("TabSelect", editor.close.bind(editor), {
once: true,
});
const progressListener = {
onLocationChange: (webProgress) => {
if (webProgress.isTopLevel) {
editor.close();
parentWindow.gBrowser.removeTabsProgressListener(progressListener);
}
},
};
parentWindow.gBrowser.addProgressListener(progressListener);
// Give the domain
editor.domain = domain;
editor.openerWindow = parentWindow;
editor.focus();
// Make boost active
this.makeBoostActiveForDomain(domain, boost.id);
return editor;
}
/**
* Will spawn a file save dialog and export the selected boost
*
* @param {Window} parentWindow The window that will instance the file picker
* @returns {Promise<void>} Returns a promise which will be resolved after the export action is complete
*/
exportBoost(parentWindow, boostData) {
// From: firefox-main/browser/base/content/browser-commands.js:354
// https://searchfox.org/firefox-main/source/browser/base/content/browser-commands.js#355:~:text=try%20%7B-,const,fp%2Eopen%28fpCallback%29%3B
const nsIFilePicker = Ci.nsIFilePicker;
const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(
parentWindow.browsingContext,
`Exporting Boost ${boostData.boostName}...`,
nsIFilePicker.modeSave
);
// Sanitizing filename
// From: https://gist.github.com/barbietunnie/7bc6d48a424446c44ff4#:~:text=bytes%22%29%3B-,var,%7D
const illegalRe = /[\/\?<>\\:\*\|":]/g;
// eslint-disable-next-line no-control-regex
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
const reservedRe = /^\.+$/;
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
let sanitized = boostData.boostName
.replace(illegalRe, "")
.replace(controlRe, "")
.replace(reservedRe, "")
.replace(windowsReservedRe, "");
// Replace if resulting filename is empty
if (!sanitized) {
sanitized = "New Boost";
}
fp.defaultString = sanitized;
fp.defaultExtension = "json";
fp.appendFilters(nsIFilePicker.filterAll);
return new Promise((resolve) => {
fp.open(async (result) => {
if (result === nsIFilePicker.returnOK && fp.file) {
try {
const boostJSON = JSON.stringify(boostData);
await IOUtils.writeUTF8(fp.file.path, boostJSON);
resolve(true);
} catch (ex) {
console.error("Export failed:", ex);
resolve(false);
}
} else {
resolve(false);
}
});
});
}
/**
* Will spawn a file open dialog and import the selected boost
*
* @param {Window} parentWindow The window that will instance the file picker
* @returns {Promise<object | null>} Returns a promise with the boost data or null
*/
importBoost(parentWindow) {
const nsIFilePicker = Ci.nsIFilePicker;
const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(parentWindow.browsingContext, "Importing Boost from JSON", nsIFilePicker.modeOpen);
fp.appendFilters(nsIFilePicker.filterAll);
return new Promise((resolve) => {
fp.open(async (result) => {
if (result === nsIFilePicker.returnOK && fp.file) {
try {
const fileContent = await IOUtils.readUTF8(fp.file.path);
resolve(JSON.parse(fileContent));
} catch (e) {
console.error("Import failed:", e);
resolve(null);
}
} else {
resolve(null);
}
});
});
}
/**
* Helper function to determine if tabs are on the right side.
* From: ZenDownloadAnimation.mjs
*/
#areTabsOnRightSide() {
return Services.prefs.getBoolPref("zen.tabs.vertical.right-side");
}
}
export const gZenBoostsManager = new nsZenBoostsManager();

View File

@@ -0,0 +1,759 @@
/* 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 http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
return new Localization(["browser/zen-boosts.ftl"], true);
});
export class SelectorComponent {
document = null;
window = null;
#initialized = false;
#content = null;
#currentState = null;
#selectedElement = null;
#lastOverElement = null;
#relatedValueIndex = 0;
static STATES = {
SELECTING: "selecting",
SELECTED: "selected",
};
#zenContentIDs = [];
#onSelect = null;
#localizationArray = [
{ id: "zen-select-this" },
{ id: "zen-select-related" },
{ id: "zen-select-cancel" },
];
safeAreaPadding = { left: 0, right: 0, top: 0, bottom: 0 };
/**
* @param {Document} document Webpage document
* @param {ZenBoostsChild} zenBoostsChild Boost JSActor child
* @param {string[]} additionalZenContentIDs Additional IDs that will be considered as non website content
* @param {Function<string>} onSelect Callback for when a selection was made. The parameter is the css selector
* @param {object[]} localizationArray An array of 3 { id: 'l10n-id' } fluent IDs for localization going from the inputs left to right
*/
constructor(
document,
zenBoostsChild,
additionalZenContentIDs,
onSelect,
localizationArray = null
) {
this.document = document;
this.window = document.ownerGlobal;
this.zenBoostsChild = zenBoostsChild;
this.#onSelect = onSelect;
if (localizationArray != null) {
this.#localizationArray = localizationArray;
}
const baseSelectorIDs = ["select-controls", "select-controls-container"];
this.#zenContentIDs = [...additionalZenContentIDs, ...baseSelectorIDs];
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
return;
}
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
this.#initializeElements();
this.setState(SelectorComponent.STATES.SELECTING);
this.#initialized = true;
}
/**
* Initializes all anonymous content and events
*/
#initializeElements() {
this.hoverDiv = this.getElementById("hover-div");
this.selectorComponent = this.getElementById("select-component");
this.cancelButton = this.getElementById("select-cancel");
this.cancelButton.addEventListener("click", this.#cancelSelect.bind(this));
this.selectThisButton = this.getElementById("select-this");
this.selectThisButton.addEventListener("click", this.#handleSelect.bind(this));
this.selectRelatedSlider = this.getElementById("select-related");
this.selectRelatedSlider.addEventListener("click", this.#handleSelect.bind(this));
// Initialize the related elements button
this.selectRelatedSlider.addEventListener("mousemove", (e) => {
const r = e.currentTarget.getBoundingClientRect();
const mouseX = e.clientX;
let value = (mouseX - r.left) / r.width;
value = Math.max(0, Math.min(1, value));
e.target.style = `--related-elements-value: ${value * 100}%;`;
const lastIndex = this.#relatedValueIndex;
this.#relatedValueIndex = Math.round(value * 8);
if (lastIndex != this.#relatedValueIndex) {
this.updateHighlight();
this.#updatePathTextField();
}
});
this.selectRelatedSlider.addEventListener("mouseout", (e) => {
e.currentTarget.style.removeProperty("--related-elements-value");
this.#relatedValueIndex = 0;
this.updateHighlight();
this.#updatePathTextField();
});
}
/**
* Sets the state of the zap mode
*
* @param {STATES} newState New state
* @param {*} data Optional additional data
*/
setState(newState, data = null) {
this.#currentState = newState;
switch (newState) {
case SelectorComponent.STATES.SELECTED:
this.#selectedElement = data;
this.#relatedValueIndex = 0; // Reset index
this.#hideHoverDiv();
this.#showSelectorComponent();
this.updateHighlight();
this.#updatePathTextField();
break;
case SelectorComponent.STATES.SELECTING:
this.#selectedElement = null;
this.#showHoverDiv();
this.#hideSelectorComponent();
this.removeHighlight();
break;
}
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
// Fetch localizations
let [thisElement, relatedElements, cancelAction] = lazy.overlayLocalization.formatMessagesSync(
this.#localizationArray
);
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-selector.css" />
<div id="select-component">
<div id="select-controls">
<input type="button" id="select-this" value="${thisElement.value}"/>
<input type="button" id="select-related" value="${relatedElements.value}"/>
<input type="button" id="select-cancel" value="${cancelAction.value}"/>
</div>
<div id="selector-preview">
<p id="selector-element-preview-text"></p>
</div>
</div>
<div id="hover-div"></div>
<div id="highlight-container"></div>
<div id="highlight-shadow" style="display:none;"></div>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(doc.querySelector("template"), true);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Handles the addition of the current zap selector
*/
#handleSelect() {
const cssPath = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
this.removeHighlight();
this.#resetHoverDiv();
// The highlight should be gone before the onSelect is called
this.window.requestAnimationFrame(() => {
this.setState(SelectorComponent.STATES.SELECTING);
if (cssPath) {
this.#onSelect(cssPath);
}
});
}
/**
* Cancles the current selection operation
*/
#cancelSelect() {
this.setState(SelectorComponent.STATES.SELECTING);
}
/**
* Rebuilds the selection highlight
*/
updateHighlight() {
this.removeHighlight();
this.showHighlight(this.getSelection());
}
/**
* Highlights a selection of elements on the website
*
* @param {List} selection A list of the web elements that should be highlighted
*/
showHighlight(selection) {
const highlightContainerDiv = this.getElementById("highlight-container");
highlightContainerDiv.style.display = "initial";
let counter = 0;
for (const element of selection) {
if (counter >= 100) {
break;
} // Avoid too many instanced objects
counter++;
const padding = 5;
const elementMeasurement = element?.getBoundingClientRect() ?? undefined;
if (elementMeasurement == undefined) {
continue;
}
const highlightDiv = this.document.createElement("div");
highlightDiv.classList.add("highlight");
Object.assign(highlightDiv.style, {
left: `${elementMeasurement.left - padding}px`,
top: `${elementMeasurement.top - padding}px`,
width: `${elementMeasurement.width + padding * 2}px`,
height: `${elementMeasurement.height + padding * 2}px`,
});
highlightContainerDiv.appendChild(highlightDiv);
}
this.getElementById("highlight-shadow").style.display = "initial";
}
/**
* Clears the highlight
*/
removeHighlight() {
const highlightContainerDiv = this.getElementById("highlight-container");
highlightContainerDiv.style.display = "none";
// Clear all children elements
highlightContainerDiv.innerHTML = "";
this.getElementById("highlight-shadow").style.display = "none";
}
/**
* Updates the path display text on the selector component
* based on the current selection
*/
#updatePathTextField() {
const maxPathLength = 64;
const selection = this.getSelection();
const selectionPath = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
if (!selectionPath) {
return;
}
const pathText = `<b>[${selection.length}]</b> ${selectionPath.substring(0, Math.min(maxPathLength, selectionPath.length))}`;
this.getElementById("selector-element-preview-text").setHTML(pathText);
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.window = null;
this.document = null;
this.#initialized = false;
}
/**
* Hides the hover selection box
*/
#hideHoverDiv() {
this.hoverDiv.style.display = "none";
}
/**
* Shows the hover selection box
*/
#showHoverDiv() {
this.hoverDiv.style.display = "initial";
}
/**
* Resets the hover selection box bounds
*/
#resetHoverDiv() {
Object.assign(this.getElementById("hover-div").style, {
top: `0px`,
left: `0px`,
width: `0px`,
height: `0px`,
});
}
/**
* Hides the selector component
*/
#hideSelectorComponent() {
this.selectorComponent.style.visibility = "hidden";
this.selectorComponent.setAttribute("is-appearing", "false");
}
/**
* Shows the selector component
*/
#showSelectorComponent() {
this.selectorComponent.style.visibility = "visible";
this.#setSelectorComponentPosition();
}
/**
* Sets the aligned and clamped position for the zap component on the document
* relative to #selectedElement
*/
#setSelectorComponentPosition() {
const bounds = this.#selectedElement.getBoundingClientRect();
const distance = 8;
const rect = this.selectorComponent.getBoundingClientRect();
const zapComponentWidth = rect.width;
const zapComponentHeight = rect.height;
const windowWidth = this.window.innerWidth;
const windowHeight = this.window.innerHeight;
const windowPadding = 10;
this.selectorComponent.setAttribute("is-appearing", "false");
// This clamps the position so the zap component never goes out of the client bounds and adds a small padding
const top = this.clamp(
bounds.top + bounds.height + distance,
windowPadding + this.safeAreaPadding.top,
windowHeight - zapComponentHeight - windowPadding - this.safeAreaPadding.bottom
);
const left = this.clamp(
bounds.left + bounds.width / 2 - zapComponentWidth / 2,
windowPadding + this.safeAreaPadding.left,
windowWidth - zapComponentWidth - windowPadding - this.safeAreaPadding.right
);
Object.assign(this.selectorComponent.style, {
top: `${top}px`,
left: `${left}px`,
});
// Adjust transform origin for animation
const targetCenterX = bounds.left + bounds.width / 2;
const targetBottomY = bounds.top + bounds.height;
let originX = this.clamp(targetCenterX - left, 0, zapComponentWidth);
let originY = this.clamp(targetBottomY - top, 0, zapComponentHeight);
this.selectorComponent.style.transformOrigin = `${originX}px ${originY}px`;
this.window.requestAnimationFrame(() => {
this.selectorComponent.setAttribute("is-appearing", "true");
});
}
/**
* This function handles page events while the overlay is active
*
* @param {Event} event The event which will be handled by the overlay
* @param {boolean} prevent True if the event should be prevented
*/
handleEvent(event, prevent) {
let isZenContent = false;
if (event?.originalTarget?.closest) {
const closestID = event.originalTarget.closest("div")?.id ?? "";
isZenContent = this.#zenContentIDs.includes(closestID);
}
switch (event.type) {
case "click":
this.#handleClick(event, isZenContent);
break;
case "mousemove":
this.#handleMouseMove(event, isZenContent);
break;
case "scroll":
this.#handlePageChange(event);
return;
case "resize":
this.#handlePageChange(event);
return;
}
// Let the interactable ids pass through
if (isZenContent) {
return;
}
if (prevent) {
// From ScreenshotsComponentChild.sys.mjs:103
// Preventing a pointerdown event throws an error in debug builds.
// See https://searchfox.org/mozilla-central/rev/b41bb321fe4bd7d03926083698ac498ebec0accf/widget/WidgetEventImpl.cpp#566-572
// Don't prevent the default context menu.
if (!["contextmenu", "pointerdown"].includes(event.type)) {
event.preventDefault();
}
event.stopImmediatePropagation();
}
}
/**
* Called after a page change to update the highlight and selector component position
*/
#handlePageChange() {
if (this.#currentState !== SelectorComponent.STATES.SELECTED) {
return;
}
this.updateHighlight();
this.#setSelectorComponentPosition();
}
/**
* Handles the mouse move event
*
* @param {Event} event Mouse move event params
* @param {boolean} isZenContent Flag if the target element is a zen related element
*/
#handleMouseMove(event, isZenContent) {
if (this.#lastOverElement === event.target) {
return;
}
if (!isZenContent) {
this.#lastOverElement = event.target;
if (this.#currentState === SelectorComponent.STATES.SELECTING) {
this.#showHoverDiv();
}
} else {
this.#hideHoverDiv();
}
if (this.#currentState !== SelectorComponent.STATES.SELECTING || !event.target) {
return;
}
const bounds = event.target.getBoundingClientRect();
const padding = 5;
Object.assign(this.getElementById("hover-div").style, {
top: `${bounds.top - padding}px`,
left: `${bounds.left - padding}px`,
width: `${bounds.width + padding * 2}px`,
height: `${bounds.height + padding * 2}px`,
});
}
/**
* Handles the mouse click event
*
* @param {Event} event Mouse move event params
* @param {boolean} isZenContent Flag if the target element is a zen related element
*/
#handleClick(event, isZenContent) {
// Safeguards for protecting anonymous content from being zapped
if (
event.target === this.document.documentElement ||
event.target === this.document.body ||
!this.document.documentElement.contains(event.target)
) {
return;
}
if (this.#currentState === SelectorComponent.STATES.SELECTING && !isZenContent) {
this.setState(SelectorComponent.STATES.SELECTED, event.target);
}
}
/**
* @param {number} x Value
* @param {number} min Minimum limit
* @param {number} max Maximum limit
* @returns {number} A value which always lies between min and max
*/
clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
}
/**
* When selecting an area to zap there can be a set of zapped elements
* since related elements can be included.
* This method returns all targeted elements for the zapping process.
*
* @returns {Element[]} An array of selected elements
*/
getSelection() {
const selector = this.getSelectionPath(
this.document,
this.#relatedValueIndex,
this.#selectedElement
);
if (!selector) {
return [];
}
return this.document.querySelectorAll(selector);
}
/**
* Used for retreiving the css path from the selected element and taking
* the related objects into account
*/
getSelectionPath(document, relatedValueIndex, selectedElement) {
let path = [];
const cssescape = (str) => CSS.escape(str);
// Body and Html nodes are not considered valid here
const isValidNode = (element) => {
if (!element) {
return false;
} else if (element.tagName.toLowerCase() === "body") {
return false;
} else if (element.tagName.toLowerCase() === "html") {
return false;
}
return true;
};
const nthChild = (element) => {
if (!element) {
return "";
}
if (!element.parentNode) {
return "";
}
const parent = element.parentNode;
const index = Array.prototype.indexOf.call(parent.children, element) + 1;
if (index === 1) {
return ":first-child";
}
if (index === parent.children.length) {
return ":last-child";
}
return `:nth-child(${index})`;
};
const getIdentification = (element, specifity = 0) => {
if (!element) {
return "";
}
const id = specifity < 2 && element.id ? `#${cssescape(element.id)}` : "";
const cls =
specifity < 1 && element.classList.length
? "." + [...element.classList].map((c) => cssescape(c)).join(".")
: "";
const tag = element.tagName ? element.tagName.toLowerCase() : "";
return `${tag}${id}${cls}`;
};
const traverse = (element, specifity = 0, pathArray) => {
let currentElement = element;
if (!isValidNode(currentElement)) {
return;
}
pathArray.push(nthChild(currentElement));
pathArray.push(getIdentification(currentElement, specifity));
if (currentElement && currentElement.parentNode) {
pathArray.push(" > ");
pathArray.push(getIdentification(currentElement.parentNode, 0));
const tempBuild = build(pathArray);
while (
tempBuild &&
document.querySelectorAll(tempBuild).length > 1 &&
isValidNode(currentElement.parentNode)
) {
currentElement = currentElement.parentNode;
pathArray.push(" > ");
pathArray.push(nthChild(currentElement.parentNode));
pathArray.push(getIdentification(currentElement.parentNode, 0));
}
}
};
const build = (pathArray) => pathArray.toReversed().join("");
const findBestExactSelector = (element, doc) => {
let buildMap = new Map();
let pathExactElement = [];
traverse(element, 0, pathExactElement);
const pathExactElementBuilt = build(pathExactElement);
const pathExactElementLength = (
pathExactElementBuilt ? doc.querySelectorAll(pathExactElementBuilt) : []
).length;
buildMap.set(pathExactElementLength, pathExactElementBuilt);
let pathTypeElement = [];
pathTypeElement.push(getIdentification(element, 2));
if (isValidNode(element.parentNode)) {
pathTypeElement.push(" > ");
traverse(element.parentNode, 0, pathTypeElement);
}
const pathTypeElementBuilt = build(pathTypeElement);
const pathTypeElementLength = (
pathTypeElementBuilt ? doc.querySelectorAll(pathTypeElementBuilt) : []
).length;
if (!buildMap.has(pathTypeElementLength)) {
buildMap.set(pathTypeElementLength, pathTypeElementBuilt);
}
let parentExactElement = [];
if (isValidNode(element.parentNode)) {
traverse(element.parentNode, 0, parentExactElement);
}
const pathParentElementBuilt = build(parentExactElement);
const pathParentElementLength = (
pathParentElementBuilt ? doc.querySelectorAll(pathParentElementBuilt) : []
).length;
if (!buildMap.has(pathParentElementLength)) {
buildMap.set(pathParentElementLength, pathParentElementBuilt);
}
let smallestLength = Number.MAX_VALUE;
let smallestLengthPath = null;
buildMap.forEach((buildPath, length) => {
if (length < smallestLength) {
smallestLength = length;
smallestLengthPath = buildPath;
}
});
return smallestLengthPath;
};
if (!isValidNode(selectedElement)) {
return null;
}
switch (relatedValueIndex) {
// Sometimes getting the exact element we want is not guaranteed by
// one specific path builder. It is best to build multiple possible paths
// and decide which one is best.
default:
case 0:
return findBestExactSelector(selectedElement, document);
// Getting the exact parent element of selected element
case 1:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
traverse(selectedElement.parentNode, 0, path);
break;
// Getting the type of selected element with same exact parent
case 2:
path.push(getIdentification(selectedElement, 2));
if (isValidNode(selectedElement.parentNode)) {
path.push(" > ");
path.push(getIdentification(selectedElement.parentNode, 0));
}
break;
// Getting the same type of selected element with same parent type
case 3:
path.push(getIdentification(selectedElement, 2));
if (isValidNode(selectedElement.parentNode)) {
path.push(" > ");
path.push(getIdentification(selectedElement.parentNode, 2));
}
break;
// Getting the same type of selected element
case 4:
path.push(getIdentification(selectedElement, 2));
break;
// Getting the same type of the parent
case 5:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
path.push(getIdentification(selectedElement.parentNode, 2));
break;
// Get any element with same parent
case 6:
if (!isValidNode(selectedElement.parentNode)) {
return null;
}
path.push("*");
path.push(" > ");
traverse(selectedElement.parentNode, 0, path);
break;
// Get elements with similar classes
case 7:
path.push(getIdentification(selectedElement, 1));
break;
}
return build(path);
}
}

View File

@@ -0,0 +1,599 @@
/* 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 http://mozilla.org/MPL/2.0/. */
export class ZapDissolve {
FRAG = `
precision mediump float;
uniform sampler2D u_DissolveTexture;
varying vec2 v_PCoord;
varying float v_PLifetime;
varying float v_POpacity;
void main() {
if(v_PLifetime >= 1.0) discard;
vec4 sampled = texture2D(u_DissolveTexture, v_PCoord);
if(sampled.a == 0.0) discard;
float distToCenter = distance(vec2(0.5, 0.5), gl_PointCoord);
if(distToCenter > 0.5) discard;
float alpha = sampled.a * v_POpacity;
float brightnessFactor = 1.5;
gl_FragColor = vec4(sampled.rgb * brightnessFactor, alpha);
}
`;
VERT = `
precision highp float;
uniform float u_AnimationDuration;
uniform float u_ParticleSize;
uniform float u_ElapsedTime;
uniform float u_ViewportWidth;
uniform float u_ViewportHeight;
uniform float u_TextureWidth;
uniform float u_TextureHeight;
uniform float u_TextureLeft;
uniform float u_TextureTop;
attribute float a_ParticleIndex;
varying vec2 v_PCoord;
varying float v_PLifetime;
varying float v_POpacity;
float hash1(float n) { return fract(sin(n) * 100000.0); }
float hash2(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123); }
float hash3(vec2 p, float offset) {
return fract(sin(dot(p, vec2(12.9898 + offset, 78.233 - offset))) * 43758.5453123);
}
/* coordinates */
vec2 toNDC(vec2 pixel) {
vec2 ndc;
ndc.x = (pixel.x / u_ViewportWidth) * 2.0 - 1.0;
ndc.y = 1.0 - (pixel.y / u_ViewportHeight) * 2.0;
return ndc;
}
float mix1(float a, float b, float t) { return a + (b - a) * t; }
/* particle layout */
vec2 particlePixelPos(float index) {
float cols = floor(u_TextureWidth / u_ParticleSize);
if(cols < 1.0) cols = 1.0;
float row = floor(index / cols);
float col = index - row * cols;
return vec2(
(col + 0.5) * u_ParticleSize + u_TextureLeft,
(row + 0.5) * u_ParticleSize + u_TextureTop
);
}
mat2 rotate2D(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(c, -s, s, c);
}
/* motion */
vec4 animatedPosition(vec2 pixelPos, float seed, float seed2, float seed3, float t) {
vec2 base = toNDC(pixelPos);
// Random rotation
float rotation1 = (seed - 0.5) * 0.52; // 15 * 2 * (π/180)
float rotation2 = (seed2 - 0.5) * 0.52;
// Random translation
float randomRadian = 6.28318 * (seed3 - 0.5);
float translateX = 60.0 * cos(randomRadian) / u_ViewportWidth * 2.0;
float translateY = 30.0 * sin(randomRadian) / u_ViewportHeight * 2.0;
vec2 rotated1 = rotate2D(rotation1 * t) * base;
vec2 translated = vec2(
mix1(rotated1.x, rotated1.x + translateX, t),
mix1(rotated1.y, rotated1.y + translateY, t)
);
vec2 finalPos = rotate2D(rotation2 * t) * (translated - base) + base;
return vec4(finalPos, 0.0, 1.0);
}
void main() {
vec2 pixelPos = particlePixelPos(a_ParticleIndex);
// Multiple seeds for different random values
float seed = hash2(pixelPos);
float seed2 = hash3(pixelPos, 1.0);
float seed3 = hash3(pixelPos, 2.0);
float normalizedX = (pixelPos.x - u_TextureLeft) / u_TextureWidth;
float frameAssignment = (seed + 2.0 * normalizedX) / 3.0;
float maxDelay = u_AnimationDuration * 0.42;
float delay = frameAssignment * maxDelay;
float particleDuration = u_AnimationDuration - delay;
float lifetime = clamp((u_ElapsedTime - delay) / particleDuration, 0.0, 1.0);
float easedT = 1.0 - pow(1.0 - lifetime, 3.0);
gl_Position = animatedPosition(pixelPos, seed, seed2, seed3, easedT);
gl_PointSize = u_ParticleSize;
v_PLifetime = lifetime;
v_POpacity = 1.0 - lifetime;
// Texture coordinates
float cols = floor(u_TextureWidth / u_ParticleSize);
if(cols < 1.0) cols = 1.0;
float row = floor(a_ParticleIndex / cols);
float col = a_ParticleIndex - row * cols;
v_PCoord = vec2(
(col + 0.5) / cols,
(row + 0.5) / floor(u_TextureHeight / u_ParticleSize)
);
}
`;
window = null;
document = null;
#initialized = false;
#content = null;
#webglContext = null;
#program;
#animationStartTime = -1;
#particlesCount = 0;
#duration = 1600;
#texture = null;
#buffer = null;
#hasTriggered = false;
#rafId = null;
/**
* @param {Document} document Webpage document
*/
constructor(document) {
if (!document) {
return;
}
this.document = document;
this.window = document.ownerGlobal;
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
return;
}
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
await this.#initializeElements();
this.#initialized = true;
}
/**
* Initializes the elements and contexts required for the rendering
*/
async #initializeElements() {
const canvas = this.getElementById("zen-zap-dissolve-canvas");
const gl = canvas.getContext("webgl", {
alpha: true,
antialias: true,
premultipliedAlpha: false,
});
this.#webglContext = gl;
if (!gl) {
console.error("WebGL is not supported.");
return;
}
this.#resizeCanvasToClientSize(canvas);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0, 0, 0, 0);
// Ensure viewport is set correctly
gl.viewport(0, 0, canvas.width, canvas.height);
this.#program = await this.#createProgram(gl, this.VERT, this.FRAG);
}
/**
* Resizes the canvas to the correct size
*
* @param {Element} canvas
*/
#resizeCanvasToClientSize(canvas) {
const width = this.window.innerWidth;
const height = this.window.innerHeight;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
// Set the WebGL viewport to match the canvas size
if (this.#webglContext) {
this.#webglContext.viewport(0, 0, width, height);
}
}
}
/**
* Creates the canvas program
*
* @param {object} gl WebGL context
* @param {string} vertContent Content of the vertex shader
* @param {string} fragContent Content of the fragment shader
* @returns {*} The WebGL program
*/
async #createProgram(gl, vertContent, fragContent) {
const vertexShader = this.#compileShader(gl, vertContent, gl.VERTEX_SHADER);
const fragmentShader = this.#compileShader(gl, fragContent, gl.FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) {
console.error("Program creation aborted: One or more shaders failed to compile.");
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.detachShader(program, vertexShader);
gl.detachShader(program, fragmentShader);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Shader program initializiation failure:", gl.getProgramInfoLog(program));
}
return program;
}
/**
* Compiles a shader
*
* @param {object} gl WebGL context
* @param {string} source Shader source
* @param {object} type Shader type
* @returns {*} The compiled shader
*/
#compileShader(gl, source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compilation failure:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Retreives image data from a given image
*
* @param {Image} image The image
* @returns {ImageData} Image data
*/
#getImageData(image) {
const canvas =
this.getElementById("zen-zap-dissolve-canvas").ownerDocument.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
return context.getImageData(0, 0, canvas.width, canvas.height);
}
/**
* Creates and loads a WebGL texture from an Image
*
* @param {Image} image The target image
*/
#loadTexture(image) {
if (this.#texture) {
this.#webglContext.deleteTexture(this.#texture);
}
const texture = this.#webglContext.createTexture();
this.#webglContext.activeTexture(this.#webglContext.TEXTURE0);
this.#webglContext.bindTexture(this.#webglContext.TEXTURE_2D, texture);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_WRAP_S,
this.#webglContext.CLAMP_TO_EDGE
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_WRAP_T,
this.#webglContext.CLAMP_TO_EDGE
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_MIN_FILTER,
this.#webglContext.NEAREST
);
this.#webglContext.texParameteri(
this.#webglContext.TEXTURE_2D,
this.#webglContext.TEXTURE_MAG_FILTER,
this.#webglContext.NEAREST
);
if (image && image instanceof Ci.nsIImageLoadingContent && image.width && image.height) {
this.#webglContext.texImage2D(
this.#webglContext.TEXTURE_2D,
0,
this.#webglContext.RGBA,
this.#webglContext.RGBA,
this.#webglContext.UNSIGNED_BYTE,
image
);
} else {
const imageData = this.#getImageData(image);
this.#webglContext.texImage2D(
this.#webglContext.TEXTURE_2D,
0,
this.#webglContext.RGBA,
imageData.width,
imageData.height,
0,
this.#webglContext.RGBA,
this.#webglContext.UNSIGNED_BYTE,
imageData.data
);
}
const textureLocation = this.#webglContext.getUniformLocation(
this.#program,
"u_DissolveTexture"
);
this.#webglContext.uniform1i(textureLocation, 0);
this.#texture = texture;
}
/**
* Binds the parameters to the program for a given Element
*
* @param {Element} element
*/
#bindParameters(element) {
const gl = this.#webglContext;
const rect = element.getBoundingClientRect();
const particleSize = 1;
const textureWidth = rect.width;
const textureHeight = rect.height;
const cols = Math.floor(textureWidth / particleSize);
const rows = Math.floor(textureHeight / particleSize);
this.#particlesCount = cols * rows;
gl.useProgram(this.#program);
this.#setUniform1f("u_AnimationDuration", this.#duration);
this.#setUniform1f("u_ParticleSize", particleSize);
this.#setUniform1f("u_ViewportWidth", this.window.innerWidth);
this.#setUniform1f("u_ViewportHeight", this.window.innerHeight);
this.#setUniform1f("u_TextureWidth", textureWidth);
this.#setUniform1f("u_TextureHeight", textureHeight);
this.#setUniform1f("u_TextureLeft", rect.left);
this.#setUniform1f("u_TextureTop", rect.top);
const indices = new Float32Array(this.#particlesCount);
for (let i = 0; i < this.#particlesCount; i++) {
indices[i] = i;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
}
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, indices, gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(this.#program, "a_ParticleIndex");
gl.enableVertexAttribArray(aLoc);
gl.vertexAttribPointer(aLoc, 1, gl.FLOAT, false, 0, 0);
this.#buffer = buffer;
}
/**
* Requests an animation frame
*/
#requestDraw() {
this.#rafId = this.window.requestAnimationFrame((t) => this.#draw(t));
}
/**
* Helper function for setting a uniform in the WebGL context
*
* @param {string} name The property name
* @param {*} value The property value
*/
#setUniform1f(name, value) {
const loc = this.#webglContext.getUniformLocation(this.#program, name);
this.#webglContext.uniform1f(loc, value);
}
/**
* Renders the output to a canvas
*
* @param {number} time The frametime
*/
#draw(time) {
const gl = this.#webglContext;
gl.useProgram(this.#program);
if (this.#animationStartTime === -1) {
this.#animationStartTime = time;
}
const elapsed = time - this.#animationStartTime;
if (elapsed > this.#duration) {
this.#resetForNextRun();
return;
}
gl.clear(gl.COLOR_BUFFER_BIT);
this.#setUniform1f("u_ElapsedTime", elapsed);
gl.drawArrays(gl.POINTS, 0, this.#particlesCount);
this.#requestDraw();
}
/**
* Displays a dissolve effect for the element
*
* @param {Element} element The element to dissolve
*/
dissolve(element) {
if (!this.#initialized || this.#hasTriggered || !element) {
return;
}
this.#hasTriggered = true;
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn("[ZapDissolve]: element has zero size. Skipping dissolve");
return;
}
const captureCanvas = this.document.createElement("canvas");
captureCanvas.width = rect.width;
captureCanvas.height = rect.height;
const ctx = captureCanvas.getContext("2d");
const canvas = this.getElementById("zen-zap-dissolve-canvas");
this.#resizeCanvasToClientSize(canvas);
ctx.drawWindow(this.window, rect.left, rect.top, rect.width, rect.height, "rgba(0,0,0,0)");
this.#loadTexture(captureCanvas);
this.#bindParameters(element);
this.#animationStartTime = -1;
this.#requestDraw();
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-zap.css" />
<canvas id="zen-zap-dissolve-canvas"></canvas>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(doc.querySelector("template"), true);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Makes the class ready for reusage
*/
#resetForNextRun() {
const gl = this.#webglContext;
if (gl) {
if (this.#texture) {
gl.deleteTexture(this.#texture);
this.#texture = null;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
this.#buffer = null;
}
gl.clear(gl.COLOR_BUFFER_BIT);
}
this.#hasTriggered = false;
this.#animationStartTime = -1;
if (this.#rafId) {
this.window.cancelAnimationFrame(this.#rafId);
this.#rafId = null;
}
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
const gl = this.#webglContext;
if (gl) {
if (this.#texture) {
gl.deleteTexture(this.#texture);
this.#texture = null;
}
if (this.#buffer) {
gl.deleteBuffer(this.#buffer);
this.#buffer = null;
}
if (this.#program) {
gl.deleteProgram(this.#program);
this.#program = null;
}
const loseCtx = gl.getExtension("WEBGL_lose_context");
loseCtx?.loseContext();
}
if (this.window != null) {
this.window.cancelAnimationFrame(this.#rafId);
}
this.#rafId = null;
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.#content = null;
this.#initialized = false;
}
}

View File

@@ -0,0 +1,391 @@
/* 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 http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZapDissolve: "resource:///modules/zen/boosts/ZenZapDissolve.sys.mjs",
SelectorComponent: "resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
return new Localization(["browser/zen-boosts.ftl"], true);
});
export class ZapOverlay {
document = null;
window = null;
#initialized = false;
#content = null;
#zapContentIDs = ["zap-list", "zap-controls-container"];
#selectorComponent = null;
#dissolvePoolSize = 5;
#dissolveEffectPool = [];
#currentDissolveIndex = 0;
/**
* @param {*} document Webpage document
* @param {*} zenBoostsChild Boost JSActor child
*/
constructor(document, zenBoostsChild) {
this.document = document;
this.window = document.ownerGlobal;
this.zenBoostsChild = zenBoostsChild;
this.#selectorComponent = new lazy.SelectorComponent(
document,
zenBoostsChild,
this.#zapContentIDs,
this.handleSelectComponentSelect.bind(this),
[{ id: "zen-zap-this" }, { id: "zen-zap-related" }, { id: "zen-zap-cancel" }]
);
// Remove the bottom unzap bar to the safe area
this.#selectorComponent.safeAreaPadding.bottom = 65;
}
/**
* Initializes the zap mode and inserts anonymous content
*/
async initialize() {
if (this.#initialized) {
console.warn("[ZenZapOverlayChild]: Skipping initialize because initialized.");
return;
}
this.#selectorComponent.initialize();
this.#content = this.document.insertAnonymousContent();
this.#content.root.appendChild(this.fragment);
this.#initializeElements();
this.#initialized = true;
}
/**
* Initializes all anonymous content and events
*/
#initializeElements() {
this.zapDoneButton = this.getElementById("zap-done");
this.zapDoneButton.addEventListener("click", this.#disableZapMode.bind(this));
this.#updateZappedList();
}
/**
* Lazily loads the next available dissolve effect.
* The returned effect might not currently be ready to trigger again.
*
* @returns {Promise<ZapDissolve>} Dissolve effect
*/
async #getNextDissolveEffect() {
// Effect does not exist yet, create and initialize
if (this.#currentDissolveIndex >= this.#dissolveEffectPool.length) {
const dissolveEffect = new lazy.ZapDissolve(this.document);
await dissolveEffect.initialize();
this.#dissolveEffectPool.push(dissolveEffect);
}
// Capture current index and increment for next call
const returnIndex = this.#currentDissolveIndex;
this.#currentDissolveIndex = (this.#currentDissolveIndex + 1) % this.#dissolvePoolSize;
return this.#dissolveEffectPool[returnIndex];
}
get content() {
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
return null;
}
return this.#content;
}
/**
* Helper for getting an anonymous element by id
*/
getElementById(id) {
return this.content.root.getElementById(id);
}
get markup() {
// Fetch localizations
let [done] = lazy.overlayLocalization.formatMessagesSync([{ id: "zen-zap-done" }]);
return `
<template>
<link rel="stylesheet" href="chrome://browser/content/zen-styles/content/zen-zap.css" />
<div id="zap-controls-container">
<div id="zap-list">
</div>
<input type="button" id="zap-done" value="${done.value}"/>
</div>
<div id="zap-border"></div>
</template>
`;
}
get fragment() {
if (!this.template) {
let parser = new DOMParser();
let doc = parser.parseFromString(this.markup, "text/html");
this.template = this.document.importNode(doc.querySelector("template"), true);
}
let fragment = this.template.content.cloneNode(true);
return fragment;
}
/**
* Handles the onSelect callback from the SelectComponent
*/
handleSelectComponentSelect(cssSelector) {
this.#handleZap(cssSelector);
}
/**
* Notifies listeners for an update in the zap list
*/
onZapUpdate() {
this.#updateZappedList();
this.zenBoostsChild.sendNotify("zap-list-update");
}
/**
* Handles the addition of the given zap selector
*/
#handleZap(cssPath) {
const useDissolve = Services.prefs.getBoolPref("zen.boosts.dissolve-on-zap");
if (!this.window.gReduceMotion && useDissolve) {
const elements = this.document.querySelectorAll(cssPath);
let counter = 0;
elements.forEach(async (element) => {
if (counter > this.#dissolvePoolSize) {
return;
}
counter++;
this.#getNextDissolveEffect().then((dissolve) => {
dissolve.dissolve(element);
});
});
}
this.zenBoostsChild.addZapSelector(cssPath);
this.onZapUpdate();
}
/**
* Handles the removal of a zap selector
*
* @param {string} cssPath The css selector of the zap
*/
#handleUnzap(cssPath) {
this.zenBoostsChild.removeZapSelector(cssPath);
this.onZapUpdate();
}
/**
* Cancles the current zap operation
*/
#cancelZap() {
this.#selectorComponent.setState(lazy.SelectorComponent.STATES.SELECTING);
}
/**
* Helper function for leaving the zap mode
*/
#disableZapMode() {
this.zenBoostsChild.disableZapMode();
}
/**
* Rebuilds the unzap button list at the bottom of the website
*/
async #updateZappedList() {
const zapList = this.getElementById("zap-list");
zapList.innerHTML = "";
const boost = await this.zenBoostsChild.getWebsiteBoost();
const { boostData } = boost.boostEntry;
boostData.zapSelectors.forEach((selector) => {
const unzapButton = zapList.ownerDocument.createElement("input");
unzapButton.type = "button";
unzapButton.id = "zen-zap-unzap";
const index = boostData.zapSelectors.indexOf(selector) + 1;
const zappedElementsCount =
selector == "" ? 0 : this.document.querySelectorAll(selector).length;
const [tooltip] = lazy.overlayLocalization.formatMessagesSync([
{
id: "zen-unzap-tooltip",
args: { elementCount: zappedElementsCount },
},
]);
unzapButton.value = index;
unzapButton.title = tooltip.value;
unzapButton.setAttribute("index", index);
unzapButton.setAttribute("selector", selector);
zapList.appendChild(unzapButton);
});
// Fetch localizations
let [addZapHelper, removeZapHelper] = lazy.overlayLocalization.formatMessagesSync([
{ id: "zen-add-zap-helper" },
{ id: "zen-remove-zap-helper" },
]);
if (!boostData.zapSelectors.length) {
const addZapHelperText = zapList.ownerDocument.createElement("p");
addZapHelperText.setHTML(addZapHelper.value);
addZapHelperText.classList.add("pcenter");
zapList.appendChild(addZapHelperText);
} else {
const removeZapHelperText = zapList.ownerDocument.createElement("p");
removeZapHelperText.setHTML(removeZapHelper.value);
zapList.appendChild(removeZapHelperText);
}
}
/**
* Handles the mouse enter event for the unzap buttons
*
* @param {Event} event
*/
#unzapButtonHover(event) {
const button = event.originalTarget;
const selector = button.getAttribute("selector");
this.zenBoostsChild.tempShowZappedElement(selector);
button.value = "×";
// This has to run with later, as the elements we are trying to highlight do not exist yet.
// The css has to load first and calculate the bounding boxes for the elements before we can highlight.
this.window.requestAnimationFrame(() => {
const selection = this.document.querySelectorAll(selector);
if (selection.length) {
this.#selectorComponent.showHighlight(selection);
}
});
// Cancle an ongoing select action
this.#cancelZap();
}
/**
* Handles the mouse exit event for the unzap buttons
*
* @param {Event} event
*/
#unzapButtonUnhover(event) {
const button = event.originalTarget;
button.value = button.getAttribute("index");
this.zenBoostsChild.tempHideZappedElement();
this.#selectorComponent.removeHighlight();
}
/**
* Handles button clicks from the unzap list
*
* @param {Event} event
*/
#unzapButtonClick(event) {
const button = event.originalTarget;
const selector = button.getAttribute("selector");
this.zenBoostsChild.tempHideZappedElement();
this.#selectorComponent.removeHighlight();
this.#cancelZap();
// In order to avoid the clicked element being null when the
// SelectorComponent receives it, push the list re-creation to the next frame
this.window.requestAnimationFrame(() => {
this.#handleUnzap(selector);
});
}
/**
* Removes all event listeners and removes the overlay from the Anonymous Content
*/
tearDown() {
this.#selectorComponent.tearDown();
this.#selectorComponent = null;
this.#dissolveEffectPool.forEach((dissolve) => {
dissolve.tearDown();
});
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
} catch {
/* This might fail but that's not an issue */
}
}
this.window = null;
this.document = null;
this.#initialized = false;
}
/**
* This function handles page events while the overlay is active
*
* @param {Event} event The event which will be handled by the overlay
* @param {boolean} prevent True if the event should be prevented
*/
handleEvent(event, prevent) {
switch (event.type) {
case "click":
this.#handleClick(event);
break;
case "mouseover":
this.#handleHoverDelegation(event);
break;
case "mouseout":
this.#handleUnhoverDelegation(event);
}
this.#selectorComponent.handleEvent(event, prevent);
}
/**
* Handles the mouse click event
*
* @param {Event} event Mouse move event params
*/
#handleClick(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonClick(event);
}
}
/**
* Handles the mouse enter event
*
* @param {Event} event Mouse enter event params
*/
#handleHoverDelegation(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonHover(event);
}
}
/**
* Handles the mouse leave event
*
* @param {Event} event Mouse leave event params
*/
#handleUnhoverDelegation(event) {
if (event.originalTarget.id == "zen-zap-unzap") {
this.#unzapButtonUnhover(event);
}
}
}

View File

@@ -0,0 +1,505 @@
// 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 http://mozilla.org/MPL/2.0/.
const AGENT_SHEET = Ci.nsIStyleSheetService.AGENT_SHEET;
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZapOverlay: "resource:///modules/zen/boosts/ZenZapOverlayChild.sys.mjs",
SelectorComponent: "resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs",
});
export class ZenBoostsChild extends JSWindowActorChild {
#currentSheet = null;
#currentState = ZenBoostsChild.STATES.NONE;
#preventableEventsAdded = false;
#zappedElementsTempShown = [];
#overlay = null;
static STATES = {
NONE: "none",
ZAP: "zap",
PICKER: "picker",
};
static OVERLAY_EVENTS = ["click", "pointerdown", "pointermove", "pointerup", "scroll", "resize"];
// A list of events that will be prevented from
// reaching the document
static PREVENTABLE_EVENTS = [
"click",
"pointerdown",
"pointermove",
"pointerup",
"mousemove",
"mousedown",
"mouseup",
"mouseenter",
"mouseover",
"mouseout",
"mouseleave",
"touchstart",
"touchmove",
"touchend",
"dblclick",
"auxclick",
"keypress",
"contextmenu",
"pointerenter",
"pointerover",
"pointerout",
"pointerleave",
];
// Caching the events in sets for performance
static ALL_EVENTS_SET = new Set([
...ZenBoostsChild.OVERLAY_EVENTS,
...ZenBoostsChild.PREVENTABLE_EVENTS,
]);
static PREVENTABLE_SET = new Set(ZenBoostsChild.PREVENTABLE_EVENTS);
/**
* Inverse of https://searchfox.org/firefox-main/rev/1a8c62b86277005f907151bc5389cf5c5091e76f/gfx/src/nsColor.h#23-27
*
* > #define NS_RGBA(_r, _g, _b, _a) \
* > ((nscolor)(((_a) << 24) | ((_b) << 16) | ((_g) << 8) | (_r)))
*
* Converts [r, g, b] array to NSColor
* Make a color out of r,g,b,a values. This assumes that the r,g,b,a
* values are properly constrained to 0-255.
*
* @param {Array} rgb - Array of red, green, blue values [0, 255]
* @param {number} rgb.0 - Red color value [0, 255]
* @param {number} rgb.1 - Green color value [0, 255]
* @param {number} rgb.2 - Blue color value [0, 255]
* @param {number} contrast - Contrast value (default 255)
* @returns {number} NSColor integer representation
*/
#rgbToNSColor([r, g, b], contrast = 255) {
// Note will be using the alpha channel for contrast, since the colors will always
// be fully opaque and we need an extra byte to store the contrast value. This allows
// us to still use an nscolor as parameter instead of having to deal with WebIDL structs
// shenanigans.
return (contrast << 24) | (b << 16) | (g << 8) | r;
}
/**
* From ZenGradientGenerator.mjs
* Converts an HSL color value to RGB. Conversion formula
* adapted from https://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @returns {Array} The RGB representation
*/
#hslToRgb(h, s, l) {
const { round } = Math;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = this.#hueToRgb(p, q, h + 1 / 3);
g = this.#hueToRgb(p, q, h);
b = this.#hueToRgb(p, q, h - 1 / 3);
}
return [round(r * 255), round(g * 255), round(b * 255)];
}
/**
* From ZenGradientGenerator.mjs
* Inverse of hslToRgb
* Converts an RGB color value to HSL. Conversion formula
* adapted from https://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param {number} r The red value
* @param {number} g The green value
* @param {number} b The blue value
* @returns {Array} The HSL representation
*/
#rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let d = max - min;
let h;
if (d === 0) {
h = 0;
} else if (max === r) {
h = ((g - b) / d) % 6;
} else if (max === g) {
h = (b - r) / d + 2;
} else if (max === b) {
h = (r - g) / d + 4;
}
let l = (min + max) / 2;
let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
return [h * 60, s, l];
}
/**
* Handles DOM events for the actor. Applies boost settings when a document
* element is inserted.
*
* @param {Event} event - The DOM event to handle.
*/
handleEvent(event) {
switch (event.type) {
case "unload":
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
this.#removeEventListeners();
break;
case "DOMDocElementInserted":
this.#applyBoostForPageIfAvailable();
break;
default:
break;
}
}
handleZapEvent(event) {
if (ZenBoostsChild.ALL_EVENTS_SET.has(event.type)) {
this.#overlay.handleEvent(event, ZenBoostsChild.PREVENTABLE_SET.has(event.type));
}
}
/**
* Adds necessary event listeners to the document
* to prevent content interactions
*/
#addEventListeners() {
this._handleZapEvent = this.handleZapEvent.bind(this);
this._disableZapMode = this.disableZapMode.bind(this);
for (let event of ZenBoostsChild.OVERLAY_EVENTS) {
this.document.addEventListener(event, this._handleZapEvent, true);
}
for (let event of ZenBoostsChild.PREVENTABLE_EVENTS) {
this.document.addEventListener(event, this._handleZapEvent, true);
}
this.#preventableEventsAdded = true;
}
/**
* Removes the event listeners from the document
*/
#removeEventListeners() {
for (let event of ZenBoostsChild.OVERLAY_EVENTS) {
this.document.removeEventListener(event, this._handleZapEvent, true);
}
if (this.#preventableEventsAdded) {
for (let event of ZenBoostsChild.PREVENTABLE_EVENTS) {
this.document.removeEventListener(event, this._handleZapEvent, true);
}
}
this.#preventableEventsAdded = false;
}
/**
* Handles messages received from the parent actor.
*
* @param {object} message - The message object containing name and data.
*/
async receiveMessage(message) {
switch (message.name) {
case "ZenBoost:BoostDataUpdated": {
const { unloadStyles = false } = message.data || {};
this.#applyBoostForPageIfAvailable(unloadStyles);
break;
}
case "ZenBoost:DisableZapMode":
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
break;
case "ZenBoost:DisablePickerMode":
if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
this.disablePickerMode();
}
break;
case "ZenBoost:ToggleZapMode":
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
this.#startZappingOverlay();
} else if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
this.disableZapMode();
}
break;
case "ZenBoost:TogglePickerMode":
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
this.#startPickingOverlay();
} else if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
this.disablePickerMode();
}
break;
case "ZenBoost:ZapModeEnabled":
return this.#currentState === ZenBoostsChild.STATES.ZAP;
case "ZenBoost:SelectorPickerModeEnabled":
return this.#currentState === ZenBoostsChild.STATES.PICKER;
case "ZenBoost:OpenInspector":
this.sendAsyncMessage("ZenBoost:OpenInspector");
break;
}
return null;
}
/**
* From ZenGradientGenerator.mjs
* Helper function for hslToRgb conversion
*/
#hueToRgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
/**
* Aquires the boost data for this website
*
* @returns {object} Boost data for the current website
*/
getWebsiteBoost() {
const domain = this.browsingContext.topWindow?.location?.host;
if (!domain) {
return null;
}
return this.sendQuery("ZenBoost:GetBoostForDomain", domain);
}
/**
* Applies the boost settings for the current page if available.
*
* @param {boolean} unloadStyles - Indicates whether to unload styles.
*/
async #applyBoostForPageIfAvailable(unloadStyles = false) {
const browsingContext = this.browsingContext;
// Prevent applying boosts to iframes or non-top-level browsing contexts.
// It makes the tab crash if we try to load stylesheets into an iframe's
if (!browsingContext || browsingContext.parent !== null) {
return;
}
const boost = await this.getWebsiteBoost();
if (unloadStyles) {
this.#unloadCurrentStyleSheet();
}
if (boost) {
const { boostData } = boost.boostEntry;
if (boost.styleSheet) {
this.#loadStyleSheet(boost.styleSheet);
}
browsingContext.isZenBoostsInverted = boostData.smartInvert;
if (boostData.enableColorBoost) {
if (boostData.autoTheme) {
// Workspace color is converted to the HSL color space
let primaryGradientColor = boost.workspaceGradient[0]?.c ?? [0, 0, 0.6];
boost.workspaceGradient.forEach((color) => {
if (color.isPrimary) {
primaryGradientColor = this.#rgbToHsl(color.c[0], color.c[1], color.c[2]);
}
});
// Workspace color is converted back to rgb
// using the same modifiers as the color above
primaryGradientColor = this.#hslToRgb(
primaryGradientColor[0] / 360,
primaryGradientColor[1] * (1 - boostData.saturation),
0.1 + primaryGradientColor[2] * 0.6 * boostData.brightness
);
const rgbColor = primaryGradientColor;
const nsColor = this.#rgbToNSColor(rgbColor, (1 - boostData.contrast) * 255);
browsingContext.zenBoostsData = nsColor;
} else {
let colorWheelColor = this.#hslToRgb(
boostData.dotAngleDeg / 360,
/* already is [0, 1] */
boostData.dotDistance * (1 - boostData.saturation),
/* lightness range from [0.1, 0.7] */
0.1 + boostData.dotDistance * 0.6 * boostData.brightness
);
const rgbColor = colorWheelColor;
const nsColor = this.#rgbToNSColor(rgbColor, (1 - boostData.contrast) * 255);
browsingContext.zenBoostsData = nsColor;
}
return;
}
}
browsingContext.zenBoostsData = 0;
}
/**
* Loads the given stylesheet into the website
*
* @param {object} styleSheet The stylesheet
*/
#loadStyleSheet(styleSheet) {
const browsingContext = this.browsingContext;
styleSheet.uri = Services.io.newURI(styleSheet.uri);
if (this.#currentSheet?.uuid !== styleSheet.uuid) {
if (this.#currentSheet) {
this.#unloadCurrentStyleSheet();
}
browsingContext.window.windowGlobalChild.browsingContext.window.windowUtils.loadSheet(
styleSheet.uri,
AGENT_SHEET
);
this.#currentSheet = styleSheet;
}
}
/**
* Unloads the currently loaded stylesheet
*/
#unloadCurrentStyleSheet() {
const browsingContext = this.browsingContext;
if (this.#currentSheet && browsingContext) {
browsingContext.window.windowGlobalChild.browsingContext.window.windowUtils.removeSheet(
this.#currentSheet.uri,
AGENT_SHEET
);
this.#currentSheet = null;
}
}
async #startZappingOverlay() {
if (this.#currentState === ZenBoostsChild.STATES.ZAP) {
return;
}
this.#currentState = ZenBoostsChild.STATES.ZAP;
this.#overlay = new lazy.ZapOverlay(this.document, this);
this.#overlay.initialize();
this.#addEventListeners();
this.sendNotify("zap-state-update");
}
async #startPickingOverlay() {
if (this.#currentState === ZenBoostsChild.STATES.PICKER) {
return;
}
this.#currentState = ZenBoostsChild.STATES.PICKER;
this.#overlay = new lazy.SelectorComponent(
this.document,
this,
[], // No additional IDs needed
this.onPickerSelection.bind(this)
);
this.#overlay.initialize();
this.#addEventListeners();
this.sendNotify("selector-picker-state-update", "onenable");
}
onPickerSelection(cssSelector) {
this.sendNotify("selector-picker-picked", cssSelector);
}
addZapSelector(selector) {
const domain = this.browsingContext.topWindow?.location?.host;
this.sendQuery("ZenBoost:ZapSelector", {
action: "add",
selector,
domain,
});
}
removeZapSelector(selector) {
const domain = this.browsingContext.topWindow?.location?.host;
this.sendQuery("ZenBoost:ZapSelector", {
action: "remove",
selector,
domain,
});
}
async tempShowZappedElement(selector) {
this.document.querySelectorAll(selector).forEach((element) => {
element.setAttribute("zen-zap-unhide", "true");
});
if (!this.#zappedElementsTempShown.includes(selector)) {
this.#zappedElementsTempShown.push(selector);
}
}
async tempHideZappedElement() {
this.#zappedElementsTempShown.forEach((selector) => {
this.document.querySelectorAll(selector).forEach((element) => {
element.removeAttribute("zen-zap-unhide");
});
});
this.#zappedElementsTempShown = [];
}
disableZapMode() {
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
return;
}
this.#currentState = ZenBoostsChild.STATES.NONE;
this.#overlay?.tearDown();
this.#overlay = null;
this.#removeEventListeners();
this.sendNotify("zap-state-update");
}
disablePickerMode() {
if (this.#currentState === ZenBoostsChild.STATES.NONE) {
return;
}
this.#currentState = ZenBoostsChild.STATES.NONE;
this.#overlay?.tearDown();
this.#overlay = null;
this.#removeEventListeners();
this.sendNotify("selector-picker-state-update", "ondisable");
}
sendNotify(topic, msg = null) {
this.sendAsyncMessage("ZenBoost:Notify", { topic, msg });
}
}

View File

@@ -0,0 +1,159 @@
// 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 http://mozilla.org/MPL/2.0/.
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
});
export class ZenBoostsParent extends JSWindowActorParent {
static OBSERVERS = [
"zen-boosts-update",
"zen-space-gradient-update",
"zen-boosts-disable-zap",
"zen-boosts-disable-picker",
];
/**
* Creates a new ZenBoostsParent actor instance and sets up an observer
* for boost update notifications.
*/
constructor() {
super();
this._observe = this.observe.bind(this);
ZenBoostsParent.OBSERVERS.forEach((observe) => {
Services.obs.addObserver(this._observe, observe);
});
}
/**
* Called when the actor is destroyed. Cleans up the observer.
*/
didDestroy() {
ZenBoostsParent.OBSERVERS.forEach((observe) => {
Services.obs.removeObserver(this._observe, observe);
});
}
/**
* Observer callback that handles boost update notifications.
* Sends a message to child actors when boosts are updated.
*
* @param {object} subject - The subject of the notification.
* @param {string} topic - The topic of the notification.
*/
observe(subject, topic) {
switch (topic) {
case "zen-boosts-update":
case "zen-space-gradient-update":
this.sendAsyncMessage("ZenBoost:BoostDataUpdated", { unloadStyles: true });
break;
case "zen-boosts-disable-zap":
this.sendAsyncMessage("ZenBoost:DisableZapMode");
break;
case "zen-boosts-disable-picker":
this.sendAsyncMessage("ZenBoost:DisablePickerMode");
break;
}
}
/**
* Handles messages received from child actors.
* Retrieves boost data for a domain when requested.
*
* @param {object} message - The message object containing name and data.
* @returns {Promise<object | null>} A promise that resolves to the boost data or null.
*/
async receiveMessage(message) {
switch (message.name) {
case "ZenBoost:OpenInspector": {
const { require } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
);
const { gDevTools } = require("devtools/client/framework/devtools");
let win = Services.wm.getMostRecentWindow("navigator:browser");
let tab = win.gBrowser.selectedTab;
let toolbox = gDevTools.getToolboxForTab(tab);
if (toolbox) {
await gDevTools.closeToolboxForTab(tab);
} else {
await gDevTools.showToolboxForTab(tab, "inspector");
}
break;
}
case "ZenBoost:Notify": {
Services.obs.notifyObservers(null, message.data.topic, message.data.msg);
break;
}
case "ZenBoost:ZapSelector": {
const data = message.data;
if (!data.action) {
break;
}
if (!data.selector) {
break;
}
if (!data.domain) {
break;
}
if (data.action == "add") {
lazy.gZenBoostsManager.addZapSelectorToActive(data.selector, data.domain);
} else if (data.action == "remove") {
lazy.gZenBoostsManager.removeZapSelectorToActive(data.selector, data.domain);
} else if (data.action == "clear") {
lazy.gZenBoostsManager.clearZapSelectorsForActive(data.domain);
}
break;
}
case "ZenBoost:GetBoostForDomain": {
const domain = message.data;
const embedder = this.browsingContext.top.embedderElement;
if (!embedder || !domain) {
break;
}
const exists = lazy.gZenBoostsManager.registeredBoostForDomain(domain);
if (!exists) {
break;
}
const topWindowIsDarkMode =
embedder.ownerGlobal.getComputedStyle(embedder).colorScheme === "dark";
const boost = lazy.gZenBoostsManager.loadActiveBoostFromStore(domain);
const currentWorkspace =
await this.browsingContext.topChromeWindow.gZenWorkspaces.getActiveWorkspace();
const styleData = await lazy.gZenBoostsManager.getStyleSheetForBoost(domain);
return {
...boost,
topWindowIsDarkMode,
workspaceGradient: currentWorkspace.theme.gradientColors,
styleSheet: styleData
? {
uuid: styleData.uuid,
uri: styleData.uri.spec,
}
: null,
};
}
default: {
console.warn(`[ZenBoostsParent]: Unknown message: ${message.name}`);
break;
}
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
# 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 http://mozilla.org/MPL/2.0/.
Classes = [
{
'cid': '{7d46aa4e-7486-4f77-ab47-81125f1a5723}',
'interfaces': ['nsIZenBoostsBackend'],
'contract_ids': ['@mozilla.org/zen/boosts-backend;1'],
'type': 'zen::nsZenBoostsBackend',
'headers': ['mozilla/nsZenBoostsBackend.h'],
'js_name': 'boosts',
'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
},
]

14
src/zen/boosts/jar.inc.mn Normal file
View File

@@ -0,0 +1,14 @@
# 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 http://mozilla.org/MPL/2.0/.
content/browser/zen-styles/zen-boosts.css (../../zen/boosts/zen-boosts.css)
content/browser/zen-styles/content/zen-zap.css (../../zen/boosts/zen-zap.css)
content/browser/zen-styles/content/zen-selector.css (../../zen/boosts/zen-selector.css)
content/browser/zen-styles/zen-advanced-color-options.css (../../zen/boosts/zen-advanced-color-options.css)
# Windows
* content/browser/zen-components/windows/zen-boost-editor.xhtml (../../zen/boosts/zen-boost-editor.xhtml)
# Images
content/browser/zen-images/boost-indicator.svg (../../zen/images/boost-indicator.svg)

36
src/zen/boosts/moz.build Normal file
View File

@@ -0,0 +1,36 @@
# 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 http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen.boosts += [
"ZenBoostsEditor.mjs",
"ZenBoostsManager.sys.mjs",
"ZenBoostStyles.sys.mjs",
"ZenSelectorComponent.sys.mjs",
"ZenZapDissolve.sys.mjs",
"ZenZapOverlayChild.sys.mjs",
]
FINAL_TARGET_FILES.actors += [
"actors/ZenBoostsChild.sys.mjs",
"actors/ZenBoostsParent.sys.mjs",
]
XPIDL_SOURCES += [
"nsIZenBoostsBackend.idl",
]
EXPORTS.mozilla += [
"nsZenBoostsBackend.h",
]
SOURCES += [
"nsZenBoostsBackend.cpp",
]
XPCOM_MANIFESTS += [
"components.conf",
]
FINAL_LIBRARY = "xul"
XPIDL_MODULE = "zen_boosts"

View File

@@ -0,0 +1,19 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
/**
* @brief Interface for Zen boosts backend.
*/
[scriptable, uuid(7d46aa4e-7486-4f77-ab47-81125f1a5723)]
interface nsIZenBoostsBackend : nsISupports {
%{C++
/*
* @brief Called when the presshell is entered. See nsDisplayListBuilder::EnterPresShell
* for context.
*/
auto onPresShellEntered(nsPresContext* aPresContext) -> void;
%}
};

View File

@@ -0,0 +1,297 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#include "nsZenBoostsBackend.h"
#include "nsIXULRuntime.h"
#include "nsPresContext.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/ServoStyleConsts.h"
#include "mozilla/ServoStyleConstsInlines.h"
#include "mozilla/MediaFeatureChange.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/BrowsingContext.h"
#define COLOR_CHANNEL_MIDPOINT 128
// It's a bit of a hacky solution, but instead of using alpha as what it is
// (opacity), we use it to store contrast information for now.
// We do this primarily to avoid having to deal with WebIDL structs and
// serialization/deserialization between parent and content processes.
#define NS_GET_CONTRAST(_c) NS_GET_A(_c)
#define MARK_MEDIA_FEATURE_CHANGED(_pc) \
(_pc)->MediaFeatureValuesChanged( \
{mozilla::RestyleHint::RecascadeSubtree(), NS_STYLE_HINT_VISUAL, \
mozilla::MediaFeatureChangeReason::PreferenceChange}, \
mozilla::MediaFeatureChangePropagation::All);
#define TRIGGER_PRES_CONTEXT_RESTYLE() \
WalkPresContexts([&](nsPresContext* aPc) { \
MARK_MEDIA_FEATURE_CHANGED(aPc); \
});
using BrowsingContext = mozilla::dom::BrowsingContext;
template <typename Callback>
void BrowsingContext::WalkPresContexts(Callback&& aCallback) {
PreOrderWalk([&](BrowsingContext* aContext) {
if (nsIDocShell* shell = aContext->GetDocShell()) {
if (RefPtr pc = shell->GetPresContext()) {
aCallback(pc.get());
}
}
});
}
/**
* @brief Called when the ZenBoostsData field is set on a browsing context.
* Triggers a restyle if the boost data has changed.
* @param aOldValue The previous value of the boost data.
*/
void BrowsingContext::DidSet(FieldIndex<IDX_ZenBoostsData>,
ZenBoostData aOldValue) {
MOZ_ASSERT(IsTop());
if (ZenBoostsData() == aOldValue) {
return;
}
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
/**
* @brief Called when the IsZenBoostsInverted field is set on a browsing
* context. Triggers a restyle if the value has changed.
* @param aOldValue The previous value of the IsZenBoostsInverted flag.
*/
void BrowsingContext::DidSet(FieldIndex<IDX_IsZenBoostsInverted>,
bool aOldValue) {
MOZ_ASSERT(IsTop());
if (IsZenBoostsInverted() == aOldValue) {
return;
}
PresContextAffectingFieldChanged();
TRIGGER_PRES_CONTEXT_RESTYLE();
}
namespace zen {
namespace {
/**
* @brief Clamps a value to the range [0, 255] using branchless operations.
* @param v The value to clamp.
* @return The clamped value in the range [0, 255].
*/
static __inline int32_t clamp255(int32_t v) {
// llvm x86 is poor at ternary operator, so use branchless min/max.
v = v & ~(v >> 31);
return (v | ((255 - v) >> 31)) & 255;
}
/**
* @brief Applies a color filter to transform an original color toward an accent
* color. Preserves the original color's perceived luminance while shifting
* hue/chroma toward the accent. Uses the alpha channel of the accent color to
* store contrast information.
* @param aOriginalColor The original color to filter.
* @param aAccentColor The accent color to filter toward (alpha channel contains
* contrast value).
* @return The filtered color with transformations applied.
*/
static nscolor zenFilterColorChannel(nscolor aOriginalColor,
nscolor aAccentColor) {
const auto r1 = NS_GET_R(aOriginalColor);
const auto g1 = NS_GET_G(aOriginalColor);
const auto b1 = NS_GET_B(aOriginalColor);
const auto a1 = NS_GET_A(aOriginalColor);
if (a1 == 0) {
// Skip processing fully transparent colors since they won't be visible and
// we want to avoid unnecessary computations with the accent color's alpha
// channel used for contrast information.
return aOriginalColor;
}
const auto r2 = NS_GET_R(aAccentColor);
const auto g2 = NS_GET_G(aAccentColor);
const auto b2 = NS_GET_B(aAccentColor);
// It's a bit of a hacky solution, but instead of using alpha as what it is
// (opacity), we use it to store contrast information for now.
// We do this primarily to avoid having to deal with WebIDL structs and
// serialization/deserialization between parent and content processes.
const auto contrast = NS_GET_CONTRAST(aAccentColor);
// Approximate perceived luminance in sRGB space
// Coefficients per Rec.709; gamma correction ignored for speed
const double origLum = 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
const double accentLum = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2;
double scale = 1.0;
// The scale explodes for very small values of the luminance
// so to counteract that we simply don't calculate it
if (accentLum > 1e-5) {
scale = origLum / accentLum;
// Limit the scale factor
scale = std::clamp(scale, 0.0, 4.0);
}
double fr = r2 * scale;
double fg = g2 * scale;
double fb = b2 * scale;
// Apply contrast adjustment: map contrast from 0255 to -1.0+1.0
// contrast = 0: maximum darkening (mix toward black)
// contrast = 127.5: no change
// contrast = 255: maximum lightening (mix toward white)
const double contrastFactor = (contrast - 128.0) / 128.0;
// Compute perceived luminance for the filtered color
const double lum = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb;
// If it's bright, mix toward white; if dark, mix toward black
if (lum >= COLOR_CHANNEL_MIDPOINT) {
const double mix = (lum - COLOR_CHANNEL_MIDPOINT) / COLOR_CHANNEL_MIDPOINT;
const double amount = contrastFactor * mix;
fr = fr + (255.0 - fr) * amount;
fg = fg + (255.0 - fg) * amount;
fb = fb + (255.0 - fb) * amount;
} else {
const double mix = (COLOR_CHANNEL_MIDPOINT - lum) / COLOR_CHANNEL_MIDPOINT;
const double amount = -contrastFactor * mix;
fr = fr * (1.0 - amount);
fg = fg * (1.0 - amount);
fb = fb * (1.0 - amount);
}
const uint8_t fr8 = clamp255(fr);
const uint8_t fg8 = clamp255(fg);
const uint8_t fb8 = clamp255(fb);
return NS_RGBA(fr8, fg8, fb8, a1);
}
/**
* @brief Inverts a color by inverting each RGB channel while preserving
* perceived luminance. This is done by inverting the color and then shifting it
* based on the sum of the inverted channels.
* @param aColor The color to invert.
* @return The inverted color with luminance preservation.
*/
inline static nscolor zenInvertColorChannel(nscolor aColor) {
const auto r = NS_GET_R(aColor);
const auto g = NS_GET_G(aColor);
const auto b = NS_GET_B(aColor);
const auto a = NS_GET_A(aColor);
if (a == 0) {
// Skip processing fully transparent colors since they won't be visible and
// we want to avoid unnecessary computations.
return aColor;
}
const auto rInv = 255 - r;
const auto gInv = 255 - g;
const auto bInv = 255 - b;
const auto max = std::max({rInv, gInv, bInv});
const auto min = std::min({rInv, gInv, bInv});
const auto sum = max + min;
const auto rShifted = sum - rInv;
const auto gShifted = sum - gInv;
const auto bShifted = sum - bInv;
return NS_RGBA(rShifted, gShifted, bShifted, a);
}
/**
* @brief Retrieves the current boost data from the browsing context.
*/
inline static void GetZenBoostsDataFromBrowsingContext(ZenBoostData* aData,
bool* aIsInverted,
nsPresContext* aPresContext = nullptr) {
auto zenBoosts = nsZenBoostsBackend::GetInstance();
if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent)) {
return;
}
if (aPresContext) {
if (auto document = aPresContext->Document()) {
if (auto browsingContext = document->GetBrowsingContext()) {
*aData = browsingContext->ZenBoostsData();
*aIsInverted = browsingContext->IsZenBoostsInverted();
}
}
} else if (auto currentBrowsingContext = zenBoosts->GetCurrentBrowsingContext()) {
*aData = currentBrowsingContext->ZenBoostsData();
*aIsInverted = currentBrowsingContext->IsZenBoostsInverted();
}
}
} // namespace
// Use the macro to inject all of the definitions for nsISupports.
NS_IMPL_ISUPPORTS(nsZenBoostsBackend, nsIZenBoostsBackend)
nsZenBoostsBackend::nsZenBoostsBackend() {};
auto nsZenBoostsBackend::GetInstance() -> nsCOMPtr<nsZenBoostsBackend> {
static nsCOMPtr<zen::nsZenBoostsBackend> zenBoosts(
do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID));
return zenBoosts;
}
auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
-> void {
// Note that aDocument can be null when entering anonymous content frames.
// We explicitly do this to prevent applying boosts to anonymous content, such
// as devtools or screenshots.
auto browsingContext = aDocument ? aDocument->GetBrowsingContext() : nullptr;
if (!browsingContext) {
return;
}
mCurrentBrowsingContext = browsingContext;
}
auto nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext) -> nscolor {
if (!XRE_IsContentProcess()) {
// Zen boosts are only supported in content, so if we somehow end up here
// without a prescontext or in the parent process, just return the original
// color.
return aColor;
}
ZenBoostData accentNS = 0;
bool invertColors = false;
GetZenBoostsDataFromBrowsingContext(&accentNS, &invertColors, aPresContext);
if (accentNS) {
// Apply a filter-like tint:
// - Preserve the original color's perceived luminance
// - Map hue/chroma toward the accent by scaling the accent's RGB
// to match the original luminance
// - Keep the original alpha
// Convert both colors to nscolor to access channels
aColor = zenFilterColorChannel(aColor, (nscolor)accentNS);
}
if (invertColors) {
aColor = zenInvertColorChannel(aColor);
}
return aColor;
}
auto nsZenBoostsBackend::ResolveStyleColor(mozilla::StyleAbsoluteColor aColor)
-> mozilla::StyleAbsoluteColor {
if (aColor.alpha == 0) {
// Skip processing fully transparent colors since they won't be visible and
// we want to avoid unnecessary computations. This also prevents issues with
// using the alpha channel for contrast information in the accent color.
return aColor;
}
const auto resultColor = FilterColorFromPresContext(aColor.ToColor());
aColor = mozilla::StyleAbsoluteColor::FromColor(resultColor);
return aColor;
}
} // namespace zen

View File

@@ -0,0 +1,81 @@
/* 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 http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ZenBoostsBackend_h__
#define mozilla_ZenBoostsBackend_h__
#include "nsColor.h"
#include "nsPresContext.h"
#include "nsIZenBoostsBackend.h"
#include "mozilla/RefPtr.h"
#define ZEN_BOOSTS_BACKEND_CONTRACTID "@mozilla.org/zen/boosts-backend;1"
using ZenBoostData = nscolor; // For now, Zen boosts data is just a color.
namespace zen {
class nsZenBoostsBackend final : public nsIZenBoostsBackend {
NS_DECL_ISUPPORTS
public:
explicit nsZenBoostsBackend();
/**
* Indicates whether the current frame being rendered is for anonymous
* content.
*/
bool mCurrentFrameIsAnonymousContent = false;
/**
* @brief Resolve a StyleAbsoluteColor to take into account Zen boosts.
* @param aColor The color to resolve.
* @return The resolved color with Zen boost filters applied, or the original
* color if no boost is active.
* @see StyleColor::ResolveColor for reference.
*/
static auto ResolveStyleColor(mozilla::StyleAbsoluteColor aColor)
-> mozilla::StyleAbsoluteColor;
/**
* @brief Filter a color based on the current Zen boost settings.
* @param aColor The color to filter.
* @param aPresContext The presentation context to use for filtering.
* @return The filtered color.
*/
static auto FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext = nullptr) -> nscolor;
/**
* @brief Called when a presshell is entered during rendering.
* @param aPresContext The presentation context that was entered.
*/
auto onPresShellEntered(mozilla::dom::Document* aDocument) -> void;
[[nodiscard]]
inline auto GetCurrentBrowsingContext() const {
return mCurrentBrowsingContext;
}
NS_DECL_NSIZENBOOSTSBACKEND
private:
~nsZenBoostsBackend() = default;
/**
* The presshell of the current document being rendered.
*/
RefPtr<mozilla::dom::BrowsingContext> mCurrentBrowsingContext;
public:
/**
* @brief Get the singleton instance of the ZenBoostsBackend.
* @return The singleton instance.
*/
static auto GetInstance() -> nsCOMPtr<nsZenBoostsBackend>;
};
} // namespace zen
#endif

View File

@@ -0,0 +1,52 @@
/*
* 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 http://mozilla.org/MPL/2.0/.
*/
#zen-boost-advanced-color-options-panel {
color-scheme: light;
-zen-window-transform-origin: 50% 0%;
padding: 8px 8px;
& p {
color: #3a3a3b;
}
& input {
width: 185px;
appearance: none !important;
height: 4px;
background-color: #b5b5b9;
border-radius: 2px;
margin-top: 10px;
}
& separator {
height: 24px !important;
}
@media (-moz-platform: macos) {
&[animate="open"] {
animation: 0.4 zen-color-options-panel-animation-macos cubic-bezier(0.14, 1.43, 0.56, 1.01) forwards !important;
}
}
}
#zen-boost-advanced-color-options-panel input::-moz-range-thumb {
width: 18px;
height: 18px;
background-color: #e6e5ea;
border: solid 1px #d1d0d5;
border-radius: 100%;
}
#zen-boost-advanced-color-options-panel input::-moz-range-thumb:active {
background-color: #cbcad0;
}
#zen-boost-advanced-color-options-container {
flex-direction: column;
}

View File

@@ -0,0 +1,151 @@
#filter substitution
<?xml version="1.0"?>
# -*- Mode: HTML -*-
#
# 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 http://mozilla.org/MPL/2.0/.
<!DOCTYPE window>
<html
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns="http://www.w3.org/1999/xhtml"
id="zenBoostWindow"
windowtype="Zen:BoostEditor"
alwaysontop="true"
customtitlebar="true"
sizemode="normal"
scrolling="false"
macanimationtype="document"
windowsmica="true"
data-l10n-sync="true">
<head>
<link rel="stylesheet" href="chrome://browser/skin/" />
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-boosts.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-buttons.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-theme.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-animations.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-panel-ui.css" />
<link rel="stylesheet" href="chrome://browser/skin/zen-icons/icons.css" />
<link rel="stylesheet" href="chrome://browser/content/zen-styles/zen-advanced-color-options.css" />
<link rel="localization" href="browser/zen-boosts.ftl"/>
<!-- Loading in the window module -->
<script>
const { nsZenBoostEditor } = ChromeUtils.importESModule( "resource:///modules/zen/boosts/ZenBoostsEditor.mjs" );
window.addEventListener("load", () => {
window.boostEditor = new nsZenBoostEditor(document, window.domain, window, window.openerWindow);
});
</script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox flex="1" id="zen-boost-editor-root">
<hbox id="zen-boost-head-wrapper">
<button id="zen-boost-close" class="subviewbutton mod-button title-button"></button>
<hbox id="zen-boost-name" flex="1">
<hbox id="zen-boost-name-container">
<html:p id="zen-boost-name-text"></html:p>
</hbox>
</hbox>
<button data-l10n-id="zen-boost-shuffle" id="zen-boost-shuffle" class="subviewbutton mod-button title-button"></button>
</hbox>
<vbox flex="1" id="zen-boost-filter-wrapper">
<hbox class="zen-boost-color-picker-gradient zen-boost-panel-disabled">
<button data-l10n-id="zen-boost-magic-theme" id="zen-boost-magic-theme" class="subviewbutton mod-button"></button>
<html:div class="zen-boost-color-picker-dot"></html:div>
<html:div class="zen-boost-color-picker-circle"></html:div>
</hbox>
<hbox flex="1" id="zen-boost-toolbar-wrapper-colors">
<button data-l10n-id="zen-boost-invert" id="zen-boost-invert" class="subviewbutton mod-button small"></button>
<button data-l10n-id="zen-boost-controls" id="zen-boost-controls" class="subviewbutton mod-button small"></button>
<button data-l10n-id="zen-boost-disable" id="zen-boost-disable" class="subviewbutton mod-button small"></button>
</hbox>
<html:div id="zen-boost-font-wrapper">
<vbox id="zen-boost-font-grid">
<!-- Font buttons will be injected here -->
</vbox>
<html:div class="visible-separator"></html:div>
<hbox flex="1" id="zen-boost-font-toolbar">
<html:select name="font" id="zen-boost-font-select" class="mod-button">
<!-- Additional font options will be injected here -->
</html:select>
<button data-l10n-id="zen-boost-text-case-toggle" id="zen-boost-text-case-toggle" class="subviewbutton mod-button"></button>
</hbox>
</html:div>
<button id="zen-boost-zap" class="subviewbutton mod-button big-button toggleable-button">
<html:p data-l10n-id="zen-boost-zap" id="zen-boost-zap-text"></html:p>
<html:p id="zen-boost-zap-value"></html:p>
</button>
<button id="zen-boost-code" class="subviewbutton mod-button big-button">
<html:p data-l10n-id="zen-boost-code" id="zen-boost-code-text"></html:p>
</button>
<hbox flex="1" id="zen-boost-toolbar-wrapper">
<button data-l10n-id="zen-boost-save" id="zen-boost-save" class="subviewbutton mod-button med"></button>
<button data-l10n-id="zen-boost-load" id="zen-boost-load" class="subviewbutton mod-button med"></button>
</hbox>
</vbox>
</vbox>
<vbox flex="1" id="zen-boost-code-editor-root">
<hbox id="zen-boost-code-top-bar">
<button id="zen-boost-back" class="subviewbutton mod-button big-button">
<html:p data-l10n-id="zen-boost-back" id="zen-boost-back-text"></html:p>
</button>
</hbox>
<vbox flex="1" id="zen-boost-code-editor">
</vbox>
<hbox id="zen-boost-code-bottom-bar">
<button data-l10n-id="zen-boost-css-picker" id="zen-boost-css-picker" class="subviewbutton mod-button big-button toggleable-button"></button>
<button data-l10n-id="zen-boost-css-inspector" id="zen-boost-css-inspector" class="subviewbutton mod-button big-button"></button>
</hbox>
</vbox>
<popupset id="mainPopupSet">
<panel type="arrow" popupalign="topmiddle" id="zen-boost-advanced-color-options-panel">
<html:div flex="1" id="zen-boost-advanced-color-options-container">
<p data-l10n-id="zen-bootst-color-contrast"></p>
<html:input id="zen-boost-color-contrast" type="range" min="0" max="1" value="0.5" step="0.01"/>
<separator />
<p data-l10n-id="zen-bootst-color-brightness"></p>
<html:input id="zen-boost-color-brightness" type="range" min="0" max="1" value="0.5" step="0.01"/>
<separator />
<p data-l10n-id="zen-bootst-color-original-saturation"></p>
<html:input id="zen-boost-color-saturation" type="range" min="0" max="1" value="0.5" step="0.01"/>
</html:div>
</panel>
</popupset>
<menupopup id="zenBoostContextMenu">
<menuitem data-l10n-id="zen-boost-edit-rename" id="zen-boost-edit-rename" command="cmd_zenBoostEditName" />
<menuitem data-l10n-id="zen-boost-edit-shuffle" id="zen-boost-edit-shuffle" command="cmd_zenBoostShuffle" />
<menuitem data-l10n-id="zen-boost-edit-reset" id="zen-boost-edit-reset" command="cmd_zenBoostReset" />
<menuseparator/>
<menuitem data-l10n-id="zen-boost-edit-delete" id="zen-boost-edit-delete" command="cmd_zenBoostDelete" />
</menupopup>
<commandset id="zenBoostCommandSet">
<command id="cmd_zenBoostEditName"
oncommand="window.boostEditor.editBoostName();" />
<command id="cmd_zenBoostShuffle"
oncommand="window.boostEditor.shuffleBoost();" />
<command id="cmd_zenBoostReset"
oncommand="window.boostEditor.resetBoost();" />
<command id="cmd_zenBoostDelete"
oncommand="window.boostEditor.deleteBoost();" />
</commandset>
</html:body>
</html>

View File

@@ -0,0 +1,789 @@
/*
* 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 http://mozilla.org/MPL/2.0/.
*/
#zenBoostWindow {
/* For the mica effect we want a white tint */
@media not (-moz-platform: linux) {
color-scheme: light;
}
appearance: none;
}
.source-editor-frame {
max-width: 1000px;
max-height: 1000px;
width: 100%;
height: 100%;
border: none;
}
#zen-boost-code-top-bar {
top: 0;
z-index: 10;
width: 100%;
height: 40px;
background-color: #f6f6f8;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
}
border: solid 0px #ededef;
border-bottom-width: 1px;
}
#zen-boost-name-container {
background-image: url("chrome://global/skin/icons/arrow-down.svg");
background-repeat: no-repeat;
background-position: calc(100% - 2px) calc(50% + 2px);
background-size: 12px 12px;
padding: 4px 18px 4px 2px;
margin-left: 4px;
border-radius: 8px;
opacity: 0.75;
background-color: transparent;
transition:
0.25s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.25s background-color cubic-bezier(0.075, 0.82, 0.165, 1);
-moz-window-dragging: no-drag;
}
#zen-boost-name-container:hover {
opacity: 0.8;
background-color: #aaaaaa22;
}
#zen-boost-code-bottom-bar {
bottom: 0;
z-index: 10;
width: 100%;
height: 60px;
background-color: #f6f6f8;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
}
border: solid 0px #ededef;
border-top-width: 1px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
}
#zen-boost-code-editor {
width: 100%;
height: calc(100% - 100px);
margin-top: 0;
margin-bottom: 0;
}
body {
overflow: clip;
display: flex;
margin: 0;
width: 100%;
}
#zen-boost-editor-root,
#zen-boost-code-editor-root {
user-select: none;
width: 100%;
height: 100%;
padding: 0px;
margin: 0px;
& button {
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 10px;
}
padding: 0 !important;
min-width: fit-content !important;
border: solid 6px transparent;
transition: background 0.1s cubic-bezier(0.075, 0.82, 0.165, 1);
appearance: none;
&:active {
transform: none !important;
}
&:hover {
opacity: 0.8;
}
}
& .button-text {
display: none;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.subviewbutton {
color: #3a3a3b;
}
#zen-boost-editor-view {
gap: 10px;
}
#zen-boost-code-top-bar {
-moz-window-dragging: drag; /* Allow dragging the window with the custom titlebar for the code view */
}
#zen-boost-head-wrapper {
-moz-window-dragging: drag; /* Allow dragging the window with the custom titlebar */
margin-bottom: -10px;
height: 40px;
min-height: 40px;
max-height: 40px;
align-items: center;
background-color: #f6f6f8;
border: solid 1px #e7e7e7ab;
@media (-moz-windows-mica) {
background-color: #f6f6f8c0;
border: none;
}
display: flex;
flex-direction: row-reverse;
@media (-moz-platform: macos) {
flex-direction: row;
}
& button {
height: 24px !important;
width: 24px !important;
aspect-ratio: 1/1 !important;
background-color: transparent;
opacity: 0.45;
transition: 0.25s opacity cubic-bezier(0.075, 0.82, 0.165, 1);
&:hover {
opacity: 0.6;
}
}
& #zen-boost-name {
transition: color 0.2s ease-in-out;
background-color: transparent;
border: none;
outline: none;
appearance: none;
display: flex;
align-items: center;
}
}
#zen-boost-name-text {
width: 100%;
justify-content: center;
font-size: 100%;
font-weight: 600;
max-width: 100px;
overflow: hidden;
}
#zen-boost-name-text {
text-align: center;
margin: auto !important;
height: min-content;
}
#zen-boost-font-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0px;
width: 100%;
padding: 2px;
& button {
padding: auto;
margin: auto;
background-color: transparent;
}
}
#zen-boost-filter-wrapper {
padding: 20px;
gap: 14px;
margin-top: 10px;
padding-top: 10px;
background-color: #fcfcfe;
-moz-window-dragging: drag;
& > * {
-moz-window-dragging: no-drag;
}
}
.big-button {
width: 100% !important;
margin: auto;
text-indent: 6px;
vertical-align: middle;
font-size: 10pt;
list-style-position: right;
& p {
width: 75%;
margin: auto;
}
}
#zen-boost-back {
width: 60px !important;
margin: 5px;
left: 5px;
display: flex;
flex-direction: row-reverse;
& hbox {
scale: 0.75;
margin-right: -7px;
margin-top: 2px;
}
}
#zen-boost-code-bottom-bar button {
content: "";
width: auto !important;
aspect-ratio: 1/1 !important;
margin: 5px;
left: 5px;
}
#zen-boost-zap {
& #zen-boost-zap-value {
text-align: right;
right: 8px;
margin: auto;
position: relative;
}
& hbox {
margin-right: 8px;
}
}
#zen-boost-zap[hideicon="true"] {
list-style: none;
list-style-type: none;
& hbox {
margin-right: 0px;
}
}
#zen-boost-size {
list-style-type: none;
& #zen-boost-size-value {
text-align: right;
right: 8px;
margin: auto;
position: relative;
}
}
.zen-boost-panel-disabled {
filter: grayscale(1);
}
.title-button {
scale: 0.9;
}
.mod-button {
height: 38px !important;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 12px;
}
background-color: #ebebed;
transition:
0.4s background-color cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s filter cubic-bezier(0.075, 0.82, 0.165, 1) !important;
&:hover {
opacity: 0.85;
}
&:active {
transform: none !important;
opacity: 0.9;
filter: brightness(0.9);
}
}
#zen-boost-text-case-toggle[case-mode="none"] {
opacity: 0.5;
&:hover {
opacity: 0.6 !important;
}
}
#zen-boost-code-top-bar .mod-button {
height: auto !important;
}
#zen-boost-code-bottom-bar button {
margin: 0;
}
#zen-boost-name {
height: 26px;
font-size: 8pt;
text-indent: 2px;
vertical-align: middle;
color: #3a3a3b;
justify-content: center;
}
#zen-boost-close {
margin-left: 0px;
margin-right: 6px;
@media (-moz-platform: macos) {
margin-left: 6px;
margin-right: 0px;
}
}
#zen-boost-shuffle {
margin-right: 0px;
margin-left: 4px;
@media (-moz-platform: macos) {
margin-right: 10px;
margin-left: 0px;
}
}
.zen-boost-button-active {
background-color: #3a3a3a;
color: #fcfcfe;
}
.zen-boost-button-active:hover {
background-color: #5b5b5c;
}
.zen-boost-button-active-transparent {
opacity: 0.35;
}
.zen-boost-button-active-transparent:hover {
opacity: 0.5 !important;
}
#zen-boost-magic-theme {
width: 18px;
height: 24px !important;
margin-top: 12px;
z-index: 4;
box-shadow: 0px 2px 4px #00000010;
position: relative;
margin-left: auto;
margin-right: auto;
& image {
width: 12px;
}
&:not(.zen-boost-button-active) {
background: white;
}
}
#zen-boost-toolbar-wrapper,
#zen-boost-toolbar-wrapper-colors {
width: 100%;
margin: auto;
justify-content: center;
align-items: center;
gap: 8px;
& .small {
margin: 0;
flex: 1 1 30%;
}
& .med {
margin: 0;
flex: 1 1 50%;
}
}
#zen-boost-font-select {
width: 95px;
height: 20px !important;
transition: 0.4s opacity cubic-bezier(0.075, 0.82, 0.165, 1) !important;
color: #727272;
background-color: transparent;
background: none;
font-size: 9pt;
padding: 0px;
text-indent: 2px;
margin-left: 8px;
margin-right: 3px;
margin-top: 8px;
opacity: 0.75;
&:hover {
opacity: 1;
}
}
#zen-boost-font-toolbar {
display: flex;
flex-direction: row;
justify-content: space-between;
& button {
border-radius: 6px;
min-width: fit-content;
height: 20px !important;
margin-left: 8px;
margin-right: 8px;
margin-top: 8px;
background-color: transparent;
}
}
#zen-boost-font-wrapper {
box-shadow: 0px 2px 8px #00000010;
background-color: #ffffff;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 8px;
}
padding-top: 4px;
padding-bottom: 4px;
display: flex;
flex-direction: column;
& .visible-separator {
content: "";
height: 1px;
width: auto;
margin-right: 12px;
margin-left: 12px;
margin-top: 2px;
opacity: 0.25;
border-top: solid 1px #9a9a9a;
}
& button {
filter: none;
color: rgba(0, 0, 0, 0.6);
border-radius: 50px;
transition: 0.1s background-color ease-in-out !important;
font-size: 12px;
padding: 2px;
&:hover {
background-color: #9a9a9a30;
}
}
}
.zen-boost-font-button-active {
background-color: #5b5b5c22 !important;
}
.zen-boost-font-button-active:hover {
background-color: #5b5b5c32 !important;
}
@property --mod-button-c1 {
syntax: "<color>";
inherits: false;
initial-value: #ebebed;
}
@property --mod-button-c2 {
syntax: "<color>";
inherits: false;
initial-value: #ebebed;
}
.mod-button[mode="orange"] {
color: #e3e9e4;
--mod-button-c1: #ffbb5d;
--mod-button-c2: #ffa01d;
}
.mod-button[mode="orange-red"] {
color: #e3e9e4;
--mod-button-c1: #ff8758;
--mod-button-c2: #ff5b1b;
}
.mod-button[mode="red"] {
color: #e3e9e4;
--mod-button-c1: #ff595f;
--mod-button-c2: #ff121b;
}
.mod-button[mode="blue"] {
color: #e3e9e4;
--mod-button-c1: #6650fc;
--mod-button-c2: #4125ff;
}
.mod-button[mode] {
background: linear-gradient(180deg, var(--mod-button-c1) 0%, var(--mod-button-c2) 100%) border-box;
transition:
0.4s background-color cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s opacity cubic-bezier(0.075, 0.82, 0.165, 1),
0.4s filter cubic-bezier(0.075, 0.82, 0.165, 1),
0.75s --mod-button-c1 cubic-bezier(0.075, 0.82, 0.165, 1),
0.55s --mod-button-c2 cubic-bezier(0.075, 0.82, 0.165, 1) !important;
}
#zen-boost-back {
background-color: transparent;
&:hover {
background-color: #e3e3e6;
}
}
#zen-boost-code-editor-root button {
opacity: 0.6;
}
.toggleable-button[enabled="true"] {
filter: invert();
opacity: 0.9;
&:active {
filter: invert();
opacity: 0.8;
}
}
.zen-boost-color-picker-gradient::after {
content: "";
position: absolute;
inset: 0;
border-radius: 8px;
margin: 8px;
z-index: 2;
background: #fbfbfdea;
background-position: -23px -23px;
backdrop-filter: saturate(2) blur(15px);
background-size: 6px 6px;
background-image: radial-gradient(#e3e9e4, 1px, transparent 0);
@media (-moz-platform: macos) {
background-size: 4px 4px;
background-image: radial-gradient(#e3e9e4 0.5px, transparent 0);
}
}
.zen-boost-color-picker-gradient::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
z-index: 1;
background: conic-gradient(
rgba(255, 0, 0, 1) 0%,
rgba(255, 162, 0, 1) 10%,
rgba(255, 242, 0, 1) 20%,
rgba(89, 255, 0, 1) 30%,
rgba(0, 255, 128, 1) 40%,
rgba(0, 255, 247, 1) 50%,
rgba(0, 89, 255, 1) 60%,
rgba(8, 0, 255, 1) 70%,
rgba(204, 0, 255, 1) 80%,
rgba(255, 0, 144, 1) 90%,
rgba(255, 0, 8, 1) 100%
)
border-box;
pointer-events: none;
}
.zen-boost-color-picker-gradient {
position: relative;
overflow: hidden;
border-radius: 16px;
box-shadow: 0px 4px 12px #00000021;
width: 100%;
aspect-ratio: 1 /1;
margin: 10px 0px 4px 0px;
min-height: calc(var(--panel-width) - var(--panel-padding) * 2 - 2px);
& [animated="true"] {
transition: all 0.4s cubic-bezier(0.14, 1.43, 0.56, 1.01) !important;
}
& .zen-boost-color-picker-circle {
position: absolute;
z-index: 3;
transform-origin: center center;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.4s ease;
left: 50%;
top: 50%;
outline: solid 1px #9a9a9a88;
border-radius: 100%;
}
& .zen-boost-color-picker-dot {
box-shadow: 0px 2px 4px #00000022;
position: absolute;
z-index: 4;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--zen-theme-picker-dot-color);
@media (-prefers-color-scheme: dark) {
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
}
cursor: pointer;
border: 3px solid #ffffff;
transform: translate(-50%, -50%);
pointer-events: none;
transform-origin: top left;
&:first-of-type {
width: 32px;
height: 32px;
border-width: 3px;
pointer-events: all;
transition: transform 0.2s;
z-index: 999;
&:hover {
transform: scale(1.05) translate(-50%, -50%);
}
}
&[dragging="true"] {
transform: scale(1.2) translate(-50%, -50%);
}
}
}
#zen-boost-editor-root:hover {
& .zen-boost-color-picker-circle {
opacity: 0.4;
}
}
/** Import animation */
/** From zen-single-components.css:211 */
@property --background-top {
syntax: "<percentage>";
inherits: false;
initial-value: -50%;
}
#import-animation {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5) 40%, rgba(255, 255, 255, 0.6) 60%, transparent 100%);
left: 0;
z-index: 999;
opacity: 0;
transform: translateY(-50%);
pointer-events: none;
}
#import-animation-shadow {
position: fixed;
mix-blend-mode: multiply;
background: #00000040;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 998;
opacity: 0;
pointer-events: none;
}
#import-animation-border {
position: absolute;
border-radius: var(--zen-border-radius);
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
}
#import-animation-border::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 4px; /* thickness of border */
z-index: 1000;
background: radial-gradient(ellipse 100% 50% at center var(--background-top), rgba(255, 255, 255, 0.9) 80%, transparent);
/* Punch out the inside once (not animated) */
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
}

View File

@@ -0,0 +1,200 @@
/*
* 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 http://mozilla.org/MPL/2.0/.
*/
@keyframes select-component-in {
0% {
scale: 0;
opacity: 0;
}
100% {
scale: 1;
opacity: 1;
}
}
#select-component {
width: min-content;
font-family: system-ui !important;
height: 98px;
pointer-events: auto !important;
transform-origin: top;
overflow: hidden;
position: fixed;
z-index: 9999;
background-color: #f5f7fb;
margin: 4px;
border-radius: 12px;
@media (-moz-platform: macos) {
border-radius: 24px;
}
box-shadow: 0px 0px 20px#00000050;
scale: 1;
opacity: 1;
@media not (-moz-platform: windows) {
&[is-appearing="true"] {
scale: 0;
opacity: 0;
animation: 0.4s select-component-in cubic-bezier(0.14, 1.43, 0.56, 1.01) forwards;
}
}
& input {
width: fit-content;
min-width: 75px;
height: 40px;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 12px;
}
padding-left: 10px;
padding-right: 10px;
font-size: 11pt;
transition:
0.15s filter ease,
0.1s box-shadow ease-out,
scale 0.35s cubic-bezier(0.11, 1.6, 0.63, 1);
&:hover {
filter: brightness(1.3);
}
&:active {
scale: 0.95;
}
}
}
#highlight-shadow {
z-index: 999;
position: fixed;
width: 100%;
height: 100%;
content: "";
background-color: #00000044;
}
#highlight-container {
z-index: 1000;
position: fixed;
width: 100%;
height: 100%;
content: "";
& .highlight {
position: fixed;
border: 1px solid #e9dd38;
background-color: #c2b72046;
border-radius: 6px;
content: "";
}
}
#select-controls {
display: flex;
gap: 10px;
padding: 10px;
@media not (-moz-platform: windows) {
gap: 12px;
padding: 12px;
}
}
#select-related {
appearance: none;
text-align: left;
width: 250px !important;
--related-elements-value: 100%;
background: linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86));
border: none;
color: #dadada;
box-shadow: 0px 0px 15px #00000052;
&:hover {
box-shadow: 0px 0px 14px #00000066;
background:
linear-gradient(to right, transparent var(--related-elements-value), gray var(--related-elements-value)),
linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86));
@media not (-moz-platform: windows) {
box-shadow: 0px 0px 20px #00000077;
}
}
}
#select-this {
appearance: none;
background: linear-gradient(0deg, rgba(246, 27, 25, 1) 0%, rgba(254, 67, 59, 1) 100%);
border: none;
color: #dadada;
box-shadow: 0px 0px 15px #00000052;
}
#select-cancel {
appearance: none;
background: rgb(90, 94, 100);
@media (-moz-platform: macos) {
background: linear-gradient(0deg, rgba(81, 83, 85, 1) 0%, rgba(108, 110, 112, 1) 100%);
}
border: none;
color: #dadada;
box-shadow: 0px 0px 15px #00000052;
}
#selector-preview {
background-color: #e0e2e63d;
outline: 1px solid #e0e2e6ae;
width: 100%;
height: 34px;
text-indent: 18px;
display: flex;
align-items: center;
}
#selector-element-preview-text {
font-family: Arial, Helvetica, sans-serif;
font-size: 9pt;
color: #797b7f;
}
#hover-div {
position: fixed;
z-index: 9999;
content: "";
outline: 1px solid #e9dd38;
border-radius: 6px;
background-color: #c2b72046;
transition: 0.1s all ease-out;
will-change: left, top, width, height;
}

158
src/zen/boosts/zen-zap.css Normal file
View File

@@ -0,0 +1,158 @@
/*
* 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 http://mozilla.org/MPL/2.0/.
*/
@keyframes select-controls-container-in {
0% {
bottom: -54px;
}
100% {
bottom: 8px;
}
}
@keyframes zap-border-in {
0% {
left: 0px;
top: 0px;
bottom: 0px;
right: 0px;
}
100% {
left: 2px;
top: 2px;
bottom: 2px;
right: 2px;
}
}
#zap-border {
z-index: 9999;
content: "";
position: fixed;
left: 2px;
top: 2px;
bottom: 2px;
right: 2px;
pointer-events: none;
background-color: transparent;
outline: 4px solid #dfd43a;
animation: 0.15s zap-border-in ease-out;
border-radius: 4px;
@media (-moz-platform: macos) {
border-radius: 8px;
}
}
#zap-controls-container {
font-family: system-ui !important;
display: flex;
pointer-events: auto !important;
z-index: 999999;
bottom: 8px;
left: 8px;
right: 8px;
margin: 0;
padding: 0 10px 0 10px;
height: 54px;
position: fixed;
border-radius: 6px;
@media (-moz-platform: macos) {
border-radius: 12px;
}
align-items: center;
gap: 8px;
color: #d6d6d6;
background-color: #1d1b1b;
border: 1px solid #2f2b2b;
animation: 0.15s select-controls-container-in cubic-bezier(0.07, 1.32, 0.41, 1);
& input {
width: min-content;
height: 36px;
border-radius: 4px;
@media (-moz-platform: macos) {
border-radius: 8px;
}
border: none;
background: linear-gradient(0deg, #afafaf 0%, #e5e5e5 100%) !important;
color: #1d1b1b;
font-weight: bold;
transition:
0.1s filter ease-in-out,
scale 0.35s cubic-bezier(0.11, 1.6, 0.63, 1);
&:hover {
filter: brightness(0.9);
}
&:active {
filter: brightness(0.8);
scale: 0.95;
}
}
& #zap-list {
display: flex;
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
align-items: center;
vertical-align: middle;
gap: 8px;
& input {
width: 36px;
flex: 0 0 auto;
}
& p {
padding-left: 12px;
font-size: 10pt;
user-select: none;
}
& .pcenter {
width: 100%;
text-align: center;
}
}
& #zap-done {
flex: 0 0 auto;
padding: 0 12px 0 12px;
}
}
#zen-zap-dissolve-canvas {
position: absolute !important;
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: 0 !important;
pointer-events: none !important;
z-index: 2147483647 !important;
}

View File

@@ -221,6 +221,7 @@ panel {
}
.permission-popup-permission-item,
.permission-popup-boost-item,
#permission-popup-storage-access-permission-list-header {
padding-block: 4px;
margin-block: 0px;

View File

@@ -404,17 +404,59 @@
display: none;
}
.permission-popup-permission-item {
.zen-permission-popup-boost-editor-button {
transition:
0.2s opacity ease,
0.2s background-color ease,
0.2s transform ease;
opacity: 0;
width: 30px;
height: 26px;
min-width: 30px;
appearance: none;
background: transparent;
border: none;
border-radius: 8px;
transform: scale(1);
-moz-context-properties: fill;
fill: white;
&:active {
transform: scale(0.95);
}
& image {
opacity: 0.6;
height: 14px;
width: calc(100% - 2px);
}
}
.permission-popup-boost-item:hover .zen-permission-popup-boost-editor-button {
opacity: 1;
&.zen-permission-popup-boost-editor-button:hover {
background-color: #aaaaaa22;
}
}
.permission-popup-permission-item,
.permission-popup-boost-item {
gap: 8px;
overflow: hidden;
align-items: center;
}
.permission-popup-permission-label-container {
.permission-popup-permission-label-container,
.permission-popup-boost-label-container {
min-width: 1px;
}
.permission-popup-permission-label {
.permission-popup-permission-label,
.permission-popup-boost-label {
margin: 0px;
font-weight: 500;
white-space: nowrap;
@@ -423,7 +465,8 @@
max-width: 100%;
}
.permission-popup-permission-icon {
.permission-popup-permission-icon,
.permission-popup-boost-icon {
fill: var(--button-primary-color);
padding: 8px;
width: 34px;
@@ -455,20 +498,24 @@
opacity 0.12s ease-in-out;
}
.permission-popup-permission-item:hover &::before {
.permission-popup-permission-item:hover &::before,
.permission-popup-boost-item:hover:not(:has(.zen-permission-popup-boost-editor-button:hover)) &::before {
transform: scale(1.05);
}
.permission-popup-permission-item:active:hover &::before {
.permission-popup-permission-item:active:hover &::before,
.permission-popup-boost-item:active:hover:not(:has(.zen-permission-popup-boost-editor-button:hover)) &::before {
transform: scale(0.95);
}
.permission-popup-permission-item[state="allow"] &::before {
.permission-popup-permission-item[state="allow"] &::before,
.permission-popup-boost-item[state="enabled"] &::before {
opacity: 1;
}
}
.zen-permission-popup-permission-state-label {
.zen-permission-popup-permission-state-label,
.zen-permission-popup-boost-state-label {
opacity: 0.6;
font-size: smaller;
font-weight: 400;
@@ -485,6 +532,8 @@
padding-top: 8px;
margin: 2px 8px 8px 8px;
gap: 8px;
& toolbarbutton {
margin: 0;
}
@@ -581,7 +630,8 @@
}
}
#zen-site-data-settings-list toolbarseparator {
#zen-site-data-settings-list toolbarseparator,
#zen-site-data-boost-list toolbarseparator {
margin: 6px 0;
}

View File

@@ -59,6 +59,24 @@ let JSWINDOWACTORS = {
},
};
if (!Services.appinfo.inSafeMode) {
JSWINDOWACTORS.ZenBoosts = {
parent: {
esModuleURI: "resource:///actors/ZenBoostsParent.sys.mjs",
},
child: {
esModuleURI: "resource:///actors/ZenBoostsChild.sys.mjs",
events: {
DOMDocElementInserted: { capture: true },
unload: {},
},
},
allFrames: true,
matches: ["*://*/*"],
enablePreference: "zen.boosts.enabled",
};
}
export let gZenActorsManager = {
init() {
ActorManagerParent.addJSProcessActors(JSPROCESSACTORS);

View File

@@ -30,7 +30,7 @@ nsZenDragAndDrop::nsZenDragAndDrop() {
}
auto nsZenDragAndDrop::GetZenDragAndDropInstance() -> nsCOMPtr<nsZenDragAndDrop> {
return do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID);
return do_GetService(ZEN_DND_MANAGER_CONTRACTID);
}
NS_IMETHODIMP

View File

@@ -8,7 +8,8 @@
#include "nsIZenDragAndDrop.h"
#include "nsCOMPtr.h"
#define ZEN_BOOSTS_BACKEND_CONTRACTID "@mozilla.org/zen/drag-and-drop;1"
#define ZEN_DND_MANAGER_CONTRACTID \
"@mozilla.org/zen/drag-and-drop;1"
namespace zen {

View File

@@ -0,0 +1,50 @@
<!--
- 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 http://mozilla.org/MPL/2.0/.
-->
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="none"/>
<style type="text/css"><![CDATA[
.sparkle {
stroke: currentColor;
stroke-width: 1;
stroke-linecap: round;
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation: sparkle 3s ease-in-out infinite;
}
@keyframes sparkle {
0% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(90deg);
}
100% {
opacity: 0;
transform: scale(0) rotate(180deg);
}
}
}*/
]]></style>
<g class="sparkle-group">
<path class="sparkle" style="animation-delay: 0.5s;" d="M10 2v2"/>
<path class="sparkle" style="animation-delay: 0.5s;" d="M11 3H9"/>
<path class="sparkle" d="M5 6v4"/>
<path class="sparkle" d="M7 8H3"/>
<path class="sparkle" style="animation-delay: 1.8s;" d="M21 20h-4"/>
<path class="sparkle" style="animation-delay: 1.8s;" d="M19 18v4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -34,10 +34,8 @@ nsZenModsBackend::nsZenModsBackend() {
auto nsZenModsBackend::CheckEnabled() -> void {
// Check if the mods backend is enabled based on the preference.
nsCOMPtr<nsIXULRuntime> appInfo =
do_GetService("@mozilla.org/xre/app-info;1");
bool inSafeMode = false;
if (appInfo) {
if (nsCOMPtr<nsIXULRuntime> appInfo = do_GetService("@mozilla.org/xre/app-info;1")) {
appInfo->GetInSafeMode(&inSafeMode);
}
mEnabled = !inSafeMode &&

View File

@@ -7,6 +7,7 @@ EXTRA_PP_COMPONENTS += [
]
DIRS += [
"boosts",
"common",
"drag-and-drop",
"glance",

View File

@@ -0,0 +1,12 @@
# 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 http://mozilla.org/MPL/2.0/.
[DEFAULT]
support-files = [
"head.js",
]
["browser_boost_selector_basic.js"]
["browser_boost_selector_invalid.js"]
["browser_boost_selector_nthchild.js"]

View File

@@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_getSelectionPath_basic() {
const doc = document.implementation.createHTMLDocument("TestDoc");
const container = doc.createElement("div");
container.id = "container";
const child1 = doc.createElement("p");
child1.className = "one";
child1.textContent = "Text 1";
const child2 = doc.createElement("span");
child2.className = "two";
child2.textContent = "Text 2";
container.appendChild(child1);
container.appendChild(child2);
doc.body.appendChild(container);
const component = new SelectorComponent(doc, null, [], () => {});
for (let i = 0; i <= 7; i++) {
const path = component.getSelectionPath(doc, i, child2);
ok(path, `getSelectionPath should return a path for relatedValueIndex=${i}`);
const selectedElements = doc.querySelectorAll(path);
if (i === 0)
ok(
selectedElements.length === 1,
"For relatedValueIndex=1 there should be exactly one queried element"
);
ok(selectedElements.length >= 1, "CSS path should select at least one element");
}
});

View File

@@ -0,0 +1,26 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_getSelectionPath_invalidNode() {
const doc = document.implementation.createHTMLDocument("TestInvalid");
const component = new SelectorComponent(doc, null, [], () => {});
// Null element
Assert.equal(component.getSelectionPath(doc, 0, null), null, "Null element should return null");
// Body element
Assert.equal(
component.getSelectionPath(doc, 0, doc.body),
null,
"Body element should return null"
);
// Html element
Assert.equal(
component.getSelectionPath(doc, 0, doc.documentElement),
null,
"HTML element should return null"
);
});

View File

@@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_getSelectionPath_nthchild() {
const doc = document.implementation.createHTMLDocument("TestDoc");
const childCount = 10;
const container = doc.createElement("div");
container.id = "container";
for (let i = 0; i < childCount; i++) {
const child = doc.createElement("p");
child.className = "child";
child.textContent = `Child ${i}`;
container.appendChild(child);
}
doc.body.appendChild(container);
const component = new SelectorComponent(doc, null, [], () => {});
for (let i = 0; i < container.children.length; i++) {
const currentNode = container.children[i];
// Get exact element
const path = component.getSelectionPath(doc, 0, currentNode);
ok(path, "Path should be generated");
const selectedElements = doc.querySelectorAll(path);
ok(
Array.from(selectedElements).includes(currentNode),
"Selector must include the selected node"
);
Assert.equal(selectedElements.length, 1, "Selector should uniquely identify the element");
}
});

View File

@@ -0,0 +1,7 @@
/* 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 http://mozilla.org/MPL/2.0/. */
const { SelectorComponent } = ChromeUtils.importESModule(
"resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs"
);

View File

@@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
BROWSER_CHROME_MANIFESTS += [
"boosts/browser.toml",
"compact_mode/browser.toml",
"container_essentials/browser.toml",
"folders/browser.toml",

View File

@@ -13,6 +13,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FeatureCallout: "resource:///modules/asrouter/FeatureCallout.sys.mjs",
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
});
export class nsZenSiteDataPanel {
@@ -63,6 +64,7 @@ export class nsZenSiteDataPanel {
this.#initCopyUrlButton();
this.#initEventListeners();
this.#initBrowserListeners();
this.#initUnifiedExtensionsManageHook();
this.#maybeShowFeatureCallout();
}
@@ -76,6 +78,7 @@ export class nsZenSiteDataPanel {
"zen-site-data-header-share",
"zen-site-data-header-bookmark",
"zen-site-data-security-info",
"zen-site-data-boost",
"zen-site-data-actions",
"zen-site-data-new-addon-button",
];
@@ -87,6 +90,50 @@ export class nsZenSiteDataPanel {
this.#initContextMenuEventListener();
}
#initBrowserListeners() {
Services.obs.addObserver(this, "zen-boosts-update");
this.window.gBrowser.addProgressListener({
onLocationChange: (aWebProgress) => {
if (aWebProgress.isTopLevel) {
this.checkIfTabIsBoosted();
}
},
});
this.window.addEventListener(
"unload",
() => {
Services.obs.removeObserver(this, "zen-boosts-update");
},
{ once: true }
);
}
observe(subject, topic) {
switch (topic) {
case "zen-boosts-update":
this.checkIfTabIsBoosted();
break;
}
}
#getCurrentDomain() {
try {
return this.window.gBrowser.currentURI.host;
} catch {
return "";
}
}
checkIfTabIsBoosted() {
const domain = this.#getCurrentDomain();
const isBoosted = lazy.gZenBoostsManager.registeredBoostForDomain(domain);
if (isBoosted) {
this.anchor.setAttribute("boosting", "true");
} else {
this.anchor.removeAttribute("boosting");
}
}
#initCopyUrlButton() {
// This function is a bit out of place, but it's related enough to the panel
// that it's easier to do it here than in a separate module.
@@ -172,12 +219,131 @@ export class nsZenSiteDataPanel {
}
#preparePanel() {
this.#resetSiteOptionsList();
this.#setSiteBoost();
this.#setSitePermissions();
this.#setSiteSecurityInfo();
this.#setSiteHeader();
this.#setAddonsOverflow();
}
#setSiteBoost() {
const domain = this.#getCurrentDomain();
const uri = this.window.gBrowser.currentURI;
const canBoostSite = lazy.gZenBoostsManager.canBoostSite(uri);
const list = this.document.getElementById("zen-site-data-boost-list");
const section = list.closest(".zen-site-data-section");
section.hidden = true;
const boostButton = this.document.getElementById("zen-site-data-boost");
if (!canBoostSite) {
boostButton.removeAttribute("boosting");
}
if (!canBoostSite) {
return;
}
if (lazy.gZenBoostsManager.registeredBoostForDomain(domain)) {
boostButton.setAttribute("boosting", "true");
} else {
boostButton.removeAttribute("boosting");
}
/* Boosts panel */
const boosts = lazy.gZenBoostsManager.loadBoostsFromStore(domain);
let validBoostCount = 0;
if (boosts) {
const activeBoostId = lazy.gZenBoostsManager.getActiveBoostId(domain);
boosts.forEach((boost) => {
const boostData = boost.boostEntry.boostData;
if (!boostData.changeWasMade) {
return;
}
validBoostCount++;
const enabled = boost.id === activeBoostId;
list.appendChild(
this.#createBoostPanelItem(
"boost-brush",
boostData.boostName,
enabled ? "Enabled" : "Disabled",
"zen-site-data-toggle-boost",
boost,
enabled
)
);
});
}
section.hidden = validBoostCount === 0;
}
#updateSiteBoost() {
const boostList = this.document.getElementById("zen-site-data-boost-list");
boostList.innerHTML = "";
this.#setSiteBoost();
}
#createBoostPanelItem(iconClass, title, description, actionId, boost = null, enabled = false) {
const container = this.document.createXULElement("hbox");
container.classList.add("permission-popup-boost-item");
container.setAttribute("align", "center");
container.setAttribute("role", "group");
container.setAttribute("data-action-id", actionId);
container.setAttribute("state", "enabled");
if (boost) {
container.setAttribute("data-boost-id", boost.id);
container.setAttribute("state", enabled ? "enabled" : "disabled");
}
const img = this.document.createXULElement("toolbarbutton");
img.classList.add("permission-popup-boost-icon", "zen-site-data-boost-icon");
img.setAttribute("closemenu", "none");
img.classList.add(iconClass);
const labelContainer = this.document.createXULElement("vbox");
labelContainer.setAttribute("flex", "1");
labelContainer.setAttribute("align", "start");
labelContainer.classList.add("permission-popup-boost-label-container");
const nameLabel = this.document.createXULElement("label");
nameLabel.setAttribute("flex", "1");
nameLabel.setAttribute("class", "permission-popup-boost-label");
nameLabel.textContent = title || "";
labelContainer.appendChild(nameLabel);
const stateLabel = this.document.createXULElement("label");
stateLabel.setAttribute("class", "zen-permission-popup-boost-state-label");
stateLabel.textContent = description || "";
labelContainer.appendChild(stateLabel);
container.appendChild(img);
container.appendChild(labelContainer);
if (boost) {
const editorButton = this.document.createXULElement("toolbarbutton");
editorButton.setAttribute("data-action-id", "zen-site-data-edit-boost");
editorButton.setAttribute("data-boost-id", boost.id);
editorButton.classList.add("zen-permission-popup-boost-editor-button");
container.appendChild(editorButton);
editorButton.addEventListener("click", (event) => {
event.stopPropagation(); // Prevents the container event
this.#onBoostClick(event);
});
}
container.addEventListener("click", this.#onBoostClick.bind(this));
return container;
}
#setAddonsOverflow() {
const addons = this.document.getElementById("zen-site-data-addons");
if (addons.getBoundingClientRect().height > 420) {
@@ -242,6 +408,13 @@ export class nsZenSiteDataPanel {
return uri.scheme.startsWith("http");
}
#resetSiteOptionsList() {
const settingsList = this.document.getElementById("zen-site-data-settings-list");
settingsList.innerHTML = "";
const boostList = this.document.getElementById("zen-site-data-boost-list");
boostList.innerHTML = "";
}
#setSiteSecurityInfo() {
const { gIdentityHandler } = this.window;
const button = this.document.getElementById("zen-site-data-security-info");
@@ -380,7 +553,6 @@ export class nsZenSiteDataPanel {
}
const separator = this.document.createXULElement("toolbarseparator");
list.innerHTML = "";
list.appendChild(separator);
const settingElements = [];
const crossSiteCookieElements = [];
@@ -517,6 +689,13 @@ export class nsZenSiteDataPanel {
this.window.gIdentityHandler._openPopup(event);
break;
}
case "zen-site-data-boost": {
const domain = this.#getCurrentDomain();
const uri = this.window.gBrowser.currentURI;
const boost = lazy.gZenBoostsManager.createNewBoost(domain);
lazy.gZenBoostsManager.openBoostWindow(this.window, boost, uri);
break;
}
case "zen-site-data-actions": {
const button = this.document.getElementById("zen-site-data-actions");
const popup = this.document.getElementById("zenSiteDataActions");
@@ -598,6 +777,34 @@ export class nsZenSiteDataPanel {
}
}
#onBoostClick(event) {
const target = event.target.closest("[data-action-id]");
if (!target) {
return;
}
const actionId = target.getAttribute("data-action-id");
const domain = this.#getCurrentDomain();
switch (actionId) {
case "zen-site-data-toggle-boost": {
const boostId = target.getAttribute("data-boost-id");
lazy.gZenBoostsManager.toggleBoostActiveForDomain(domain, boostId);
this.#updateSiteBoost();
break;
}
case "zen-site-data-edit-boost": {
const boostId = target.getAttribute("data-boost-id");
const uri = this.window.gBrowser.currentURI;
const boost = lazy.gZenBoostsManager.loadBoostFromStore(domain, boostId);
lazy.gZenBoostsManager.openBoostWindow(this.window, boost, uri);
this.unifiedPanel.hidePopup();
break;
}
}
}
#onClickEvent(event) {
const id = event.target.id;
switch (id) {

View File

@@ -1574,6 +1574,9 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors);
}
});
// Notify observers that gradient updated
Services.obs.notifyObservers(null, "zen-space-gradient-update");
}
fixTheme(theme) {
@@ -1676,8 +1679,8 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture);
let currentWorkspace = gZenWorkspaces.getActiveWorkspace();
currentWorkspace.theme = gradient;
if (!skipSave) {
currentWorkspace.theme = gradient;
gZenWorkspaces.saveWorkspace(currentWorkspace);
}