mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
[mv3] Add support for custom CSS-based cosmetic filters
Related issue: https://github.com/uBlockOrigin/uBOL-home/issues/325 This is a first version, with support only for custom filters which are plain CSS selectors. Future versions will extend support to style-based and procedural cosmetic filters. Manually text-editing existing custom filters is currently not supported, this will be added in a future version in the Develop pane. To remove existing custom filters, the "Remove a custom filter" tool can be used.
This commit is contained in:
parent
cf13c4b4dc
commit
0b0294af4f
36 changed files with 2463 additions and 778 deletions
|
|
@ -65,6 +65,24 @@
|
|||
"<all_urls>"
|
||||
],
|
||||
"use_dynamic_url": true
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/picker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"use_dynamic_url": true
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/unpicker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"use_dynamic_url": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,12 @@
|
|||
"message": "filtering mode",
|
||||
"description": "Label in the popup panel for the current filtering mode"
|
||||
},
|
||||
"popupLocalToolsLabel": {
|
||||
"message": "On this website",
|
||||
"description": "Label in the popup panel got the local tools section"
|
||||
},
|
||||
"popupTipReport": {
|
||||
"message": "Report an issue on this website",
|
||||
"message": "Report an issue",
|
||||
"description": "Tooltip used for the 'chat' icon in the panel"
|
||||
},
|
||||
"popupTipDashboard": {
|
||||
|
|
@ -288,13 +292,21 @@
|
|||
"description": "A button to navigate to the blocked page"
|
||||
},
|
||||
"zapperTipEnter": {
|
||||
"message": "Enter element zapper mode",
|
||||
"message": "Remove an element",
|
||||
"description": "Tooltip for the button used to enter zapper mode"
|
||||
},
|
||||
"zapperTipQuit": {
|
||||
"message": "Exit element zapper mode",
|
||||
"description": "Tooltip for the button used to exit zapper mode"
|
||||
},
|
||||
"pickerTipEnter": {
|
||||
"message": "Create a custom filter",
|
||||
"description": "Label for the menu entry to create cosmetic filters"
|
||||
},
|
||||
"unpickerTipEnter": {
|
||||
"message": "Remove a custom filter",
|
||||
"description": "Label for the menu entry to delete cosmetic filters"
|
||||
},
|
||||
"developDropdownLabel": {
|
||||
"message": "View:",
|
||||
"description": "A label of a dropdown list"
|
||||
|
|
@ -342,5 +354,25 @@
|
|||
"dnrRulesCountInfo": {
|
||||
"message": "Number of registered rules: {count}",
|
||||
"description": "Short sentence to report the number of currently registered DNR rules"
|
||||
},
|
||||
"pickerSliderLabel": {
|
||||
"message": "Move the slider to select the best match",
|
||||
"description": "Label to describe the purpose of the slider"
|
||||
},
|
||||
"pickerPick": {
|
||||
"message": "Pick",
|
||||
"description": "Text for the button to re-enter element-picking mode"
|
||||
},
|
||||
"pickerPreview": {
|
||||
"message": "Preview",
|
||||
"description": "Text for the button to activate preview mode"
|
||||
},
|
||||
"pickerCreate": {
|
||||
"message": "Create",
|
||||
"description": "Text for the button to create the filter"
|
||||
},
|
||||
"unpickerUsage": {
|
||||
"message": "Select a filter below to highlight matching elements in the webpage. Click the trash can to remove a filter.",
|
||||
"description": "Summary description on how to use the tool to remove custom filters"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ body[data-pane="about"] > section[data-pane="about"] {
|
|||
|
||||
/* high dpi devices */
|
||||
:root.hidpi .tabButton {
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
|
|||
146
platform/mv3/extension/css/picker-ui.css
Normal file
146
platform/mv3/extension/css/picker-ui.css
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
:root#ubol-picker {
|
||||
--ubol-overlay-fill: rgba(255,64,64,0.10);
|
||||
--ubol-overlay-border: #F00;
|
||||
}
|
||||
|
||||
#ubol-picker.paused svg#overlay {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:root aside {
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border-2);
|
||||
max-height: 40vh;
|
||||
max-width: min(32rem, 100vw - 4px);
|
||||
min-width: min(24rem, 100vw - 4px);
|
||||
row-gap: 1em;
|
||||
width: min(32rem, 100vw - 4px);
|
||||
}
|
||||
|
||||
#ubol-picker aside > section:last-of-type {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
#ubol-picker aside > section:not(#windowbar,#moreOrLess) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
#ubol-picker[data-view="0"] aside section[data-view="1"],
|
||||
#ubol-picker[data-view="0"] aside section[data-view="2"] {
|
||||
display: none;
|
||||
}
|
||||
#ubol-picker[data-view="1"] aside section[data-view="2"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ubol-picker:not(.paused) aside > section:not(#windowbar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ubol-picker textarea {
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
height: 6em;
|
||||
min-height: 3em;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
}
|
||||
#ubol-picker.mobile textarea {
|
||||
height: unset;
|
||||
}
|
||||
#ubol-picker .resultsetWidgets {
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: smaller;
|
||||
gap: 0.25em;
|
||||
}
|
||||
#ubol-picker .resultsetWidgets > span:first-of-type {
|
||||
display: flex;
|
||||
margin: 0 1em;
|
||||
}
|
||||
#ubol-picker .resultsetWidgets label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
#ubol-picker .resultsetWidgets #resultsetCount {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 8ch;
|
||||
}
|
||||
|
||||
#ubol-picker #toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#ubol-picker #toolbar button {
|
||||
min-width: 5em;
|
||||
}
|
||||
|
||||
#ubol-picker #candidateFilters {
|
||||
font-family: monospace;
|
||||
font-size: small;
|
||||
min-height: 6em;
|
||||
overflow-y: auto;
|
||||
word-break: break-all;
|
||||
}
|
||||
#ubol-picker #candidateFilters ul {
|
||||
margin: 0;
|
||||
padding-inline-start: calc(2ch + 4px);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
#ubol-picker #candidateFilters ul > li {
|
||||
list-style-type: '\25A0\00A0';
|
||||
}
|
||||
#ubol-picker #candidateFilters ul >li:has(:not(span.on)) {
|
||||
list-style-type: '\25A1\00A0';
|
||||
}
|
||||
#ubol-picker #candidateFilters ul > li:nth-of-type(2n+1) {
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
#ubol-picker #candidateFilters ul > li > span {
|
||||
border: 1px solid transparent;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
#ubol-picker #candidateFilters ul > li > span.on {
|
||||
background-color: var(--ink-1);
|
||||
color: var(--surface-1);
|
||||
}
|
||||
#ubol-picker #candidateFilters ul > li > span:hover {
|
||||
border: 1px solid var(--ink-1);
|
||||
}
|
||||
|
||||
#ubol-picker #moreOrLess {
|
||||
color: var(--ink-2);
|
||||
column-gap: 0;
|
||||
display: grid;
|
||||
font-size: small;
|
||||
grid-template: auto / 1fr 1fr;
|
||||
justify-items: stretch;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#ubol-picker #moreOrLess > span {
|
||||
cursor: pointer;
|
||||
padding: var(--default-gap-xxsmall) var(--default-gap-xsmall);
|
||||
}
|
||||
#ubol-picker #moreOrLess > span:last-of-type {
|
||||
text-align: end;
|
||||
}
|
||||
#ubol-picker[data-view="2"] aside #moreOrLess > span:first-of-type {
|
||||
visibility: hidden;
|
||||
}
|
||||
#ubol-picker[data-view="0"] aside #moreOrLess > span:last-of-type {
|
||||
visibility: hidden;
|
||||
}
|
||||
#ubol-picker.desktop aside #moreOrLess > span:hover {
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
|
||||
#ubol-picker.preview #toolbar #preview {
|
||||
color: var(--accent-ink-1);
|
||||
background-color: var(--accent-surface-1);
|
||||
}
|
||||
#ubol-picker.preview #overlay path {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
min-width: var(--popup-min-width);
|
||||
}
|
||||
:root body.loading {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
a {
|
||||
color: var(--ink-1);
|
||||
|
|
@ -36,16 +36,12 @@ a {
|
|||
align-self: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 340px;
|
||||
}
|
||||
:root.portrait #main {
|
||||
align-self: inherit;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--hr-ink);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
#hostname {
|
||||
|
|
@ -69,8 +65,9 @@ body[data-forbid~="filteringMode"] .filteringModeSlider {
|
|||
pointer-events: none;
|
||||
}
|
||||
body[data-forbid~="dashboard"] #gotoDashboard,
|
||||
body[data-forbid~="zapper"] #gotoZapper {
|
||||
visibility: hidden;
|
||||
body[data-forbid~="zapper"] #gotoZapper,
|
||||
body[data-forbid~="picker"] #gotoPicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#filteringModeText {
|
||||
|
|
@ -131,102 +128,46 @@ body.needReload #refresh {
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
#rulesetStats {
|
||||
padding: 0 var(--popup-gap-thin);
|
||||
}
|
||||
#rulesetStats .rulesetDetails h1 {
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
margin: 0.5em 0 0.25em 0;
|
||||
}
|
||||
#rulesetStats .rulesetDetails p {
|
||||
color: var(--ink-2);
|
||||
font-size: var(--font-size-smaller);
|
||||
margin: 0.25em 0 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.itemRibbon {
|
||||
column-gap: var(--popup-gap);
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
grid-template: auto / 1fr 1fr;
|
||||
margin: var(--popup-gap);
|
||||
}
|
||||
.itemRibbon > span + span {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.toolRibbon {
|
||||
align-items: start;
|
||||
background-color: var(--popup-toolbar-surface);
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
grid-template: auto / repeat(4, 1fr);
|
||||
justify-items: stretch;
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: calc(var(--font-size) - 1px);
|
||||
line-height: calc(var(--font-size) * 2.2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
:root:not(.isHTTP) .needHTTP {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolRibbon > label {
|
||||
font-size: small;
|
||||
color: var(--ink-2);
|
||||
line-height: calc(var(--font-size) * 1.8);
|
||||
margin-inline-start: 0.5em;
|
||||
}
|
||||
.toolRibbon .tool {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1.4em;
|
||||
min-width: 32px;
|
||||
padding: var(--popup-gap)
|
||||
var(--popup-gap-extra-thin);
|
||||
unicode-bidi: embed;
|
||||
visibility: hidden;
|
||||
}
|
||||
.toolRibbon .tool:hover {
|
||||
color: var(--ink-1);
|
||||
fill: var(--ink-1);
|
||||
}
|
||||
.toolRibbon .tool.enabled {
|
||||
visibility: visible;
|
||||
.toolRibbon .tool .fa-icon {
|
||||
font-size: 1.4em;
|
||||
min-width: 1.4em;
|
||||
}
|
||||
.toolRibbon .tool .caption {
|
||||
color: var(--ink-2);
|
||||
font: 10px/12px sans-serif;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
.toolRibbon .tool:not(.enabled) {
|
||||
display: none;
|
||||
}
|
||||
body.mobile.no-tooltips .toolRibbon .tool {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
.toolRibbon.genericTools {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#moreOrLess {
|
||||
column-gap: 0;
|
||||
display: grid;
|
||||
grid-template: auto / 1fr 1fr;
|
||||
justify-items: stretch;
|
||||
margin: 1px 0 0 0;
|
||||
}
|
||||
#moreOrLess > span {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: var(--popup-gap-thin) var(--popup-gap);
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#moreButton .fa-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
#lessButton {
|
||||
border-inline-start: 1px solid var(--surface-1);
|
||||
text-align: end;
|
||||
}
|
||||
body[data-section="a b"] #moreButton {
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
body[data-section=""] #lessButton {
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
body:not([data-section~="a"]) [data-section="a"] {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -234,25 +175,6 @@ body:not([data-section~="b"]) [data-section="b"] {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* configurable UI elements */
|
||||
:root:not(.mobile) .toolRibbon .caption,
|
||||
:root.mobile body.no-tooltips .toolRibbon .caption,
|
||||
:root.mobile body[data-ui~="-captions"] .toolRibbon .caption {
|
||||
display: none;
|
||||
}
|
||||
:root.mobile .toolRibbon .caption,
|
||||
:root:not(.mobile) body[data-ui~="+captions"] .toolRibbon .caption {
|
||||
display: inherit;
|
||||
}
|
||||
:root:not(.mobile) .toolRibbon .tool,
|
||||
:root.mobile body.no-tooltips .toolRibbon .tool,
|
||||
:root.mobile body[data-ui~="-captions"] .toolRibbon .tool {
|
||||
padding: var(--popup-gap) var(--popup-gap-thin);
|
||||
}
|
||||
:root.mobile #moreOrLess > span {
|
||||
padding: var(--popup-gap);
|
||||
}
|
||||
|
||||
/* horizontally-constrained viewport */
|
||||
:root.portrait body {
|
||||
overflow-y: auto;
|
||||
|
|
@ -265,9 +187,6 @@ body:not([data-section~="b"]) [data-section="b"] {
|
|||
:root.desktop {
|
||||
display: flex;
|
||||
}
|
||||
:root.desktop body {
|
||||
--popup-gap: calc(var(--font-size) * 0.875);
|
||||
}
|
||||
:root.desktop .rulesetTools [id]:hover {
|
||||
background-color: var(--popup-ruleset-tool-surface-hover);
|
||||
}
|
||||
|
|
@ -277,11 +196,3 @@ body:not([data-section~="b"]) [data-section="b"] {
|
|||
:root.desktop .tool:hover {
|
||||
background-color: var(--popup-toolbar-surface-hover);
|
||||
}
|
||||
:root.desktop #moreOrLess > span:hover {
|
||||
background-color: var(--surface-2);
|
||||
/* background-color: var(--popup-toolbar-surface-hover); */
|
||||
}
|
||||
|
||||
#templates {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
99
platform/mv3/extension/css/tool-overlay-ui.css
Normal file
99
platform/mv3/extension/css/tool-overlay-ui.css
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
:root {
|
||||
--ubol-overlay-fill: rgba(255,255,255,0.2);
|
||||
--ubol-overlay-border: #FFF;
|
||||
--ubol-overlay-canvas: rgba(128,128,128,0.3);
|
||||
}
|
||||
:root.dark aside {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root,
|
||||
:root > body {
|
||||
background: transparent;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
}
|
||||
:root > body.loading {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:root :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
svg#overlay {
|
||||
cursor: crosshair;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
svg#overlay > path:first-child {
|
||||
fill: var(--ubol-overlay-canvas);
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
svg#overlay > path + path {
|
||||
stroke: var(--ubol-overlay-border);
|
||||
stroke-width: 1.5px;
|
||||
fill: var(--ubol-overlay-fill);
|
||||
}
|
||||
|
||||
:root aside {
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
}
|
||||
:root body[dir="rtl"] aside {
|
||||
left: 2px;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
:root.minimized aside > section:not(#windowbar) {
|
||||
display: none !important;
|
||||
}
|
||||
:root.minimized aside {
|
||||
min-width: min(12em, 100vw - 4px);
|
||||
width: min(12em, 100vw - 4px);
|
||||
}
|
||||
|
||||
:root aside #windowbar {
|
||||
border-bottom: 1px solid var(--border-1);
|
||||
display: flex;
|
||||
}
|
||||
:root aside #windowbar > div {
|
||||
fill: none;
|
||||
height: 2em;
|
||||
stroke: var(--ink-1);
|
||||
stroke-width: 2px;
|
||||
width: 2em;
|
||||
}
|
||||
:root.minimized aside #windowbar > div {
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
}
|
||||
:root.minimized aside #windowbar > #minimize svg > path,
|
||||
#windowbar #minimize svg > rect {
|
||||
display: none;
|
||||
}
|
||||
:root.minimized aside #windowbar > #minimize svg > rect {
|
||||
display: initial;
|
||||
}
|
||||
:root #windowbar > #move {
|
||||
align-items: center;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAFElEQVQI12NgwAfKy8v/M5ANYLoBshgEyQo6H9UAAAAASUVORK5CYII=');
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:root #windowbar > div:hover {
|
||||
background-color: var(--surface-2)
|
||||
}
|
||||
64
platform/mv3/extension/css/unpicker-ui.css
Normal file
64
platform/mv3/extension/css/unpicker-ui.css
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
:root#ubol-unpicker {
|
||||
--ubol-overlay-fill: rgba(64,255,64,0.10);
|
||||
--ubol-overlay-border: #0F0;
|
||||
}
|
||||
|
||||
#ubol-unpicker svg#overlay {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:root aside {
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border-2);
|
||||
max-height: 40vh;
|
||||
max-width: min(32rem, 100vw - 4px);
|
||||
min-width: min(24rem, 100vw - 4px);
|
||||
width: min(32rem, 100vw - 4px);
|
||||
}
|
||||
|
||||
#ubol-unpicker aside > section:not(#windowbar) {
|
||||
margin: 1em 1em 0 1em;
|
||||
}
|
||||
#ubol-unpicker aside > section:not(#windowbar):last-of-type {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
#ubol-unpicker aside > section [data-i18n="unpickerUsage"] {
|
||||
color: var(--ink-2);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#ubol-unpicker #customFilters {
|
||||
font-family: monospace;
|
||||
}
|
||||
#ubol-unpicker .customFilter {
|
||||
display: flex;
|
||||
}
|
||||
#ubol-unpicker .customFilter:nth-of-type(2n+1) {
|
||||
background-color: var(--surface-2);
|
||||
}
|
||||
#ubol-unpicker .customFilter > span.selector {
|
||||
flex-grow: 1;
|
||||
font-size: small;
|
||||
padding: 0.5em;
|
||||
}
|
||||
#ubol-unpicker .customFilter.on > span.selector {
|
||||
background-color: var(--ink-1);
|
||||
color: var(--surface-1);
|
||||
}
|
||||
#ubol-unpicker .customFilter.removed > span.selector {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
#ubol-unpicker .customFilter.removed > span.remove {
|
||||
display: none;
|
||||
}
|
||||
#ubol-unpicker .customFilter.removed span.selector {
|
||||
pointer-events: none;
|
||||
}
|
||||
#ubol-unpicker .customFilter > span.fa-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 1.25em;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#ubol-unpicker .customFilter:not(.removed) > span.undo {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -1,54 +1,16 @@
|
|||
:root {
|
||||
:root#ubol-zapper {
|
||||
--quit-button-size: max(4em, min(6em, calc(100vw / 8), calc(100vh / 8)));
|
||||
--ubol-overlay-fill: rgba(255,255,63,0.10);
|
||||
--ubol-overlay-border: #FF0;
|
||||
}
|
||||
|
||||
html#ubol-zapper,
|
||||
#ubol-zapper body {
|
||||
background: transparent;
|
||||
cursor: not-allowed;
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
}
|
||||
#ubol-zapper :focus {
|
||||
outline: none;
|
||||
}
|
||||
#ubol-zapper aside {
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
position: fixed;
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
#ubol-zapper body[dir="rtl"] aside {
|
||||
left: 2px;
|
||||
right: unset;
|
||||
}
|
||||
#ubol-zapper svg#sea {
|
||||
cursor: crosshair;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#ubol-zapper svg#sea > path:first-child {
|
||||
fill: rgba(0,0,0,0.5);
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
#ubol-zapper svg#sea > path + path {
|
||||
stroke: #FF0;
|
||||
stroke-width: 0.5px;
|
||||
fill: rgba(255,255,63,0.20);
|
||||
}
|
||||
|
||||
#ubol-zapper aside > div {
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid rgba(0,0,0,0.5);
|
||||
|
|
|
|||
|
|
@ -133,7 +133,6 @@
|
|||
</div>
|
||||
<div class="li"><span data-i18n="aboutDependencies"></span></div>
|
||||
<div class="liul">
|
||||
<div class="li"><span><a href="https://github.com/chrismsimpson/Metropolis" target="_blank">Metropolis font family</a> by <a href="https://github.com/chrismsimpson">Chris Simpson</a></span></div>
|
||||
<div class="li"><span><a href="https://github.com/rsms/inter" target="_blank">Inter font family</a> by <a href="https://github.com/rsms">Rasmus Andersson</a></span></div>
|
||||
<div class="li"><span><a href="https://fontawesome.com/" target="_blank">FontAwesome font family</a> by <a href="https://github.com/davegandy">Dave Gandy</a></span></div>
|
||||
<div class="li"><span><a href="https://github.com/mathiasbynens/punycode.js" target="_blank">Punycode.js</a> by <a href="https://github.com/mathiasbynens">Mathias Bynens</a></span></div>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@ import {
|
|||
syncWithBrowserPermissions,
|
||||
} from './mode-manager.js';
|
||||
|
||||
import {
|
||||
addCustomFilter,
|
||||
hasCustomFilters,
|
||||
injectCustomFilters,
|
||||
removeCustomFilter,
|
||||
selectorsFromCustomFilters,
|
||||
uninjectCustomFilters,
|
||||
} from './filter-manager.js';
|
||||
|
||||
import {
|
||||
adminReadEx,
|
||||
getAdminRulesets,
|
||||
|
|
@ -56,7 +65,6 @@ import {
|
|||
getEffectiveDynamicRules,
|
||||
getEffectiveSessionRules,
|
||||
getEffectiveUserRules,
|
||||
getEnabledRulesetsDetails,
|
||||
getRulesetDetails,
|
||||
patchDefaultRulesets,
|
||||
setStrictBlockMode,
|
||||
|
|
@ -153,14 +161,15 @@ function setDeveloperMode(state) {
|
|||
|
||||
function onMessage(request, sender, callback) {
|
||||
|
||||
const tabId = sender?.tab?.id ?? false;
|
||||
const frameId = tabId && (sender?.frameId ?? false);
|
||||
|
||||
// Does not require trusted origin.
|
||||
|
||||
switch ( request.what ) {
|
||||
|
||||
case 'insertCSS': {
|
||||
const tabId = sender?.tab?.id ?? false;
|
||||
const frameId = sender?.frameId ?? false;
|
||||
if ( tabId === false || frameId === false ) { return; }
|
||||
if ( frameId === false ) { return false; }
|
||||
browser.scripting.insertCSS({
|
||||
css: request.css,
|
||||
origin: 'USER',
|
||||
|
|
@ -172,9 +181,7 @@ function onMessage(request, sender, callback) {
|
|||
}
|
||||
|
||||
case 'removeCSS': {
|
||||
const tabId = sender?.tab?.id ?? false;
|
||||
const frameId = sender?.frameId ?? false;
|
||||
if ( tabId === false || frameId === false ) { return; }
|
||||
if ( frameId === false ) { return false; }
|
||||
browser.scripting.removeCSS({
|
||||
css: request.css,
|
||||
origin: 'USER',
|
||||
|
|
@ -186,13 +193,26 @@ function onMessage(request, sender, callback) {
|
|||
}
|
||||
|
||||
case 'toggleToolbarIcon': {
|
||||
const tabId = sender?.tab?.id ?? false;
|
||||
if ( tabId ) {
|
||||
toggleToolbarIcon(tabId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case 'injectCustomFilters':
|
||||
if ( frameId === false ) { return false; }
|
||||
injectCustomFilters(tabId, frameId, request.hostname).then(selectors => {
|
||||
callback(selectors);
|
||||
});
|
||||
return true;
|
||||
|
||||
case 'uninjectCustomFilters':
|
||||
if ( frameId === false ) { return false; }
|
||||
uninjectCustomFilters(tabId, frameId, request.hostname).then(( ) => {
|
||||
callback();
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -301,24 +321,18 @@ function onMessage(request, sender, callback) {
|
|||
case 'popupPanelData': {
|
||||
Promise.all([
|
||||
hasBroadHostPermissions(),
|
||||
getFilteringMode(request.hostname),
|
||||
getEnabledRulesetsDetails(),
|
||||
getFilteringMode(request.normalHostname),
|
||||
adminReadEx('disabledFeatures'),
|
||||
hasCustomFilters(request.hostname),
|
||||
]).then(results => {
|
||||
const [
|
||||
hasOmnipotence,
|
||||
level,
|
||||
rulesetDetails,
|
||||
disabledFeatures,
|
||||
] = results;
|
||||
callback({
|
||||
hasOmnipotence,
|
||||
level,
|
||||
hasOmnipotence: results[0],
|
||||
level: results[1],
|
||||
autoReload: rulesetConfig.autoReload,
|
||||
rulesetDetails,
|
||||
isSideloaded,
|
||||
developerMode: rulesetConfig.developerMode,
|
||||
disabledFeatures,
|
||||
disabledFeatures: results[2],
|
||||
hasCustomFilters: results[3],
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
|
@ -432,6 +446,30 @@ function onMessage(request, sender, callback) {
|
|||
});
|
||||
return true;
|
||||
|
||||
case 'addCustomFilter':
|
||||
addCustomFilter(request.hostname, request.selector).then(modified => {
|
||||
if ( modified !== true ) { return; }
|
||||
return registerInjectables();
|
||||
}).then(( ) => {
|
||||
callback();
|
||||
})
|
||||
return true;
|
||||
|
||||
case 'removeCustomFilter':
|
||||
removeCustomFilter(request.hostname, request.selector).then(modified => {
|
||||
if ( modified !== true ) { return; }
|
||||
return registerInjectables();
|
||||
}).then(( ) => {
|
||||
callback();
|
||||
});
|
||||
return true;
|
||||
|
||||
case 'selectorsFromCustomFilters':
|
||||
selectorsFromCustomFilters(request.hostname).then(selectors => {
|
||||
callback(selectors);
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -446,7 +484,7 @@ function onCommand(command, tab) {
|
|||
case 'enter-zapper-mode': {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/zapper.js' ],
|
||||
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/zapper.js' ],
|
||||
target: { tabId: tab.id },
|
||||
});
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
export const webext = self.browser || self.chrome;
|
||||
export const dnr = webext.declarativeNetRequest;
|
||||
export const dnr = webext.declarativeNetRequest || {};
|
||||
export const INITIATOR_DOMAINS = 'initiatorDomains';
|
||||
export const EXCLUDED_INITIATOR_DOMAINS = 'excludedInitiatorDomains';
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,17 @@ export async function localRemove(key) {
|
|||
return browser.storage.local.remove(key);
|
||||
}
|
||||
|
||||
export async function localKeys() {
|
||||
if ( browser.storage instanceof Object === false ) { return; }
|
||||
if ( browser.storage.local instanceof Object === false ) { return; }
|
||||
if ( browser.storage.local.getKeys ) {
|
||||
return browser.storage.local.getKeys();
|
||||
}
|
||||
const bin = await browser.storage.local.get(null);
|
||||
if ( bin instanceof Object === false ) { return; }
|
||||
return Object.keys(bin);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function sessionRead(key) {
|
||||
|
|
|
|||
157
platform/mv3/extension/js/filter-manager.js
Normal file
157
platform/mv3/extension/js/filter-manager.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
|
||||
Copyright (C) 2022-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
import {
|
||||
browser,
|
||||
localKeys,
|
||||
localRead,
|
||||
localRemove,
|
||||
localWrite,
|
||||
} from './ext.js';
|
||||
|
||||
import {
|
||||
intersectHostnameIters,
|
||||
matchesFromHostnames,
|
||||
strArrayEq,
|
||||
subtractHostnameIters,
|
||||
} from './utils.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function selectorsFromCustomFilters(hostname) {
|
||||
const promises = [];
|
||||
let hn = hostname;
|
||||
while ( hn !== '' ) {
|
||||
promises.push(localRead(`site.${hn}`));
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
hn = hn.slice(pos + 1);
|
||||
}
|
||||
const results = await Promise.all(promises);
|
||||
const out = [];
|
||||
for ( let i = 0; i < promises.length; i++ ) {
|
||||
const selectors = results[i];
|
||||
if ( selectors === undefined ) { continue; }
|
||||
selectors.forEach(selector => { out.push(selector.slice(1)); });
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function hasCustomFilters(hostname) {
|
||||
const selectors = await selectorsFromCustomFilters(hostname);
|
||||
return selectors?.length ?? 0;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function injectCustomFilters(tabId, frameId, hostname) {
|
||||
const selectors = await selectorsFromCustomFilters(hostname);
|
||||
if ( selectors.length === 0 ) { return; }
|
||||
await browser.scripting.insertCSS({
|
||||
css: `${selectors.join(',\n')}{display:none!important;}`,
|
||||
origin: 'USER',
|
||||
target: { tabId, frameIds: [ frameId ] },
|
||||
}).catch(reason => {
|
||||
console.log(reason);
|
||||
});
|
||||
return selectors;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function uninjectCustomFilters(tabId, frameId, hostname) {
|
||||
const selectors = await selectorsFromCustomFilters(hostname);
|
||||
if ( selectors.length === 0 ) { return; }
|
||||
return browser.scripting.removeCSS({
|
||||
css: `${selectors.join(',\n')}{display:none!important;}`,
|
||||
origin: 'USER',
|
||||
target: { tabId, frameIds: [ frameId ] },
|
||||
}).catch(reason => {
|
||||
console.log(reason);
|
||||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function registerCustomFilters(context) {
|
||||
const storageKeys = await localKeys() || [];
|
||||
const siteKeys = storageKeys.filter(a => a.startsWith('site.'));
|
||||
if ( siteKeys.length === 0 ) { return; }
|
||||
|
||||
const { none } = context.filteringModeDetails;
|
||||
let hostnames = siteKeys.map(a => a.slice(5));
|
||||
if ( none.has('all-urls') ) {
|
||||
const { basic, optimal, complete } = context.filteringModeDetails;
|
||||
hostnames = intersectHostnameIters(hostnames, [
|
||||
...basic, ...optimal, ...complete
|
||||
]);
|
||||
} else if ( none.size !== 0 ) {
|
||||
hostnames = [ ...subtractHostnameIters(hostnames, none) ];
|
||||
}
|
||||
if ( hostnames.length === 0 ) { return; }
|
||||
|
||||
const registered = context.before.get('css-user');
|
||||
context.before.delete('css-user'); // Important!
|
||||
|
||||
const directive = {
|
||||
id: 'css-user',
|
||||
js: [ '/js/scripting/css-user.js' ],
|
||||
matches: matchesFromHostnames(hostnames),
|
||||
runAt: 'document_start',
|
||||
};
|
||||
|
||||
if ( registered === undefined ) {
|
||||
context.toAdd.push(directive);
|
||||
} else if ( strArrayEq(registered.matches, directive.matches) === false ) {
|
||||
context.toRemove.push('css-user');
|
||||
context.toAdd.push(directive);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function addCustomFilter(hostname, selector) {
|
||||
const key = `site.${hostname}`;
|
||||
const selectors = await localRead(key) || [];
|
||||
const filter = `0${selector}`;
|
||||
if ( selectors.includes(filter) ) { return false; }
|
||||
selectors.push(filter);
|
||||
selectors.sort();
|
||||
await localWrite(key, selectors);
|
||||
return true;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function removeCustomFilter(hostname, selector) {
|
||||
const key = `site.${hostname}`;
|
||||
const selectors = await localRead(key);
|
||||
if ( selectors === undefined ) { return false; }
|
||||
const i = selectors.indexOf(`0${selector}`);
|
||||
if ( i === -1 ) { return false; }
|
||||
selectors.splice(i, 1);
|
||||
await selectors.length !== 0
|
||||
? localWrite(key, selectors)
|
||||
: localRemove(key);
|
||||
return true;
|
||||
}
|
||||
431
platform/mv3/extension/js/picker-ui.js
Normal file
431
platform/mv3/extension/js/picker-ui.js
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
import { dom, qs$, qsa$ } from './dom.js';
|
||||
import { localRead, localWrite } from './ext.js';
|
||||
import { toolOverlay } from './tool-overlay-ui.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let selectorPartsDB = new Map();
|
||||
let sliderParts = [];
|
||||
let previewCSS = '';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function isValidSelector(selector) {
|
||||
isValidSelector.error = undefined;
|
||||
if ( selector === '' ) { return false; }
|
||||
try {
|
||||
void document.querySelector(`${selector},a`);
|
||||
} catch (reason) {
|
||||
isValidSelector.error = reason;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onSvgTouch(ev) {
|
||||
if ( ev.type === 'touchstart' ) {
|
||||
onSvgTouch.x0 = ev.touches[0].screenX;
|
||||
onSvgTouch.y0 = ev.touches[0].screenY;
|
||||
onSvgTouch.t0 = ev.timeStamp;
|
||||
return;
|
||||
}
|
||||
if ( onSvgTouch.x0 === undefined ) { return; }
|
||||
const stopX = ev.changedTouches[0].screenX;
|
||||
const stopY = ev.changedTouches[0].screenY;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(stopX - onSvgTouch.x0, 2) +
|
||||
Math.pow(stopY - onSvgTouch.y0, 2)
|
||||
);
|
||||
// Interpret touch events as a tap if:
|
||||
// - Swipe is not valid; and
|
||||
// - The time between start and stop was less than 200ms.
|
||||
const duration = ev.timeStamp - onSvgTouch.t0;
|
||||
if ( distance >= 32 || duration >= 200 ) { return; }
|
||||
onSvgClicked({
|
||||
type: 'touch',
|
||||
target: ev.target,
|
||||
clientX: ev.changedTouches[0].pageX,
|
||||
clientY: ev.changedTouches[0].pageY,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
onSvgTouch.x0 = onSvgTouch.y0 = 0;
|
||||
onSvgTouch.t0 = 0;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onSvgClicked(ev) {
|
||||
// Unpause picker if:
|
||||
// - click outside dialog AND
|
||||
// - not in preview mode
|
||||
if ( dom.cl.has(dom.root, 'paused') ) {
|
||||
if ( dom.cl.has(dom.root, 'preview') ) {
|
||||
updatePreview(false);
|
||||
}
|
||||
unpausePicker();
|
||||
return;
|
||||
}
|
||||
// Force dialog to always be visible when using a touch-driven device.
|
||||
if ( ev.type === 'touch' ) {
|
||||
dom.cl.add(dom.root, 'show');
|
||||
}
|
||||
toolOverlay.postMessage({
|
||||
what: 'candidatesAtPoint',
|
||||
mx: ev.clientX,
|
||||
my: ev.clientY,
|
||||
broad: ev.ctrlKey,
|
||||
}).then(details => {
|
||||
showDialog(details);
|
||||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onKeyPressed(ev) {
|
||||
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
||||
quitPicker();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMinimizeClicked() {
|
||||
if ( dom.cl.has(dom.root, 'paused') === false ) {
|
||||
pausePicker();
|
||||
highlightCandidate();
|
||||
return;
|
||||
}
|
||||
dom.cl.toggle(dom.root, 'minimized');
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onFilterTextChanged() {
|
||||
highlightCandidate();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function toggleView(view, persist = false) {
|
||||
dom.root.dataset.view = `${view}`;
|
||||
if ( persist !== true ) { return; }
|
||||
localWrite('picker.view', dom.root.dataset.view);
|
||||
}
|
||||
|
||||
function onViewToggled(dir) {
|
||||
let view = parseInt(dom.root.dataset.view, 10);
|
||||
view += dir;
|
||||
if ( view < 0 ) { view = 0; }
|
||||
if ( view > 2 ) { view = 2; }
|
||||
toggleView(view, true);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function selectorFromCandidates() {
|
||||
const selectorParts = [];
|
||||
let liPrevious = null;
|
||||
for ( const li of qsa$('#candidateFilters li') ) {
|
||||
const selector = [];
|
||||
for ( const span of qsa$(li, '.on[data-part]') ) {
|
||||
selector.push(span.textContent);
|
||||
}
|
||||
if ( selector.length !== 0 ) {
|
||||
if ( liPrevious !== null ) {
|
||||
if ( li.previousElementSibling === liPrevious ) {
|
||||
selectorParts.unshift(' > ');
|
||||
} else if ( liPrevious !== li ) {
|
||||
selectorParts.unshift(' ');
|
||||
}
|
||||
}
|
||||
liPrevious = li;
|
||||
selectorParts.unshift(selector.join(''));
|
||||
}
|
||||
}
|
||||
return selectorParts.join('');
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onSliderChanged(ev) {
|
||||
updateSlider(ev.target.valueAsNumber);
|
||||
}
|
||||
|
||||
function updateSlider(i) {
|
||||
qs$('#slider').value = i;
|
||||
dom.cl.remove('#candidateFilters [data-part]', 'on');
|
||||
const parts = sliderParts[i];
|
||||
for ( const address of parts ) {
|
||||
dom.cl.add(`#candidateFilters [data-part="${address}"]`, 'on');
|
||||
}
|
||||
const selector = selectorFromCandidates();
|
||||
qs$('textarea').value = selector;
|
||||
highlightCandidate();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function updateElementCount(details) {
|
||||
const { count, error } = details;
|
||||
const span = qs$('#resultsetCount');
|
||||
if ( error ) {
|
||||
span.textContent = 'Error';
|
||||
span.setAttribute('title', error);
|
||||
} else {
|
||||
span.textContent = count;
|
||||
span.removeAttribute('title');
|
||||
}
|
||||
const disabled = Boolean(count) === false ? '' : null;
|
||||
dom.attr('#create', 'disabled', disabled);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onPreviewClicked() {
|
||||
dom.cl.toggle(dom.root, 'preview');
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function updatePreview(state) {
|
||||
if ( state === undefined ) {
|
||||
state = dom.cl.has(dom.root, 'preview');
|
||||
} else {
|
||||
dom.cl.toggle(dom.root, 'preview', state)
|
||||
}
|
||||
if ( previewCSS !== '' ) {
|
||||
toolOverlay.postMessage({ what: 'removeCSS', css: previewCSS });
|
||||
previewCSS = '';
|
||||
}
|
||||
if ( state === false ) { return; }
|
||||
const selector = qs$('textarea').value;
|
||||
if ( isValidSelector(selector) === false ) { return; }
|
||||
previewCSS = `${selector}{display:none!important;}`;
|
||||
toolOverlay.postMessage({ what: 'insertCSS', css: previewCSS });
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function onCreateClicked() {
|
||||
const selector = qs$('textarea').value;
|
||||
if ( isValidSelector(selector) === false ) { return; }
|
||||
await toolOverlay.postMessage({ what: 'uninjectCustomFilters' }).then(( ) =>
|
||||
toolOverlay.sendMessage({
|
||||
what: 'addCustomFilter',
|
||||
hostname: toolOverlay.url.hostname,
|
||||
selector,
|
||||
})
|
||||
).then(( ) =>
|
||||
toolOverlay.postMessage({ what: 'injectCustomFilters' })
|
||||
);
|
||||
qs$('textarea').value = '';
|
||||
dom.cl.remove(dom.root, 'preview');
|
||||
quitPicker();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function attributeNameFromSelector(part) {
|
||||
const pos = part.search(/\^?=/);
|
||||
return part.slice(1, pos);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onCandidateClicked(ev) {
|
||||
const target = ev.target;
|
||||
if ( target.matches('[data-part]') ) {
|
||||
const address = target.dataset.part;
|
||||
const part = selectorPartsDB.get(parseInt(address, 10));
|
||||
if ( part.startsWith('[') ) {
|
||||
if ( target.textContent === part ) {
|
||||
target.textContent = `[${attributeNameFromSelector(part)}]`;
|
||||
dom.cl.remove(target, 'on');
|
||||
} else if ( dom.cl.has(target, 'on') ) {
|
||||
target.textContent = part;
|
||||
} else {
|
||||
dom.cl.add(target, 'on');
|
||||
}
|
||||
} else {
|
||||
dom.cl.toggle(target, 'on');
|
||||
}
|
||||
} else if ( target.matches('li') ) {
|
||||
if ( qs$(target, ':scope > span:not(.on)') !== null ) {
|
||||
dom.cl.add(qsa$(target, ':scope > [data-part]:not(.on)'), 'on');
|
||||
} else {
|
||||
dom.cl.remove(qsa$(target, ':scope > [data-part]'), 'on');
|
||||
}
|
||||
}
|
||||
const selector = selectorFromCandidates();
|
||||
qs$('textarea').value = selector;
|
||||
highlightCandidate();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function showDialog(msg) {
|
||||
pausePicker();
|
||||
|
||||
/* global */selectorPartsDB = new Map(msg.partsDB);
|
||||
const { listParts } = msg;
|
||||
const root = qs$('#candidateFilters');
|
||||
const ul = qs$(root, 'ul');
|
||||
while ( ul.firstChild !== null ) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
for ( const parts of listParts ) {
|
||||
const li = document.createElement('li');
|
||||
for ( const address of parts ) {
|
||||
const span = document.createElement('span');
|
||||
const part = selectorPartsDB.get(address);
|
||||
span.dataset.part = address;
|
||||
if ( part.startsWith('[') ) {
|
||||
span.textContent = `[${attributeNameFromSelector(part)}]`;
|
||||
} else {
|
||||
span.textContent = part;
|
||||
}
|
||||
li.append(span);
|
||||
}
|
||||
ul.appendChild(li);
|
||||
}
|
||||
|
||||
/* global */sliderParts = msg.sliderParts;
|
||||
const slider = qs$('#slider');
|
||||
const last = sliderParts.length - 1;
|
||||
dom.attr(slider, 'max', last);
|
||||
dom.attr(slider, 'value', last);
|
||||
dom.attr(slider, 'disabled', last !== 0 ? null : '');
|
||||
updateSlider(last);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function highlightCandidate() {
|
||||
const selector = qs$('textarea').value;
|
||||
if ( isValidSelector(selector) === false ) {
|
||||
toolOverlay.postMessage({ what: 'unhighlight' });
|
||||
updateElementCount({ count: 0, error: isValidSelector.error });
|
||||
return;
|
||||
}
|
||||
toolOverlay.postMessage({
|
||||
what: 'highlightFromSelector',
|
||||
selector,
|
||||
}).then(result => {
|
||||
updateElementCount(result);
|
||||
});
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* paused:
|
||||
* - select element mode disabled
|
||||
* - preview mode enabled or disabled
|
||||
* - dialog unminimized
|
||||
*
|
||||
* unpaused:
|
||||
* - select element mode enabled
|
||||
* - preview mode disabled
|
||||
* - dialog minimized
|
||||
*
|
||||
* */
|
||||
|
||||
function pausePicker() {
|
||||
dom.cl.add(dom.root, 'paused');
|
||||
dom.cl.remove(dom.root, 'minimized');
|
||||
toolOverlay.highlightElementUnderMouse(false);
|
||||
}
|
||||
|
||||
function unpausePicker() {
|
||||
dom.cl.remove(dom.root, 'paused', 'preview');
|
||||
dom.cl.add(dom.root, 'minimized');
|
||||
updatePreview();
|
||||
toolOverlay.postMessage({
|
||||
what: 'togglePreview',
|
||||
state: false,
|
||||
});
|
||||
toolOverlay.highlightElementUnderMouse(true);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function startPicker() {
|
||||
toolOverlay.postMessage({ what: 'startTool' });
|
||||
|
||||
localRead('picker.view').then(value => {
|
||||
if ( Boolean(value) === false ) { return; }
|
||||
toggleView(value);
|
||||
});
|
||||
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
dom.on('svg#overlay', 'click', onSvgClicked);
|
||||
dom.on('svg#overlay', 'touchstart', onSvgTouch, { passive: true });
|
||||
dom.on('svg#overlay', 'touchend', onSvgTouch);
|
||||
dom.on('#minimize', 'click', onMinimizeClicked);
|
||||
dom.on('textarea', 'input', onFilterTextChanged);
|
||||
dom.on('#quit', 'click', quitPicker);
|
||||
dom.on('#slider', 'input', onSliderChanged);
|
||||
dom.on('#pick', 'click', resetPicker);
|
||||
dom.on('#preview', 'click', onPreviewClicked);
|
||||
dom.on('#moreOrLess > span:first-of-type', 'click', ( ) => { onViewToggled(1); });
|
||||
dom.on('#moreOrLess > span:last-of-type', 'click', ( ) => { onViewToggled(-1); });
|
||||
dom.on('#create', 'click', ( ) => { onCreateClicked(); });
|
||||
dom.on('#candidateFilters ul', 'click', onCandidateClicked);
|
||||
toolOverlay.highlightElementUnderMouse(true);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function quitPicker() {
|
||||
updatePreview(false);
|
||||
toolOverlay.stop();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function resetPicker() {
|
||||
toolOverlay.postMessage({ what: 'unhighlight' });
|
||||
unpausePicker();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'startTool':
|
||||
startPicker();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Wait for the content script to establish communication
|
||||
toolOverlay.start(onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -19,15 +19,9 @@
|
|||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
import {
|
||||
browser,
|
||||
localRead, localWrite,
|
||||
runtime,
|
||||
sendMessage,
|
||||
} from './ext.js';
|
||||
|
||||
import { browser, runtime, sendMessage } from './ext.js';
|
||||
import { dom, qs$ } from './dom.js';
|
||||
import { i18n, i18n$ } from './i18n.js';
|
||||
import { i18n$ } from './i18n.js';
|
||||
import punycode from './punycode.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -208,8 +202,7 @@ dom.on(
|
|||
ev => {
|
||||
const span = ev.target;
|
||||
const level = parseInt(span.dataset.level, 10);
|
||||
dom.text(
|
||||
'#filteringModeText > span:nth-of-type(2)',
|
||||
dom.text('#filteringModeText > span:nth-of-type(2)',
|
||||
i18n$(`filteringMode${level}Name`)
|
||||
);
|
||||
}
|
||||
|
|
@ -226,65 +219,7 @@ dom.on(
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
// The popup panel is made of sections. Visibility of sections can be
|
||||
// toggled on/off.
|
||||
|
||||
const maxNumberOfSections = 2;
|
||||
|
||||
const sectionBitsFromAttribute = function() {
|
||||
const value = dom.body.dataset.section;
|
||||
if ( value === '' ) { return 0; }
|
||||
let bits = 0;
|
||||
for ( const c of value.split(' ') ) {
|
||||
bits |= 1 << (c.charCodeAt(0) - 97);
|
||||
}
|
||||
return bits;
|
||||
};
|
||||
|
||||
const sectionBitsToAttribute = function(bits) {
|
||||
if ( typeof bits !== 'number' ) { return; }
|
||||
if ( isNaN(bits) ) { return; }
|
||||
const value = [];
|
||||
for ( let i = 0; i < maxNumberOfSections; i++ ) {
|
||||
const bit = 1 << i;
|
||||
if ( (bits & bit) === 0 ) { continue; }
|
||||
value.push(String.fromCharCode(97 + i));
|
||||
}
|
||||
dom.body.dataset.section = value.join(' ');
|
||||
};
|
||||
|
||||
async function toggleSections(more) {
|
||||
let currentBits = sectionBitsFromAttribute();
|
||||
let newBits = currentBits;
|
||||
for ( let i = 0; i < maxNumberOfSections; i++ ) {
|
||||
const bit = 1 << (more ? i : maxNumberOfSections - i - 1);
|
||||
if ( more ) {
|
||||
newBits |= bit;
|
||||
} else {
|
||||
newBits &= ~bit;
|
||||
}
|
||||
if ( newBits !== currentBits ) { break; }
|
||||
}
|
||||
if ( newBits === currentBits ) { return; }
|
||||
sectionBitsToAttribute(newBits);
|
||||
localWrite('popupPanelSections', newBits);
|
||||
}
|
||||
|
||||
localRead('popupPanelSections').then(bits => {
|
||||
sectionBitsToAttribute(bits || 0);
|
||||
});
|
||||
|
||||
dom.on('#moreButton', 'click', ( ) => {
|
||||
toggleSections(true);
|
||||
});
|
||||
|
||||
dom.on('#lessButton', 'click', ( ) => {
|
||||
toggleSections(false);
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('#showMatchedRules', 'click', ev => {
|
||||
dom.on('#gotoMatchedRules', 'click', ev => {
|
||||
if ( ev.isTrusted !== true ) { return; }
|
||||
if ( ev.button !== 0 ) { return; }
|
||||
sendMessage({
|
||||
|
|
@ -295,7 +230,7 @@ dom.on('#showMatchedRules', 'click', ev => {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('[data-i18n-title="popupTipReport"]', 'click', ev => {
|
||||
dom.on('#gotoReport', 'click', ev => {
|
||||
if ( ev.isTrusted !== true ) { return; }
|
||||
let url;
|
||||
try {
|
||||
|
|
@ -314,7 +249,7 @@ dom.on('[data-i18n-title="popupTipReport"]', 'click', ev => {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('[data-i18n-title="popupTipDashboard"]', 'click', ev => {
|
||||
dom.on('#gotoDashboard', 'click', ev => {
|
||||
if ( ev.isTrusted !== true ) { return; }
|
||||
if ( ev.button !== 0 ) { return; }
|
||||
runtime.openOptionsPage();
|
||||
|
|
@ -325,7 +260,29 @@ dom.on('[data-i18n-title="popupTipDashboard"]', 'click', ev => {
|
|||
dom.on('#gotoZapper', 'click', ( ) => {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/zapper.js' ],
|
||||
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/zapper.js' ],
|
||||
target: { tabId: currentTab.id },
|
||||
});
|
||||
self.close();
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('#gotoPicker', 'click', ( ) => {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/picker.js' ],
|
||||
target: { tabId: currentTab.id },
|
||||
});
|
||||
self.close();
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('#gotoUnpicker', 'click', ( ) => {
|
||||
if ( browser.scripting === undefined ) { return; }
|
||||
browser.scripting.executeScript({
|
||||
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/unpicker.js' ],
|
||||
target: { tabId: currentTab.id },
|
||||
});
|
||||
self.close();
|
||||
|
|
@ -357,7 +314,8 @@ async function init() {
|
|||
const response = await sendMessage({
|
||||
what: 'popupPanelData',
|
||||
origin: url.origin,
|
||||
hostname: normalizedHostname(tabURL.hostname),
|
||||
normalHostname: normalizedHostname(tabURL.hostname),
|
||||
hostname: tabURL.hostname,
|
||||
});
|
||||
if ( response instanceof Object ) {
|
||||
Object.assign(popupPanelData, response);
|
||||
|
|
@ -370,46 +328,17 @@ async function init() {
|
|||
|
||||
dom.text('#hostname', punycode.toUnicode(tabURL.hostname));
|
||||
|
||||
dom.cl.toggle('#showMatchedRules', 'enabled',
|
||||
dom.cl.toggle('#gotoMatchedRules', 'enabled',
|
||||
popupPanelData.isSideloaded === true &&
|
||||
popupPanelData.developerMode &&
|
||||
typeof currentTab.id === 'number' &&
|
||||
isNaN(currentTab.id) === false
|
||||
);
|
||||
|
||||
const isNetworkPage = url.protocol === 'http:' || url.protocol === 'https:';
|
||||
const isHTTP = url.protocol === 'http:' || url.protocol === 'https:';
|
||||
dom.cl.toggle(dom.root, 'isHTTP', isHTTP);
|
||||
|
||||
dom.cl.toggle('#reportFilterIssue', 'enabled', isNetworkPage );
|
||||
dom.cl.toggle('#gotoZapper', 'enabled', isNetworkPage);
|
||||
|
||||
const parent = qs$('#rulesetStats');
|
||||
for ( const details of popupPanelData.rulesetDetails || [] ) {
|
||||
const div = dom.clone('#templates .rulesetDetails');
|
||||
qs$(div, 'h1').append(i18n.patchUnicodeFlags(details.name));
|
||||
const { rules, filters, css } = details;
|
||||
let ruleCount = rules.plain + rules.regex;
|
||||
if ( popupPanelData.hasOmnipotence ) {
|
||||
ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders;
|
||||
}
|
||||
let specificCount = 0;
|
||||
if ( typeof css.specific === 'number' ) {
|
||||
specificCount += css.specific;
|
||||
}
|
||||
if ( typeof css.declarative === 'number' ) {
|
||||
specificCount += css.declarative;
|
||||
}
|
||||
if ( typeof css.procedural === 'number' ) {
|
||||
specificCount += css.procedural;
|
||||
}
|
||||
dom.text(
|
||||
qs$(div, 'p'),
|
||||
i18n$('perRulesetStats')
|
||||
.replace('{{ruleCount}}', ruleCount.toLocaleString())
|
||||
.replace('{{filterCount}}', filters.accepted.toLocaleString())
|
||||
.replace('{{cssSpecificCount}}', specificCount.toLocaleString())
|
||||
);
|
||||
parent.append(div);
|
||||
}
|
||||
dom.cl.toggle('#gotoUnpicker', 'enabled', popupPanelData.hasCustomFilters);
|
||||
|
||||
dom.cl.remove(dom.body, 'loading');
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { fetchJSON } from './fetch.js';
|
||||
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
|
||||
import { getFilteringModeDetails } from './mode-manager.js';
|
||||
import { registerCustomFilters } from './filter-manager.js';
|
||||
import { registerToolbarIconToggler } from './action.js';
|
||||
import { ubolLog } from './debug.js';
|
||||
|
||||
|
|
@ -600,13 +601,16 @@ async function registerInjectables() {
|
|||
toRemove,
|
||||
};
|
||||
|
||||
registerDeclarative(context);
|
||||
registerProcedural(context);
|
||||
registerScriptlet(context, scriptletDetails);
|
||||
registerSpecific(context);
|
||||
registerGeneric(context, genericDetails);
|
||||
registerHighGeneric(context, genericDetails);
|
||||
registerToolbarIconToggler(context);
|
||||
await Promise.all([
|
||||
registerDeclarative(context),
|
||||
registerProcedural(context),
|
||||
registerScriptlet(context, scriptletDetails),
|
||||
registerSpecific(context),
|
||||
registerGeneric(context, genericDetails),
|
||||
registerHighGeneric(context, genericDetails),
|
||||
registerCustomFilters(context),
|
||||
registerToolbarIconToggler(context),
|
||||
]);
|
||||
|
||||
toRemove.push(...Array.from(before.keys()));
|
||||
|
||||
|
|
|
|||
39
platform/mv3/extension/js/scripting/css-user.js
Normal file
39
platform/mv3/extension/js/scripting/css-user.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
|
||||
Copyright (C) 2019-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
(async function uBOL_cssUser() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const docURL = new URL(document.baseURI);
|
||||
chrome.runtime.sendMessage({
|
||||
what: 'injectCustomFilters',
|
||||
hostname: docURL.hostname,
|
||||
}).then(selectors => {
|
||||
self.ubolInjectedCustomFilters = selectors;
|
||||
}).catch(( ) => {
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
void 0;
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
const r = callback(`${hnparts.slice(i).join('.')}`, details);
|
||||
if ( r !== undefined ) { return r; }
|
||||
}
|
||||
if ( details.hasEntities !== true ) { return; }
|
||||
if ( details?.hasEntities !== true ) { return; }
|
||||
const n = hnpartslen - 1;
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
for ( let j = n; j > i; j-- ) {
|
||||
|
|
|
|||
270
platform/mv3/extension/js/scripting/picker.js
Normal file
270
platform/mv3/extension/js/scripting/picker.js
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
(async ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const ubolOverlay = self.ubolOverlay;
|
||||
if ( ubolOverlay === undefined ) { return; }
|
||||
if ( ubolOverlay.file === '/picker-ui.html' ) { return; }
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function attributeNameFromPart(part) {
|
||||
const pos = part.search(/\^?=/);
|
||||
return part.slice(1, pos);
|
||||
}
|
||||
|
||||
function selectorFromAddresses(partsDB, addresses) {
|
||||
const selector = [];
|
||||
let majorLast = -1;
|
||||
for ( const address of addresses ) {
|
||||
const major = address >>> 12;
|
||||
if ( majorLast !== -1 ) {
|
||||
const delta = majorLast - major;
|
||||
if ( delta > 1 ) {
|
||||
selector.push(' ');
|
||||
} else if ( delta === 1 ) {
|
||||
selector.push(' > ');
|
||||
}
|
||||
}
|
||||
majorLast = major;
|
||||
const part = partsDB.get(address);
|
||||
selector.push(
|
||||
(address & 0xF) === 3
|
||||
? `[${attributeNameFromPart(part)}]`
|
||||
: part
|
||||
);
|
||||
}
|
||||
return selector.join('');
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Selector part address:
|
||||
* 0b00000000_00000000_0000
|
||||
* | | |
|
||||
* | | +-- 4-bit: Descriptor
|
||||
* | +------- 8-bit: Part index
|
||||
* +---------------- 8-bit: List index
|
||||
* Descriptor:
|
||||
* - 0: tag name
|
||||
* - 1: id
|
||||
* - 2: class
|
||||
* - 3: attribute
|
||||
* - 4: :nth-of-type
|
||||
* List index: 0 is deepest
|
||||
*
|
||||
* Selector part addresses are used to reference parts in associated database.
|
||||
*
|
||||
* */
|
||||
|
||||
function candidatesAtPoint(x, y) {
|
||||
// We need at least one element.
|
||||
let elem = null;
|
||||
if ( typeof x === 'number' ) {
|
||||
elem = ubolOverlay.elementFromPoint(x, y);
|
||||
} else if ( x instanceof HTMLElement ) {
|
||||
elem = x;
|
||||
x = undefined;
|
||||
}
|
||||
|
||||
const partsDB = new Map();
|
||||
const listParts = [];
|
||||
while ( elem && elem !== document.body ) {
|
||||
const tagName = elem.localName;
|
||||
const addressMajor = listParts.length << 12;
|
||||
partsDB.set(addressMajor, CSS.escape(tagName));
|
||||
const parts = [ addressMajor ];
|
||||
// Id
|
||||
if ( typeof elem.id === 'string' && elem.id !== '' ) {
|
||||
const address = addressMajor | parts.length << 4 | 1;
|
||||
partsDB.set(address, `#${CSS.escape(elem.id)}`);
|
||||
parts.push(address);
|
||||
}
|
||||
// Classes
|
||||
for ( const name of elem.classList.values() ) {
|
||||
const address = addressMajor | parts.length << 4 | 2;
|
||||
partsDB.set(address, `.${CSS.escape(name)}`);
|
||||
parts.push(address);
|
||||
}
|
||||
// Attributes
|
||||
for ( const name of elem.getAttributeNames() ) {
|
||||
if ( name === 'id' || name === 'class' ) { continue; }
|
||||
if ( excludedAttributeExpansion.includes(name) ) {
|
||||
const address = addressMajor | parts.length << 4 | 3;
|
||||
partsDB.set(address, `[${CSS.escape(name)}]`);
|
||||
parts.push(address);
|
||||
continue;
|
||||
}
|
||||
let value = elem.getAttribute(name);
|
||||
const pos = value.search(/[\n\r]/);
|
||||
if ( pos !== -1 ) {
|
||||
value = value.slice(0, pos);
|
||||
}
|
||||
const address = addressMajor | parts.length << 4 | 3;
|
||||
partsDB.set(address, `[${CSS.escape(name)}="${value}"]`);
|
||||
parts.push(address);
|
||||
}
|
||||
// https://github.com/chrisaljoudi/uBlock/issues/637
|
||||
// If the selector is still ambiguous at this point, further narrow
|
||||
// using `:nth-of-type`.
|
||||
const parentNode = elem.parentNode;
|
||||
if ( ubolOverlay.qsa(parentNode, `:scope > ${selectorFromAddresses(partsDB, parts)}`).length > 1 ) {
|
||||
let i = 1;
|
||||
while ( elem.previousSibling !== null ) {
|
||||
elem = elem.previousSibling;
|
||||
if ( typeof elem.localName !== 'string' ) { continue; }
|
||||
if ( elem.localName !== tagName ) { continue; }
|
||||
i++;
|
||||
}
|
||||
const address = addressMajor | parts.length << 4 | 4;
|
||||
partsDB.set(address, `:nth-of-type(${i})`);
|
||||
parts.push(address);
|
||||
}
|
||||
listParts.push(parts);
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
if ( listParts.length === 0 ) { return; }
|
||||
|
||||
const sliderCandidates = [];
|
||||
for ( let i = 0, n = listParts.length; i < n; i++ ) {
|
||||
sliderCandidates.push(listParts[i]);
|
||||
for ( let j = i + 1; j < n; j++ ) {
|
||||
sliderCandidates.push([
|
||||
...listParts[j],
|
||||
...sliderCandidates.at(-1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
const sliderMap = new Map();
|
||||
for ( const candidates of sliderCandidates ) {
|
||||
if ( candidates.some(a => (a & 0xF) === 1) ) {
|
||||
const selectorPath = candidates.filter(a => (a & 0xF) === 1);
|
||||
sliderMap.set(JSON.stringify(selectorPath), 0);
|
||||
} else if ( candidates.some(a => (a & 0xF) === 4) ) {
|
||||
const selectorPath = candidates.filter(a => {
|
||||
return a &= 0xF, a === 0 || a === 4;
|
||||
});
|
||||
sliderMap.set(JSON.stringify(selectorPath), 0);
|
||||
}
|
||||
if ( candidates.some(a => (a & 0xF) === 2) ) {
|
||||
const selectorPath = candidates.filter(a => {
|
||||
return a &= 0xF, a === 0 || a === 2;
|
||||
});
|
||||
sliderMap.set(JSON.stringify(selectorPath), 0);
|
||||
}
|
||||
const selectorPath = candidates.filter(a => {
|
||||
return a &= 0xF, a === 0 || a === 3;
|
||||
});
|
||||
sliderMap.set(JSON.stringify(selectorPath), 0);
|
||||
}
|
||||
sliderMap.delete('[]');
|
||||
const elemToIdMap = new Map();
|
||||
const resultSetMap = new Map();
|
||||
let elemId = 1;
|
||||
for ( const json of sliderMap.keys() ) {
|
||||
const addresses = JSON.parse(json);
|
||||
const selector = selectorFromAddresses(partsDB, addresses);
|
||||
if ( excludedSelectors.includes(selector) ) { continue; }
|
||||
const elems = ubolOverlay.qsa(document, selector);
|
||||
if ( elems.length === 0 ) { continue; }
|
||||
const resultSet = [];
|
||||
for ( const elem of elems ) {
|
||||
if ( elemToIdMap.has(elem) === false ) {
|
||||
elemToIdMap.set(elem, elemId++);
|
||||
}
|
||||
resultSet.push(elemToIdMap.get(elem));
|
||||
}
|
||||
const resultSetKey = JSON.stringify(resultSet.sort());
|
||||
const current = resultSetMap.get(resultSetKey);
|
||||
if ( current ) {
|
||||
if ( current.length < addresses.length ) { continue; }
|
||||
if ( current.length === addresses.length ) {
|
||||
if ( addresses.some(a => (a & 0xF) === 2) === false ) {
|
||||
if ( current.some(a => (a & 0xF) === 2) ) { continue; }
|
||||
}
|
||||
}
|
||||
}
|
||||
resultSetMap.set(resultSetKey, addresses);
|
||||
}
|
||||
const sliderParts = Array.from(resultSetMap).toSorted((a, b) => {
|
||||
let amajor = a[1].at(-1) >>> 12;
|
||||
let bmajor = b[1].at(-1) >>> 12;
|
||||
if ( amajor !== bmajor ) { return bmajor - amajor; }
|
||||
amajor = a[1].at(0) >>> 12;
|
||||
bmajor = b[1].at(0) >>> 12;
|
||||
if ( amajor !== bmajor ) { return bmajor - amajor; }
|
||||
if ( a[0].length !== b[0].length ) {
|
||||
return b[0].length - a[0].length;
|
||||
}
|
||||
return b[1].length - a[1].length;
|
||||
}).map(a => a[1]);
|
||||
return {
|
||||
partsDB: Array.from(partsDB),
|
||||
listParts,
|
||||
sliderParts,
|
||||
};
|
||||
}
|
||||
|
||||
const excludedAttributeExpansion = [
|
||||
'sizes',
|
||||
'srcset',
|
||||
];
|
||||
const excludedSelectors = [
|
||||
'div',
|
||||
'span',
|
||||
];
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'injectCustomFilters':
|
||||
return ubolOverlay.sendMessage({ what: 'injectCustomFilters',
|
||||
hostname: ubolOverlay.url.hostname,
|
||||
});
|
||||
case 'uninjectCustomFilters':
|
||||
return ubolOverlay.sendMessage({ what: 'uninjectCustomFilters',
|
||||
hostname: ubolOverlay.url.hostname,
|
||||
});
|
||||
case 'candidatesAtPoint':
|
||||
return candidatesAtPoint(msg.mx, msg.my, msg.broad);
|
||||
case 'insertCSS':
|
||||
return ubolOverlay.sendMessage(msg);
|
||||
case 'removeCSS':
|
||||
return ubolOverlay.sendMessage(msg);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
await ubolOverlay.install('/picker-ui.html', onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
|
||||
void 0;
|
||||
360
platform/mv3/extension/js/scripting/tool-overlay.js
Normal file
360
platform/mv3/extension/js/scripting/tool-overlay.js
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
(function uBOLOverlay() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if ( self.ubolOverlay ) {
|
||||
self.ubolOverlay.stop();
|
||||
self.ubolOverlay = undefined;
|
||||
}
|
||||
|
||||
self.ubolOverlay = {
|
||||
file: '',
|
||||
webext: typeof browser === 'object' ? browser : chrome,
|
||||
url: new URL(document.baseURI),
|
||||
port: null,
|
||||
highlightedElements: [],
|
||||
secretAttr: (( ) => {
|
||||
let secret = String.fromCharCode((Math.random() * 26) + 97);
|
||||
do {
|
||||
secret += (Math.floor(Math.random() * 2147483647) + 2147483647)
|
||||
.toString(36)
|
||||
.slice(2);
|
||||
} while ( secret.length < 8 );
|
||||
return secret;
|
||||
})(),
|
||||
|
||||
start() {
|
||||
const cssStyle = [
|
||||
'background: transparent',
|
||||
'border: 0',
|
||||
'border-radius: 0',
|
||||
'box-shadow: none',
|
||||
'color-scheme: light dark',
|
||||
'display: block',
|
||||
'filter: none',
|
||||
'height: 100vh',
|
||||
' height: 100svh',
|
||||
'left: 0',
|
||||
'margin: 0',
|
||||
'max-height: none',
|
||||
'max-width: none',
|
||||
'min-height: unset',
|
||||
'min-width: unset',
|
||||
'opacity: 1',
|
||||
'outline: 0',
|
||||
'padding: 0',
|
||||
'pointer-events: auto',
|
||||
'position: fixed',
|
||||
'top: 0',
|
||||
'transform: none',
|
||||
'visibility: hidden',
|
||||
'width: 100%',
|
||||
'z-index: 2147483647',
|
||||
''
|
||||
].join(' !important;\n');
|
||||
this.pickerCSS = [
|
||||
`:root > [${this.secretAttr}] { ${cssStyle} }`,
|
||||
`:root > [${this.secretAttr}-loaded] { visibility: visible !important; }`,
|
||||
`:root > [${this.secretAttr}-click] { pointer-events: none !important; }`,
|
||||
].join('\n');
|
||||
this.sendMessage({ what: 'insertCSS', css: this.pickerCSS });
|
||||
self.addEventListener('scroll', this.onViewportChanged, { passive: true });
|
||||
self.addEventListener('resize', this.onViewportChanged, { passive: true });
|
||||
self.addEventListener('keydown', this.onKeyPressed, true);
|
||||
},
|
||||
|
||||
stop() {
|
||||
if ( this.pickerCSS ) {
|
||||
this.sendMessage({ what: 'removeCSS', css: this.pickerCSS });
|
||||
this.pickerCSS = undefined;
|
||||
}
|
||||
self.removeEventListener('scroll', this.onViewportChanged, { passive: true });
|
||||
self.removeEventListener('resize', this.onViewportChanged, { passive: true });
|
||||
self.removeEventListener('keydown', this.onKeyPressed, true);
|
||||
if ( this.frame ) {
|
||||
this.frame.remove();
|
||||
this.frame = null;
|
||||
}
|
||||
if ( this.port ) {
|
||||
this.port.close();
|
||||
this.port.onmessage = null;
|
||||
this.port.onmessageerror = null;
|
||||
this.port = null;
|
||||
}
|
||||
this.onmessage = null;
|
||||
self.ubolOverlay = undefined;
|
||||
},
|
||||
|
||||
onViewportChanged() {
|
||||
self.ubolOverlay.highlightUpdate();
|
||||
},
|
||||
|
||||
onKeyPressed(ev) {
|
||||
if ( ev.key !== 'Escape' && ev.which !== 27 ) { return; }
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if ( self.ubolOverlay.onmessage ) {
|
||||
self.ubolOverlay.onmessage({ what: 'quitTool' });
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage(msg) {
|
||||
try {
|
||||
return this.webext.runtime.sendMessage(msg).catch(( ) => { });
|
||||
} catch {
|
||||
}
|
||||
},
|
||||
|
||||
onMessage(wrapped) {
|
||||
// Response to script-initiated message?
|
||||
if ( typeof wrapped?.fromScriptId === 'number' ) {
|
||||
const resolve = this.pendingMessages.get(wrapped.fromScriptId);
|
||||
if ( resolve ) {
|
||||
this.pendingMessages.delete(wrapped.fromScriptId);
|
||||
resolve(wrapped.msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const onmessage = this.onmessage;
|
||||
const msg = wrapped.msg || wrapped;
|
||||
let response;
|
||||
switch ( msg.what ) {
|
||||
case 'startTool':
|
||||
this.start();
|
||||
break;
|
||||
case 'quitTool':
|
||||
this.stop();
|
||||
break;
|
||||
case 'highlightElementAtPoint':
|
||||
this.highlightElementAtPoint(msg.mx, msg.my);
|
||||
break;
|
||||
case 'highlightFromSelector': {
|
||||
const { elems, error } = this.elementsFromSelector(msg.selector);
|
||||
this.highlightElements(elems);
|
||||
if ( msg.scrollTo && elems.length !== 0 ) {
|
||||
elems[0].scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
response = { count: elems.length, error };
|
||||
break;
|
||||
}
|
||||
case 'unhighlight':
|
||||
this.unhighlight();
|
||||
break;
|
||||
}
|
||||
response = onmessage && onmessage(msg) || response;
|
||||
// Send response if this is frame-initiated message
|
||||
if ( wrapped?.fromFrameId && this.port ) {
|
||||
const { fromFrameId } = wrapped;
|
||||
if ( response instanceof Promise ) {
|
||||
response.then(response => {
|
||||
if ( this.port === null ) { return; }
|
||||
this.port.postMessage({ fromFrameId, msg: response });
|
||||
});
|
||||
} else {
|
||||
this.port.postMessage({ fromFrameId, msg: response });
|
||||
}
|
||||
}
|
||||
},
|
||||
postMessage(msg) {
|
||||
if ( this.port === null ) { return; }
|
||||
const wrapped = {
|
||||
fromScriptId: this.messageId++,
|
||||
msg,
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
this.pendingMessages.set(wrapped.fromScriptId, resolve);
|
||||
this.port.postMessage(wrapped);
|
||||
});
|
||||
},
|
||||
messageId: 1,
|
||||
pendingMessages: new Map(),
|
||||
|
||||
getElementBoundingClientRect(elem) {
|
||||
let rect = typeof elem.getBoundingClientRect === 'function'
|
||||
? elem.getBoundingClientRect()
|
||||
: { height: 0, left: 0, top: 0, width: 0 };
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/1024
|
||||
// Try not returning an empty bounding rect.
|
||||
if ( rect.width !== 0 && rect.height !== 0 ) {
|
||||
return rect;
|
||||
}
|
||||
if ( elem.shadowRoot instanceof DocumentFragment ) {
|
||||
return this.getElementBoundingClientRect(elem.shadowRoot);
|
||||
}
|
||||
let left = rect.left,
|
||||
right = left + rect.width,
|
||||
top = rect.top,
|
||||
bottom = top + rect.height;
|
||||
for ( const child of elem.children ) {
|
||||
rect = this.getElementBoundingClientRect(child);
|
||||
if ( rect.width === 0 || rect.height === 0 ) { continue; }
|
||||
if ( rect.left < left ) { left = rect.left; }
|
||||
if ( rect.right > right ) { right = rect.right; }
|
||||
if ( rect.top < top ) { top = rect.top; }
|
||||
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
|
||||
}
|
||||
return {
|
||||
left, right,
|
||||
top, bottom,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
};
|
||||
},
|
||||
|
||||
highlightUpdate() {
|
||||
const ow = self.innerWidth;
|
||||
const oh = self.innerHeight;
|
||||
const islands = [];
|
||||
for ( const elem of this.highlightedElements ) {
|
||||
const rect = this.getElementBoundingClientRect(elem);
|
||||
// Ignore offscreen areas
|
||||
if ( rect.left > ow ) { continue; }
|
||||
if ( rect.top > oh ) { continue; }
|
||||
if ( rect.left + rect.width < 0 ) { continue; }
|
||||
if ( rect.top + rect.height < 0 ) { continue; }
|
||||
islands.push(
|
||||
`M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
|
||||
);
|
||||
}
|
||||
this.port.postMessage({
|
||||
what: 'svgPaths',
|
||||
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
|
||||
islands: islands.join(''),
|
||||
});
|
||||
},
|
||||
|
||||
highlightElements(iter = []) {
|
||||
this.highlightedElements =
|
||||
Array.from(iter).filter(a =>
|
||||
a instanceof Element && a !== this.frame
|
||||
);
|
||||
this.highlightUpdate();
|
||||
},
|
||||
|
||||
qsa(node, selector) {
|
||||
if ( node !== null ) {
|
||||
try {
|
||||
const elems = node.querySelectorAll(selector);
|
||||
this.qsa.error = undefined;
|
||||
return elems;
|
||||
} catch (reason) {
|
||||
this.qsa.error = `${reason}`;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
elementFromPoint(x, y) {
|
||||
if ( x !== undefined ) {
|
||||
this.lastX = x; this.lastY = y;
|
||||
} else if ( this.lastX !== undefined ) {
|
||||
x = this.lastX; y = this.lastY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const magicAttr = `${this.secretAttr}-click`;
|
||||
this.frame.setAttribute(magicAttr, '');
|
||||
let elem = document.elementFromPoint(x, y);
|
||||
if ( elem === document.body || elem === document.documentElement ) {
|
||||
elem = null;
|
||||
}
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
|
||||
this.frame.removeAttribute(magicAttr);
|
||||
return elem;
|
||||
},
|
||||
|
||||
elementsFromSelector(selector) {
|
||||
const elems = this.qsa(document, selector);
|
||||
return { elems, error: this.qsa.error };
|
||||
},
|
||||
|
||||
highlightElementAtPoint(x, y) {
|
||||
const elem = self.ubolOverlay.elementFromPoint(x, y);
|
||||
this.highlightElements([ elem ]);
|
||||
},
|
||||
|
||||
unhighlight() {
|
||||
this.highlightElements([]);
|
||||
},
|
||||
|
||||
async install(file, onmessage) {
|
||||
this.file = file;
|
||||
const dynamicURL = new URL(this.webext.runtime.getURL(file));
|
||||
return new Promise(resolve => {
|
||||
const frame = document.createElement('iframe');
|
||||
const secretAttr = this.secretAttr;
|
||||
frame.setAttribute(secretAttr, '');
|
||||
const onLoad = ( ) => {
|
||||
frame.onload = null;
|
||||
frame.setAttribute(`${secretAttr}-loaded`, '');
|
||||
const channel = new MessageChannel();
|
||||
const port = channel.port1;
|
||||
port.onmessage = ev => {
|
||||
self.ubolOverlay &&
|
||||
self.ubolOverlay.onMessage(ev.data || {})
|
||||
};
|
||||
port.onmessageerror = ( ) => {
|
||||
self.ubolOverlay &&
|
||||
self.ubolOverlay.onMessage({ what: 'quitTool' })
|
||||
};
|
||||
const realURL = new URL(dynamicURL);
|
||||
realURL.hostname =
|
||||
self.ubolOverlay.webext.i18n.getMessage('@@extension_id');
|
||||
frame.contentWindow.postMessage(
|
||||
{
|
||||
what: 'startOverlay',
|
||||
url: document.baseURI,
|
||||
width: self.innerWidth,
|
||||
height: self.innerHeight,
|
||||
},
|
||||
realURL.origin,
|
||||
[ channel.port2 ]
|
||||
);
|
||||
frame.contentWindow.focus();
|
||||
self.ubolOverlay.onmessage = onmessage;
|
||||
self.ubolOverlay.port = port;
|
||||
self.ubolOverlay.frame = frame;
|
||||
resolve(true);
|
||||
};
|
||||
if ( dynamicURL.protocol !== 'safari-web-extension:' ) {
|
||||
frame.onload = ( ) => {
|
||||
frame.onload = onLoad;
|
||||
frame.contentWindow.location = dynamicURL.href;
|
||||
};
|
||||
} else {
|
||||
frame.onload = onLoad;
|
||||
frame.setAttribute('src', dynamicURL.href);
|
||||
}
|
||||
document.documentElement.append(frame);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
|
||||
void 0;
|
||||
61
platform/mv3/extension/js/scripting/unpicker.js
Normal file
61
platform/mv3/extension/js/scripting/unpicker.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
(async ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const ubolOverlay = self.ubolOverlay;
|
||||
if ( ubolOverlay === undefined ) { return; }
|
||||
if ( ubolOverlay.file === '/unpicker-ui.html' ) { return; }
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'injectCustomFilters':
|
||||
return ubolOverlay.sendMessage({ what: 'injectCustomFilters',
|
||||
hostname: ubolOverlay.url.hostname,
|
||||
});
|
||||
case 'uninjectCustomFilters':
|
||||
return ubolOverlay.sendMessage({ what: 'uninjectCustomFilters',
|
||||
hostname: ubolOverlay.url.hostname,
|
||||
});
|
||||
case 'removeCustomFilter':
|
||||
return ubolOverlay.sendMessage({ what: 'removeCustomFilter',
|
||||
hostname: ubolOverlay.url.hostname,
|
||||
selector: msg.selector,
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
await ubolOverlay.install('/unpicker-ui.html', onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
|
||||
void 0;
|
||||
|
|
@ -23,187 +23,9 @@
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const zapper = self.uBOLZapper = self.uBOLZapper || {};
|
||||
|
||||
if ( zapper.injected ) { return; }
|
||||
zapper.injected = true;
|
||||
|
||||
const webext = typeof browser === 'object' ? browser : chrome;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const sendMessage = msg => {
|
||||
try {
|
||||
webext.runtime.sendMessage(msg).catch(( ) => { });
|
||||
} catch {
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const zapperSecret = (( ) => {
|
||||
let secret = String.fromCharCode((Math.random() * 26) + 97);
|
||||
do {
|
||||
secret += (Math.floor(Math.random() * 2147483647) + 2147483647)
|
||||
.toString(36)
|
||||
.slice(2);
|
||||
} while ( secret.length < 8 );
|
||||
return secret;
|
||||
})();
|
||||
|
||||
const zapperCSSStyle = [
|
||||
'background: transparent',
|
||||
'border: 0',
|
||||
'border-radius: 0',
|
||||
'box-shadow: none',
|
||||
'color-scheme: light dark',
|
||||
'display: block',
|
||||
'filter: none',
|
||||
'height: 100vh',
|
||||
' height: 100svh',
|
||||
'left: 0',
|
||||
'margin: 0',
|
||||
'max-height: none',
|
||||
'max-width: none',
|
||||
'min-height: unset',
|
||||
'min-width: unset',
|
||||
'opacity: 1',
|
||||
'outline: 0',
|
||||
'padding: 0',
|
||||
'pointer-events: auto',
|
||||
'position: fixed',
|
||||
'top: 0',
|
||||
'transform: none',
|
||||
'visibility: hidden',
|
||||
'width: 100%',
|
||||
'z-index: 2147483647',
|
||||
''
|
||||
].join(' !important;\n');
|
||||
|
||||
const zapperCSS = `
|
||||
:root > [${zapperSecret}] {
|
||||
${zapperCSSStyle}
|
||||
}
|
||||
:root > [${zapperSecret}-loaded] {
|
||||
visibility: visible !important;
|
||||
}
|
||||
:root [${zapperSecret}-click] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
sendMessage({ what: 'insertCSS', css: zapperCSS });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const getElementBoundingClientRect = function(elem) {
|
||||
let rect = typeof elem.getBoundingClientRect === 'function'
|
||||
? elem.getBoundingClientRect()
|
||||
: { height: 0, left: 0, top: 0, width: 0 };
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/1024
|
||||
// Try not returning an empty bounding rect.
|
||||
if ( rect.width !== 0 && rect.height !== 0 ) {
|
||||
return rect;
|
||||
}
|
||||
if ( elem.shadowRoot instanceof DocumentFragment ) {
|
||||
return getElementBoundingClientRect(elem.shadowRoot);
|
||||
}
|
||||
|
||||
let left = rect.left,
|
||||
right = left + rect.width,
|
||||
top = rect.top,
|
||||
bottom = top + rect.height;
|
||||
|
||||
for ( const child of elem.children ) {
|
||||
rect = getElementBoundingClientRect(child);
|
||||
if ( rect.width === 0 || rect.height === 0 ) { continue; }
|
||||
if ( rect.left < left ) { left = rect.left; }
|
||||
if ( rect.right > right ) { right = rect.right; }
|
||||
if ( rect.top < top ) { top = rect.top; }
|
||||
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
|
||||
}
|
||||
|
||||
return {
|
||||
bottom,
|
||||
height: bottom - top,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
width: right - left
|
||||
};
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const highlightElement = function(elem) {
|
||||
if ( elem !== true ) {
|
||||
if ( elem !== undefined ) {
|
||||
if ( elem === highlightElement.current ) { return; }
|
||||
if ( elem !== zapperFrame ) {
|
||||
highlightElement.current = elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
elem = highlightElement.current;
|
||||
|
||||
const ow = self.innerWidth;
|
||||
const oh = self.innerHeight;
|
||||
const islands = [];
|
||||
|
||||
if ( elem !== null ) {
|
||||
const rect = getElementBoundingClientRect(elem);
|
||||
// Ignore offscreen areas
|
||||
if (
|
||||
rect.left <= ow && rect.top <= oh &&
|
||||
rect.left + rect.width >= 0 && rect.top + rect.height >= 0
|
||||
) {
|
||||
islands.push(
|
||||
`M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
zapperFramePort.postMessage({
|
||||
what: 'svgPaths',
|
||||
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
|
||||
islands: islands.join(''),
|
||||
});
|
||||
};
|
||||
highlightElement.current = null;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const elementFromPoint = (( ) => {
|
||||
let lastX, lastY;
|
||||
|
||||
return (x, y) => {
|
||||
if ( x !== undefined ) {
|
||||
lastX = x; lastY = y;
|
||||
} else if ( lastX !== undefined ) {
|
||||
x = lastX; y = lastY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if ( !zapperFrame ) { return null; }
|
||||
const magicAttr = `${zapperSecret}-click`;
|
||||
zapperFrame.setAttribute(magicAttr, '');
|
||||
let elem = document.elementFromPoint(x, y);
|
||||
if ( elem === document.body || elem === document.documentElement ) {
|
||||
elem = null;
|
||||
}
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
|
||||
zapperFrame.removeAttribute(magicAttr);
|
||||
return elem;
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const highlightElementAtPoint = function(mx, my) {
|
||||
const elem = elementFromPoint(mx, my);
|
||||
highlightElement(elem);
|
||||
};
|
||||
const ubolOverlay = self.ubolOverlay;
|
||||
if ( ubolOverlay === undefined ) { return; }
|
||||
if ( ubolOverlay.file === '/zapper-ui.html' ) { return; }
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
@ -213,18 +35,18 @@ const highlightElementAtPoint = function(mx, my) {
|
|||
// With touch-driven devices, first highlight the element and remove only
|
||||
// when tapping again the highlighted area.
|
||||
|
||||
const zapElementAtPoint = function(mx, my, options) {
|
||||
function zapElementAtPoint(mx, my, options) {
|
||||
if ( options.highlight ) {
|
||||
const elem = elementFromPoint(mx, my);
|
||||
const elem = ubolOverlay.elementFromPoint(mx, my);
|
||||
if ( elem ) {
|
||||
highlightElement(elem);
|
||||
ubolOverlay.highlightElements([ elem ]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let elemToRemove = highlightElement.current;
|
||||
let elemToRemove = ubolOverlay.highlightedElements?.[0] ?? null;
|
||||
if ( elemToRemove === null && mx !== undefined ) {
|
||||
elemToRemove = elementFromPoint(mx, my);
|
||||
elemToRemove = ubolOverlay.elementFromPoint(mx, my);
|
||||
}
|
||||
|
||||
if ( elemToRemove instanceof Element === false ) { return; }
|
||||
|
|
@ -261,86 +83,38 @@ const zapElementAtPoint = function(mx, my, options) {
|
|||
}
|
||||
}
|
||||
elemToRemove.remove();
|
||||
highlightElementAtPoint(mx, my);
|
||||
};
|
||||
ubolOverlay.highlightElementAtPoint(mx, my);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onKeyPressed = function(ev) {
|
||||
// Delete
|
||||
if (
|
||||
(ev.key === 'Delete' || ev.key === 'Backspace') ) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
zapElementAtPoint();
|
||||
return;
|
||||
}
|
||||
// Esc
|
||||
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
quitZapper();
|
||||
return;
|
||||
}
|
||||
};
|
||||
function onKeyPressed(ev) {
|
||||
if ( ev.key !== 'Delete' && ev.key !== 'Backspace' ) { return; }
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
zapElementAtPoint();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://github.com/chrisaljoudi/uBlock/issues/190
|
||||
// May need to dynamically adjust the height of the overlay + new position
|
||||
// of highlighted elements.
|
||||
|
||||
const onViewportChanged = function() {
|
||||
highlightElement(true);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startZapper = function() {
|
||||
zapperFrame.focus();
|
||||
self.addEventListener('scroll', onViewportChanged, { passive: true });
|
||||
self.addEventListener('resize', onViewportChanged, { passive: true });
|
||||
function startZapper() {
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const quitZapper = function() {
|
||||
self.removeEventListener('scroll', onViewportChanged, { passive: true });
|
||||
self.removeEventListener('resize', onViewportChanged, { passive: true });
|
||||
function quitZapper() {
|
||||
self.removeEventListener('keydown', onKeyPressed, true);
|
||||
if ( zapperFramePort ) {
|
||||
zapperFramePort.close();
|
||||
zapperFramePort.onmessage = null;
|
||||
zapperFramePort.onmessageerror = null;
|
||||
zapperFramePort = null;
|
||||
}
|
||||
if ( zapperFrame ) {
|
||||
zapperFrame.remove();
|
||||
zapperFrame = null;
|
||||
}
|
||||
sendMessage({ what: 'removeCSS', css: zapperCSS });
|
||||
zapper.injected = false;
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onFrameMessage = function(msg) {
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'start':
|
||||
case 'startTool':
|
||||
startZapper();
|
||||
if ( Boolean(zapperFramePort) === false ) { break; }
|
||||
highlightElement(true);
|
||||
break;
|
||||
case 'quitZapper':
|
||||
case 'quitTool':
|
||||
quitZapper();
|
||||
break;
|
||||
case 'highlightElementAtPoint':
|
||||
highlightElementAtPoint(msg.mx, msg.my);
|
||||
break;
|
||||
case 'unhighlight':
|
||||
highlightElement(null);
|
||||
break;
|
||||
case 'zapElementAtPoint':
|
||||
zapElementAtPoint(msg.mx, msg.my, msg.options);
|
||||
if ( msg.options.highlight !== true && msg.options.stay !== true ) {
|
||||
|
|
@ -350,68 +124,11 @@ const onFrameMessage = function(msg) {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// zapper-ui.html will be injected in the page through an iframe, and
|
||||
// is a sandboxed so as to prevent the page from interfering with its
|
||||
// content and behavior.
|
||||
//
|
||||
// The purpose of zapper.js is to install the zapper UI, and wait for the
|
||||
// component to establish a direct communication channel.
|
||||
//
|
||||
// When the zapper is installed on a page, the only change the page sees is an
|
||||
// iframe with a random attribute. The page can't see the content of the
|
||||
// iframe, and cannot interfere with its style properties. However the page
|
||||
// can remove the iframe.
|
||||
|
||||
const bootstrap = async ( ) => {
|
||||
const dynamicURL = new URL(webext.runtime.getURL('/zapper-ui.html'));
|
||||
return new Promise(resolve => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.setAttribute(zapperSecret, '');
|
||||
const onZapperLoad = ( ) => {
|
||||
frame.onload = null;
|
||||
frame.setAttribute(`${zapperSecret}-loaded`, '');
|
||||
const channel = new MessageChannel();
|
||||
const port = channel.port1;
|
||||
port.onmessage = ev => {
|
||||
onFrameMessage(ev.data || {});
|
||||
};
|
||||
port.onmessageerror = ( ) => {
|
||||
quitZapper();
|
||||
};
|
||||
const realURL = new URL(dynamicURL);
|
||||
realURL.hostname = webext.i18n.getMessage('@@extension_id');
|
||||
frame.contentWindow.postMessage(
|
||||
{ what: 'zapperStart' },
|
||||
realURL.origin,
|
||||
[ channel.port2 ]
|
||||
);
|
||||
frame.contentWindow.focus();
|
||||
resolve({
|
||||
zapperFrame: frame,
|
||||
zapperFramePort: port,
|
||||
});
|
||||
};
|
||||
if ( dynamicURL.protocol !== 'safari-web-extension:' ) {
|
||||
frame.onload = ( ) => {
|
||||
frame.onload = onZapperLoad;
|
||||
frame.contentWindow.location = dynamicURL.href;
|
||||
};
|
||||
} else {
|
||||
frame.onload = onZapperLoad;
|
||||
frame.setAttribute('src', dynamicURL.href);
|
||||
}
|
||||
document.documentElement.append(frame);
|
||||
});
|
||||
};
|
||||
|
||||
let { zapperFrame, zapperFramePort } = await bootstrap();
|
||||
if ( zapperFrame && zapperFramePort ) { return; }
|
||||
|
||||
quitZapper();
|
||||
await ubolOverlay.install('/zapper-ui.html', onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
|
|||
241
platform/mv3/extension/js/tool-overlay-ui.js
Normal file
241
platform/mv3/extension/js/tool-overlay-ui.js
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
import { dom, qs$ } from './dom.js';
|
||||
import { sendMessage } from './ext.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export const toolOverlay = {
|
||||
url: new URL('about:blank'),
|
||||
svgRoot: qs$('svg#overlay'),
|
||||
svgOcean: qs$('svg#overlay > path'),
|
||||
svgIslands: qs$('svg#overlay > path + path'),
|
||||
emptyPath: 'M0 0',
|
||||
port: null,
|
||||
|
||||
start(onmessage) {
|
||||
this.onmessage = onmessage;
|
||||
globalThis.addEventListener('message', ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what !== 'startOverlay' ) { return; }
|
||||
if ( Array.isArray(ev.ports) === false ) { return; }
|
||||
if ( ev.ports.length === 0 ) { return; }
|
||||
toolOverlay.port = ev.ports[0];
|
||||
toolOverlay.port.onmessage = ev => {
|
||||
this.onMessage(ev.data || {});
|
||||
};
|
||||
toolOverlay.port.onmessageerror = ( ) => {
|
||||
this.onmessage({ what: 'stopTool' });
|
||||
};
|
||||
this.moveable = qs$('aside:has(#move)');
|
||||
if ( this.moveable !== null ) {
|
||||
dom.on('aside #move', 'pointerdown', ev => {
|
||||
this.mover(ev);
|
||||
});
|
||||
}
|
||||
this.onMessage({ what: 'startTool',
|
||||
url: msg.url,
|
||||
width: msg.width,
|
||||
height: msg.height,
|
||||
});
|
||||
dom.cl.remove(dom.body, 'loading');
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
stop() {
|
||||
this.highlightElementUnderMouse(false);
|
||||
if ( this.port ) {
|
||||
this.port.postMessage({ what: 'quitTool' });
|
||||
this.port.onmessage = null;
|
||||
this.port.onmessageerror = null;
|
||||
this.port = null;
|
||||
}
|
||||
},
|
||||
|
||||
onMessage(wrapped) {
|
||||
// Response to frame-initiated message?
|
||||
if ( typeof wrapped?.fromFrameId === 'number' ) {
|
||||
const resolve = this.pendingMessages.get(wrapped.fromFrameId);
|
||||
if ( resolve ) {
|
||||
this.pendingMessages.delete(wrapped.fromFrameId);
|
||||
resolve(wrapped.msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const msg = wrapped.msg || wrapped;
|
||||
switch ( msg.what ) {
|
||||
case 'startTool': {
|
||||
this.url.href = msg.url;
|
||||
const ow = msg.width;
|
||||
const oh = msg.height;
|
||||
this.svgOcean.setAttribute('d', `M0 0h${ow}v${oh}h-${ow}z`);
|
||||
break;
|
||||
}
|
||||
case 'svgPaths':
|
||||
this.svgOcean.setAttribute('d', msg.ocean + msg.islands);
|
||||
this.svgIslands.setAttribute('d', msg.islands || this.emptyPath);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const response = this.onmessage && this.onmessage(msg) || undefined;
|
||||
// Send response if this is script-initiated message
|
||||
if ( wrapped?.fromScriptId && this.port ) {
|
||||
const { fromScriptId } = wrapped;
|
||||
if ( response instanceof Promise ) {
|
||||
response.then(response => {
|
||||
if ( this.port === null ) { return; }
|
||||
this.port.postMessage({ fromScriptId, msg: response });
|
||||
});
|
||||
} else {
|
||||
this.port.postMessage({ fromScriptId, msg: response });
|
||||
}
|
||||
}
|
||||
},
|
||||
postMessage(msg) {
|
||||
if ( this.port === null ) { return; }
|
||||
const wrapped = {
|
||||
fromFrameId: this.messageId++,
|
||||
msg,
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
this.pendingMessages.set(wrapped.fromFrameId, resolve);
|
||||
this.port.postMessage(wrapped);
|
||||
});
|
||||
},
|
||||
messageId: 1,
|
||||
pendingMessages: new Map(),
|
||||
|
||||
sendMessage(msg) {
|
||||
return sendMessage(msg);
|
||||
},
|
||||
|
||||
highlightElementUnderMouse(state) {
|
||||
if ( dom.cl.has(dom.root, 'mobile') ) { return; }
|
||||
if ( state === this.mstrackerOn ) { return; }
|
||||
this.mstrackerOn = state;
|
||||
if ( this.mstrackerOn ) {
|
||||
dom.on(document, 'mousemove', this.onHover, { passive: true });
|
||||
return;
|
||||
}
|
||||
dom.off(document, 'mousemove', this.onHover, { passive: true });
|
||||
if ( this.mstrackerTimer === undefined ) { return; }
|
||||
self.cancelAnimationFrame(this.mstrackerTimer);
|
||||
this.mstrackerTimer = undefined;
|
||||
},
|
||||
onTimer() {
|
||||
toolOverlay.mstrackerTimer = undefined;
|
||||
if ( toolOverlay.port === null ) { return; }
|
||||
toolOverlay.port.postMessage({
|
||||
what: 'highlightElementAtPoint',
|
||||
mx: toolOverlay.mstrackerX,
|
||||
my: toolOverlay.mstrackerY,
|
||||
});
|
||||
},
|
||||
onHover(ev) {
|
||||
toolOverlay.mstrackerX = ev.clientX;
|
||||
toolOverlay.mstrackerY = ev.clientY;
|
||||
if ( toolOverlay.mstrackerTimer !== undefined ) { return; }
|
||||
toolOverlay.mstrackerTimer =
|
||||
self.requestAnimationFrame(toolOverlay.onTimer);
|
||||
},
|
||||
mstrackerOn: false,
|
||||
mstrackerX: 0, mstrackerY: 0,
|
||||
mstrackerTimer: undefined,
|
||||
|
||||
mover(ev) {
|
||||
const target = ev.target;
|
||||
if ( target.matches('#move') === false ) { return; }
|
||||
if ( dom.cl.has(this.moveable, 'moving') ) { return; }
|
||||
target.setPointerCapture(ev.pointerId);
|
||||
this.moverIsTouch = ev.type.startsWith('touch');
|
||||
if ( this.moverIsTouch ) {
|
||||
const touch = ev.touches[0];
|
||||
this.moverX0 = touch.pageX;
|
||||
this.moverY0 = touch.pageY;
|
||||
} else {
|
||||
this.moverX0 = ev.pageX;
|
||||
this.moverY0 = ev.pageY;
|
||||
}
|
||||
const rect = this.moveable.getBoundingClientRect();
|
||||
this.moverCX0 = rect.x + rect.width / 2;
|
||||
this.moverCY0 = rect.y + rect.height / 2;
|
||||
dom.cl.add(this.moveable, 'moving');
|
||||
self.addEventListener('pointermove', this.moverMoveAsync, { capture: true });
|
||||
self.addEventListener('pointerup', this.moverStop, { capture: true, once: true });
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
},
|
||||
moverMove() {
|
||||
this.moverTimer = undefined;
|
||||
const cx1 = this.moverCX0 + this.moverX1 - this.moverX0;
|
||||
const cy1 = this.moverCY0 + this.moverY1 - this.moverY0;
|
||||
const rootW = dom.root.clientWidth;
|
||||
const rootH = dom.root.clientHeight;
|
||||
const moveableW = this.moveable.clientWidth;
|
||||
const moveableH = this.moveable.clientHeight;
|
||||
if ( cx1 < rootW / 2 ) {
|
||||
this.moveable.style.setProperty('left', `${Math.max(cx1-moveableW/2,2)}px`);
|
||||
this.moveable.style.removeProperty('right');
|
||||
} else {
|
||||
this.moveable.style.removeProperty('left');
|
||||
this.moveable.style.setProperty('right', `${Math.max(rootW-cx1-moveableW/2,2)}px`);
|
||||
}
|
||||
if ( cy1 < rootH / 2 ) {
|
||||
this.moveable.style.setProperty('top', `${Math.max(cy1-moveableH/2,2)}px`);
|
||||
this.moveable.style.removeProperty('bottom');
|
||||
} else {
|
||||
this.moveable.style.removeProperty('top');
|
||||
this.moveable.style.setProperty('bottom', `${Math.max(rootH-cy1-moveableH/2,2)}px`);
|
||||
}
|
||||
},
|
||||
moverMoveAsync(ev) {
|
||||
if ( toolOverlay.moverTimer !== undefined ) { return; }
|
||||
if ( toolOverlay.moverIsTouch ) {
|
||||
const touch = ev.touches[0];
|
||||
toolOverlay.moverX1 = touch.pageX;
|
||||
toolOverlay.moverY1 = touch.pageY;
|
||||
} else {
|
||||
toolOverlay.moverX1 = ev.pageX;
|
||||
toolOverlay.moverY1 = ev.pageY;
|
||||
}
|
||||
toolOverlay.moverTimer = self.requestAnimationFrame(( ) => {
|
||||
toolOverlay.moverMove();
|
||||
});
|
||||
},
|
||||
moverStop(ev) {
|
||||
if ( dom.cl.has(toolOverlay.moveable, 'moving') === false ) { return; }
|
||||
dom.cl.remove(toolOverlay.moveable, 'moving');
|
||||
self.removeEventListener('pointermove', toolOverlay.moverMoveAsync, { capture: true });
|
||||
ev.target.releasePointerCapture(ev.pointerId);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
},
|
||||
moveable: null,
|
||||
moverIsTouch: false,
|
||||
moverX0: 0, moverY0: 0,
|
||||
moverX1: 0, moverY1: 0,
|
||||
moverCX0: 0, moverCY0: 0,
|
||||
moverTimer: undefined,
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
164
platform/mv3/extension/js/unpicker-ui.js
Normal file
164
platform/mv3/extension/js/unpicker-ui.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a comprehensive, efficient content blocker
|
||||
Copyright (C) 2025-present Raymond Hill
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
import { dom, qs$, qsa$ } from './dom.js';
|
||||
import { faIconsInit } from './fa-icons.js';
|
||||
import { toolOverlay } from './tool-overlay-ui.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMinimizeClicked() {
|
||||
dom.cl.toggle(dom.root, 'minimized');
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function highlight() {
|
||||
const selectors = [];
|
||||
for ( const selectorElem of qsa$('#customFilters .customFilter.on > span.selector') ) {
|
||||
selectors.push(selectorElem.textContent);
|
||||
}
|
||||
if ( selectors.length !== 0 ) {
|
||||
toolOverlay.postMessage({
|
||||
what: 'highlightFromSelector',
|
||||
selector: selectors.join(','),
|
||||
scrollTo: true,
|
||||
});
|
||||
} else {
|
||||
toolOverlay.postMessage({ what: 'unhighlight' });
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onFilterClicked(ev) {
|
||||
const target = ev.target;
|
||||
const filterElem = target.closest('.customFilter');
|
||||
if ( filterElem === null ) { return; }
|
||||
const selectorElem = qs$(filterElem, ':scope > span.selector');
|
||||
if ( target === selectorElem ) {
|
||||
if ( dom.cl.has(filterElem, 'on') ) {
|
||||
dom.cl.remove(filterElem, 'on');
|
||||
} else {
|
||||
dom.cl.remove('.customFilter.on', 'on');
|
||||
dom.cl.add(filterElem, 'on');
|
||||
}
|
||||
highlight();
|
||||
return;
|
||||
}
|
||||
const selector = selectorElem.textContent;
|
||||
const trashElem = qs$(filterElem, ':scope > span.remove');
|
||||
if ( target === trashElem ) {
|
||||
dom.cl.add(filterElem, 'removed');
|
||||
dom.cl.remove(filterElem, 'on');
|
||||
toolOverlay.sendMessage({ what: 'removeCustomFilter',
|
||||
hostname: toolOverlay.url.hostname,
|
||||
selector,
|
||||
}).then(( ) => {
|
||||
autoSelectFilter();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const undoElem = qs$(filterElem, ':scope > span.undo');
|
||||
if ( target === undoElem ) {
|
||||
dom.cl.remove(filterElem, 'removed');
|
||||
toolOverlay.sendMessage({ what: 'addCustomFilter',
|
||||
hostname: toolOverlay.url.hostname,
|
||||
selector,
|
||||
}).then(( ) => {
|
||||
dom.cl.remove('.customFilter.on', 'on');
|
||||
dom.cl.add(filterElem, 'on');
|
||||
highlight();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function autoSelectFilter() {
|
||||
let filterElem = qs$('.customFilter.on');
|
||||
if ( filterElem !== null ) { return; }
|
||||
filterElem = qs$('.customFilter:not(.removed)');
|
||||
if ( filterElem !== null ) {
|
||||
dom.cl.add(filterElem, 'on');
|
||||
}
|
||||
highlight();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function populateFilters(selectors) {
|
||||
const container = qs$('#customFilters');
|
||||
dom.clear(container);
|
||||
const rowTemplate = qs$('template#customFilterRow');
|
||||
for ( const selector of selectors ) {
|
||||
const row = rowTemplate.content.cloneNode(true);
|
||||
qs$(row, '.customFilter > span.selector').textContent = selector;
|
||||
container.append(row);
|
||||
}
|
||||
faIconsInit(container);
|
||||
autoSelectFilter();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function startUnpicker() {
|
||||
const selectors = await toolOverlay.sendMessage({
|
||||
what: 'selectorsFromCustomFilters',
|
||||
hostname: toolOverlay.url.hostname,
|
||||
})
|
||||
if ( selectors.length === 0 ) {
|
||||
return quitUnpicker();
|
||||
}
|
||||
await toolOverlay.postMessage({ what: 'startTool' });
|
||||
await toolOverlay.postMessage({ what: 'uninjectCustomFilters' });
|
||||
populateFilters(selectors);
|
||||
dom.on('#minimize', 'click', onMinimizeClicked);
|
||||
dom.on('#customFilters', 'click', onFilterClicked);
|
||||
dom.on('#quit', 'click', quitUnpicker);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function quitUnpicker() {
|
||||
await toolOverlay.postMessage({ what: 'injectCustomFilters' });
|
||||
toolOverlay.stop();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'startTool':
|
||||
startUnpicker();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Wait for the content script to establish communication
|
||||
toolOverlay.start(onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -19,40 +19,26 @@
|
|||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const $id = id => document.getElementById(id);
|
||||
const $stor = selector => document.querySelector(selector);
|
||||
|
||||
const svgRoot = $stor('svg#sea');
|
||||
const svgOcean = svgRoot.children[0];
|
||||
const svgIslands = svgRoot.children[1];
|
||||
const NoPaths = 'M0 0';
|
||||
|
||||
let zapperScriptPort;
|
||||
import { dom } from './dom.js';
|
||||
import { toolOverlay } from './tool-overlay-ui.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onSvgClicked = function(ev) {
|
||||
function onSvgClicked(ev) {
|
||||
// If zap mode, highlight element under mouse, this makes the zapper usable
|
||||
// on touch screens.
|
||||
zapperScriptPort.postMessage({
|
||||
toolOverlay.postMessage({
|
||||
what: 'zapElementAtPoint',
|
||||
mx: ev.clientX,
|
||||
my: ev.clientY,
|
||||
options: {
|
||||
stay: true,
|
||||
highlight: ev.target !== svgIslands,
|
||||
highlight: ev.target !== toolOverlay.svgIslands,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
Swipe right:
|
||||
Remove current highlight
|
||||
|
||||
*/
|
||||
/******************************************************************************/
|
||||
|
||||
const onSvgTouch = (( ) => {
|
||||
let startX = 0, startY = 0;
|
||||
|
|
@ -67,7 +53,6 @@ const onSvgTouch = (( ) => {
|
|||
if ( startX === undefined ) { return; }
|
||||
const stopX = ev.changedTouches[0].screenX;
|
||||
const stopY = ev.changedTouches[0].screenY;
|
||||
const angle = Math.abs(Math.atan2(stopY - startY, stopX - startX));
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(stopX - startX, 2) +
|
||||
Math.pow(stopY - startY, 2)
|
||||
|
|
@ -76,77 +61,23 @@ const onSvgTouch = (( ) => {
|
|||
// - Swipe is not valid; and
|
||||
// - The time between start and stop was less than 200ms.
|
||||
const duration = ev.timeStamp - t0;
|
||||
if ( distance < 32 && duration < 200 ) {
|
||||
onSvgClicked({
|
||||
type: 'touch',
|
||||
target: ev.target,
|
||||
clientX: ev.changedTouches[0].pageX,
|
||||
clientY: ev.changedTouches[0].pageY,
|
||||
});
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
if ( distance < 64 ) { return; }
|
||||
const angleUpperBound = Math.PI * 0.25 * 0.5;
|
||||
const swipeRight = angle < angleUpperBound;
|
||||
if ( swipeRight === false ) { return; }
|
||||
if ( ev.cancelable ) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
// Swipe right.
|
||||
if ( svgIslands.getAttribute('d') === NoPaths ) { return; }
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'unhighlight'
|
||||
if ( distance >= 32 || duration >= 200 ) { return; }
|
||||
onSvgClicked({
|
||||
type: 'touch',
|
||||
target: ev.target,
|
||||
clientX: ev.changedTouches[0].pageX,
|
||||
clientY: ev.changedTouches[0].pageY,
|
||||
});
|
||||
ev.preventDefault();
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const svgListening = (( ) => {
|
||||
let on = false;
|
||||
let timer;
|
||||
let mx = 0, my = 0;
|
||||
|
||||
const onTimer = ( ) => {
|
||||
timer = undefined;
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'highlightElementAtPoint',
|
||||
mx,
|
||||
my,
|
||||
});
|
||||
};
|
||||
|
||||
const onHover = ev => {
|
||||
mx = ev.clientX;
|
||||
my = ev.clientY;
|
||||
if ( timer === undefined ) {
|
||||
timer = self.requestAnimationFrame(onTimer);
|
||||
}
|
||||
};
|
||||
|
||||
return state => {
|
||||
if ( $stor(':root.mobile') !== null ) { return; }
|
||||
if ( state === on ) { return; }
|
||||
on = state;
|
||||
if ( on ) {
|
||||
document.addEventListener('mousemove', onHover, { passive: true });
|
||||
return;
|
||||
}
|
||||
document.removeEventListener('mousemove', onHover, { passive: true });
|
||||
if ( timer !== undefined ) {
|
||||
self.cancelAnimationFrame(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onKeyPressed = function(ev) {
|
||||
function onKeyPressed(ev) {
|
||||
// Delete
|
||||
if ( ev.key === 'Delete' || ev.key === 'Backspace' ) {
|
||||
zapperScriptPort.postMessage({
|
||||
toolOverlay.postMessage({
|
||||
what: 'zapElementAtPoint',
|
||||
options: { stay: true },
|
||||
});
|
||||
|
|
@ -157,81 +88,45 @@ const onKeyPressed = function(ev) {
|
|||
quitZapper();
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onScriptMessage = function(msg) {
|
||||
function startZapper() {
|
||||
toolOverlay.postMessage({ what: 'startTool' });
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
dom.on('svg#overlay', 'click', onSvgClicked);
|
||||
dom.on('svg#overlay', 'touchstart', onSvgTouch, { passive: true });
|
||||
dom.on('svg#overlay', 'touchend', onSvgTouch);
|
||||
dom.on('#quit', 'click', quitZapper );
|
||||
dom.on('#pick', 'click', resetZapper );
|
||||
toolOverlay.highlightElementUnderMouse(true);
|
||||
}
|
||||
|
||||
function quitZapper() {
|
||||
self.removeEventListener('keydown', onKeyPressed, true);
|
||||
toolOverlay.stop();
|
||||
}
|
||||
|
||||
function resetZapper() {
|
||||
toolOverlay.postMessage({ what: 'unhighlight' });
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'svgPaths': {
|
||||
let { ocean, islands } = msg;
|
||||
ocean += islands;
|
||||
svgOcean.setAttribute('d', ocean);
|
||||
svgIslands.setAttribute('d', islands || NoPaths);
|
||||
case 'startTool':
|
||||
startZapper();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startZapper = function(port) {
|
||||
zapperScriptPort = port;
|
||||
zapperScriptPort.onmessage = ev => {
|
||||
onScriptMessage(ev.data || {});
|
||||
};
|
||||
zapperScriptPort.onmessageerror = ( ) => {
|
||||
quitZapper();
|
||||
};
|
||||
zapperScriptPort.postMessage({ what: 'start' });
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
$stor('svg#sea').addEventListener('click', onSvgClicked);
|
||||
$stor('svg#sea').addEventListener('touchstart', onSvgTouch, { passive: true });
|
||||
$stor('svg#sea').addEventListener('touchend', onSvgTouch);
|
||||
$id('quit').addEventListener('click', quitZapper );
|
||||
$id('pick').addEventListener('click', resetZapper );
|
||||
svgListening(true);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const quitZapper = function() {
|
||||
self.removeEventListener('keydown', onKeyPressed, true);
|
||||
$stor('svg#sea').removeEventListener('click', onSvgClicked);
|
||||
$stor('svg#sea').removeEventListener('touchstart', onSvgTouch, { passive: true });
|
||||
$stor('svg#sea').removeEventListener('touchend', onSvgTouch);
|
||||
$id('quit').removeEventListener('click', quitZapper );
|
||||
$id('pick').removeEventListener('click', resetZapper );
|
||||
svgListening(false);
|
||||
if ( zapperScriptPort ) {
|
||||
zapperScriptPort.postMessage({ what: 'quitZapper' });
|
||||
zapperScriptPort.close();
|
||||
zapperScriptPort.onmessage = null;
|
||||
zapperScriptPort.onmessageerror = null;
|
||||
zapperScriptPort = null;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const resetZapper = function() {
|
||||
zapperScriptPort.postMessage({
|
||||
what: 'unhighlight'
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Wait for the content script to establish communication
|
||||
|
||||
globalThis.addEventListener('message', ev => {
|
||||
const msg = ev.data || {};
|
||||
if ( msg.what !== 'zapperStart' ) { return; }
|
||||
if ( Array.isArray(ev.ports) === false ) { return; }
|
||||
if ( ev.ports.length === 0 ) { return; }
|
||||
startZapper(ev.ports[0]);
|
||||
}, { once: true });
|
||||
toolOverlay.start(onMessage);
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
|
|||
56
platform/mv3/extension/picker-ui.html
Normal file
56
platform/mv3/extension/picker-ui.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="ubol-picker" class="minimized" data-view="0">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>uBO Lite Zapper</title>
|
||||
<link rel="stylesheet" href="/css/default.css">
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<link rel="stylesheet" href="/css/fa-icons.css">
|
||||
<link rel="stylesheet" href="/css/tool-overlay-ui.css">
|
||||
<link rel="stylesheet" href="/css/picker-ui.css">
|
||||
</head>
|
||||
|
||||
<body class="loading">
|
||||
<aside style="right: 2px; bottom: 2px;">
|
||||
<section id="windowbar">
|
||||
<div id="minimize"><svg viewBox="0 0 64 64"><path d="M 16,48 H 48" /><rect x="16" y="16" height="32" width="32" /></svg></div>
|
||||
<div id="move"></div>
|
||||
<div id="quit" data-i18n-title="pickerQuit"><svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></svg></div>
|
||||
</section>
|
||||
<section data-view="2">
|
||||
<textarea spellcheck="false"></textarea>
|
||||
</section>
|
||||
<section class="resultsetWidgets" data-view="0">
|
||||
<span>
|
||||
<label for="slider" data-i18n="pickerSliderLabel">_</label>
|
||||
<span id="resultsetCount"></span>
|
||||
</span>
|
||||
<input id="slider" type="range" min="0" max="10" step="1">
|
||||
</section>
|
||||
<section id="toolbar" data-view="0">
|
||||
<div>
|
||||
<button id="pick" type="button" data-i18n="pickerPick">_<span class="hover"></span></button>
|
||||
<button id="preview" type="button" data-i18n="pickerPreview">_<span class="hover"></span></button>
|
||||
</div>
|
||||
<button id="create" type="button" class="preferred" disabled data-i18n="pickerCreate"><span class="hover"></span></button>
|
||||
</section>
|
||||
<section id="candidateFilters" data-view="1">
|
||||
<ul></ul>
|
||||
</section>
|
||||
<section id="moreOrLess">
|
||||
<span><span data-i18n="popupMoreButton"></span> <span class="fa-icon fa-icon-vflipped">angle-up</span><span class="hover"></span></span>
|
||||
<span><span class="fa-icon">angle-up</span> <span data-i18n="popupLessButton"></span><span class="hover"></span></span>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<svg id="overlay"><path d></path><path d="M0 0"></path></svg>
|
||||
|
||||
<script src="js/theme.js" type="module"></script>
|
||||
<script src="js/fa-icons.js" type="module"></script>
|
||||
<script src="js/i18n.js" type="module"></script>
|
||||
<script src="js/picker-ui.js" type="module"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -28,29 +28,38 @@
|
|||
<!-- -------- -->
|
||||
<div id="filteringModeText"><label data-i18n="popupFilteringModeLabel">_</label><br><span>_</span><span></span></div>
|
||||
<!-- -------- -->
|
||||
<div class="toolRibbon pageTools">
|
||||
<span id="gotoZapper" class="fa-icon tool enabled" data-i18n-title="zapperTipEnter">bolt<span class="caption" data-i18n="zapperTipEnter"></span></span>
|
||||
<span id="showMatchedRules" class="fa-icon tool" tabindex="0" title="Show matched rules">list-alt<span class="caption">Show matched rules</span></span>
|
||||
<span id="reportFilterIssue" class="fa-icon tool enabled" data-i18n-title="popupTipReport">comment-alt<span class="caption" data-i18n="popupTipReport"></span></span>
|
||||
<span id="gotoDashboard" class="fa-icon tool enabled" tabindex="0" data-i18n-title="popupTipDashboard">cogs<span class="caption" data-i18n="popupTipDashboard"></span></span>
|
||||
</div>
|
||||
<!-- -------- -->
|
||||
<div id="rulesetStats" data-section="a">
|
||||
</div>
|
||||
<hr data-section="a">
|
||||
<!-- -------- -->
|
||||
<div id="moreOrLess">
|
||||
<span id="moreButton">
|
||||
<span data-i18n="popupMoreButton">_</span> <span class="fa-icon">angle-up</span>
|
||||
<hr class="needHTTP">
|
||||
<div class="toolRibbon localTools needHTTP">
|
||||
<label>On this website</label>
|
||||
<span id="gotoZapper" class="tool enabled" tabindex="0">
|
||||
<span class="fa-icon">bolt</span>
|
||||
<span data-i18n="zapperTipEnter">_</span>
|
||||
</span>
|
||||
<span id="lessButton">
|
||||
<span class="fa-icon">angle-up</span> <span data-i18n="popupLessButton">_</span>
|
||||
<span id="gotoPicker" class="tool enabled" tabindex="0">
|
||||
<span class="fa-icon">eye-slash</span>
|
||||
<span data-i18n="pickerTipEnter">_</span>
|
||||
</span>
|
||||
<span id="gotoUnpicker" class="tool" tabindex="0">
|
||||
<span class="fa-icon">eye-open</span>
|
||||
<span data-i18n="unpickerTipEnter">_</span>
|
||||
</span>
|
||||
<span id="gotoReport" class="tool enabled" tabindex="0">
|
||||
<span class="fa-icon">comment-alt</span>
|
||||
<span data-i18n="popupTipReport">_</span>
|
||||
</span>
|
||||
<span id="gotoMatchedRules" class="tool" tabindex="0">
|
||||
<span class="fa-icon">list-alt</span>
|
||||
<span>Show matched rules</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- -------- -->
|
||||
<hr>
|
||||
<div class="toolRibbon globalTools">
|
||||
<span id="gotoDashboard" class="tool enabled" tabindex="0">
|
||||
<span class="fa-icon">cogs</span>
|
||||
<span data-i18n="popupTipDashboard">_</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="templates">
|
||||
<div class="rulesetDetails"><h1></h1><p data-section="b"></p></div>
|
||||
</div>
|
||||
|
||||
<script src="js/theme.js" type="module"></script>
|
||||
|
|
|
|||
40
platform/mv3/extension/unpicker-ui.html
Normal file
40
platform/mv3/extension/unpicker-ui.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="ubol-unpicker">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>uBO Lite Zapper</title>
|
||||
<link rel="stylesheet" href="/css/default.css">
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<link rel="stylesheet" href="/css/fa-icons.css">
|
||||
<link rel="stylesheet" href="/css/tool-overlay-ui.css">
|
||||
<link rel="stylesheet" href="/css/unpicker-ui.css">
|
||||
</head>
|
||||
|
||||
<body class="loading">
|
||||
<aside style="right: 2px; bottom: 2px;">
|
||||
<section id="windowbar">
|
||||
<div id="minimize"><svg viewBox="0 0 64 64"><path d="M 16,48 H 48" /><rect x="16" y="16" height="32" width="32" /></svg></div>
|
||||
<div id="move"></div>
|
||||
<div id="quit" data-i18n-title="pickerQuit"><svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></svg></div>
|
||||
</section>
|
||||
<section>
|
||||
<span data-i18n="unpickerUsage">_</span>
|
||||
</section>
|
||||
<section id="customFilters"></section>
|
||||
</aside>
|
||||
|
||||
<svg id="overlay"><path d></path><path d="M0 0"></path></svg>
|
||||
|
||||
<template id="customFilterRow">
|
||||
<div class="customFilter"><span class="selector"></span><span class="remove fa-icon">trash-o</span><span class="undo fa-icon">undo</span></div>
|
||||
</template>
|
||||
|
||||
<script src="js/theme.js" type="module"></script>
|
||||
<script src="js/fa-icons.js" type="module"></script>
|
||||
<script src="js/i18n.js" type="module"></script>
|
||||
<script src="js/unpicker-ui.js" type="module"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
<meta name="color-scheme" content="light dark">
|
||||
<title>uBO Lite Zapper</title>
|
||||
<link rel="stylesheet" href="/css/default.css">
|
||||
<link rel="stylesheet" href="/css/tool-overlay-ui.css">
|
||||
<link rel="stylesheet" href="/css/zapper-ui.css">
|
||||
</head>
|
||||
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
</svg>
|
||||
</div>
|
||||
</aside>
|
||||
<svg id="sea"><path d></path><path d></path></svg>
|
||||
<svg id="overlay"><path d></path><path d="M0 0"></path></svg>
|
||||
|
||||
<script src="js/theme.js" type="module"></script>
|
||||
<script src="js/i18n.js" type="module"></script>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,22 @@
|
|||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/picker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"/unpicker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,22 @@
|
|||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"picker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resources": [
|
||||
"unpicker-ui.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -453,7 +453,7 @@
|
|||
--popup-cell-block-surface: rgb(var(--popup-cell-block-surface-rgb));
|
||||
--popup-power-ink: rgb(var(--popup-power-ink-rgb));
|
||||
--popup-toolbar-surface: rgb(var(--primary-80) / 15%);
|
||||
--popup-toolbar-surface-hover: rgb(var(--primary-80) / 20%);
|
||||
--popup-toolbar-surface-hover: rgb(var(--primary-80) / 30%);
|
||||
--popup-ruleset-tool-ink: var(--ink-1);
|
||||
--popup-ruleset-tool-surface: rgb(var(--primary-80) / 15%);
|
||||
--popup-ruleset-tool-surface-hover: rgb(var(--primary-80) / 20%);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<body>
|
||||
<aside style="right: 2px; bottom: 2px;">
|
||||
<div id="windowbar"><div id="minimize"><svg viewBox="0 0 64 64"><path d="M 16,48 H 48" /><rect x="16" y="16" height="32" width="32" /></div><div id="move"></div><div id="quit" data-i18n-title="pickerQuit"><svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></div></div>
|
||||
<div id="windowbar"><div id="minimize"><svg viewBox="0 0 64 64"><path d="M 16,48 H 48" /><rect x="16" y="16" height="32" width="32" /></svg></div><div id="move"></div><div id="quit" data-i18n-title="pickerQuit"><svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></svg></div></div>
|
||||
<section>
|
||||
<div>
|
||||
<div class="codeMirrorContainer codeMirrorBreakAll cm-theme-override"></div>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ else
|
|||
fi
|
||||
|
||||
echo "*** uBOLite.mv3: Copying common files"
|
||||
cp -R "$UBO_DIR"/src/css/fonts/* "$UBOL_DIR"/css/fonts/
|
||||
cp -R "$UBO_DIR"/src/css/fonts/Inter "$UBOL_DIR"/css/fonts/
|
||||
cp "$UBO_DIR"/src/css/themes/default.css "$UBOL_DIR"/css/
|
||||
cp "$UBO_DIR"/src/css/common.css "$UBOL_DIR"/css/
|
||||
cp "$UBO_DIR"/src/css/dashboard-common.css "$UBOL_DIR"/css/
|
||||
|
|
|
|||
Loading…
Reference in a new issue