diff --git a/platform/mv3/extension/css/dashboard.css b/platform/mv3/extension/css/dashboard.css index 6506c2272..454a79987 100644 --- a/platform/mv3/extension/css/dashboard.css +++ b/platform/mv3/extension/css/dashboard.css @@ -33,6 +33,7 @@ } body[data-pane="settings"] #dashboard-nav .tabButton[data-pane="settings"], +body[data-pane="rulesets"] #dashboard-nav .tabButton[data-pane="rulesets"], body[data-pane="about"] #dashboard-nav .tabButton[data-pane="about"] { background-color: var(--dashboard-tab-active-surface); border-bottom: 3px solid var(--dashboard-tab-active-ink); @@ -44,6 +45,7 @@ body > section { display: none; } body[data-pane="settings"] > section[data-pane="settings"], +body[data-pane="rulesets"] > section[data-pane="rulesets"], body[data-pane="about"] > section[data-pane="about"] { display: block; } diff --git a/platform/mv3/extension/css/settings.css b/platform/mv3/extension/css/settings.css index 3de13560c..da6e13a1a 100644 --- a/platform/mv3/extension/css/settings.css +++ b/platform/mv3/extension/css/settings.css @@ -79,92 +79,101 @@ h3[data-i18n="filteringMode0Name"]::first-letter { } #lists { - margin: 0.5em 0 0 0; - padding: 0; + padding-block-end: 8rem; } -.groupEntry:not([data-groupkey="user"]) .geDetails::before { - color: var(--ink-3); - content: '\2212'; - font-family: monospace; - font-size: large; - margin-inline-end: 0.25em; - -webkit-margin-end: 0.25em; +.listEntry { + display: flex; + flex-direction: column; } -.groupEntry.hideUnused:not([data-groupkey="user"]) .geDetails::before { - content: '+'; +.listEntry[data-nodeid] > .detailbar .listExpander { + cursor: pointer; + top: 2px; } -.groupEntry { - margin: 0.5em 0; - } -.groupEntry .geDetails { +.listEntry[data-role="rootnode"] > .detailbar, +.listEntry[data-nodeid] > .detailbar .count { cursor: pointer; } -.groupEntry .geName { +.listEntry[data-role="rootnode"] > .detailbar > *:not(.listExpander) { pointer-events: none; } -.groupEntry .geCount { +.listEntry .detailbar .count { + align-self: flex-end; color: var(--ink-3); - font-size: 90%; + font-size: small; pointer-events: none; } .listEntries { - margin-inline-start: 0.6em; - -webkit-margin-start: 0.6em; + display: flex; + flex-direction: column; } -.groupEntry:not([data-groupkey="user"]) .listEntry:not(.isDefault).unused { +.listEntry:not([data-role="rootnode"]) > .listEntries { + margin-inline-start: var(--checkbox-size); + } +.listEntry.hideUnused > .listEntries > .listEntry:not(.isDefault):has(> .detailbar input:not(:checked)) { display: none; } - .listEntry.fromAdmin:has(input[disabled]:not(:checked)) { display: none; } .listEntry > * { - margin-left: 0; - margin-right: 0; unicode-bidi: embed; } +.listEntry h3 { + display: inline-block; + margin: 0; + } +.listEntry > .detailbar { + align-items: center; + display: inline-flex; + margin: calc(var(--default-gap-xsmall) / 2 + var(--default-gap-xxsmall) / 2) 0; + white-space: nowrap; + } +.listEntry > .detailbar > *:not(:first-child) { + margin-inline-start: var(--default-gap-xxsmall); + } +.listEntry[data-nodeid="default"] > .detailbar > .listExpander { + display: none; + } +.listEntry > .detailbar > .listExpander svg { + transform: rotate(180deg); + transform-origin: 50%; + } +.listEntry.hideUnused > .detailbar > .listExpander svg { + transform: rotate(90deg); + } .listEntry .checkbox:has(input[disabled]), .listEntry .checkbox:has(input[disabled]) ~ span { filter: var(--checkbox-disabled-filter); } -.listEntry .listname { - white-space: nowrap; - } .listEntry a, .listEntry .fa-icon { color: var(--info0-ink); fill: var(--info0-ink); - display: none; font-size: 120%; - margin: 0 0.2em 0 0; } .listEntry .fa-icon:hover { transform: scale(1.25); } -.listEntry .content { - display: inline-flex; - } -.listEntry a.towiki { - display: inline-flex; - } -.listEntry.support a.support { - display: inline-flex; - } -.listEntry.mustread a.mustread { - color: var(--info1-ink); - fill: var(--info1-ink); - display: inline-flex; - } -.listEntry .status { - cursor: default; +.listEntry .iconbar a.support[href="#"] { display: none; -} + } -body.noMoreRuleset .listEntry:not(.checked) { +body.noMoreRuleset .listEntry:has(> .detailbar input:not(:checked)) { opacity: 0.5; pointer-events: none; } +#lists.searchMode > .listEntries .listEntries, +#lists.searchMode > .listEntries .listEntry.searchMatch { + display: flex !important; + } +#lists.searchMode > .listEntries .listEntry { + display: none; + } +#lists.searchMode > .listEntries .listExpander { + visibility: hidden; + } + /* touch-screen devices */ :root.mobile .listEntry .fa-icon { font-size: 120%; diff --git a/platform/mv3/extension/dashboard.html b/platform/mv3/extension/dashboard.html index 1cf758205..73dd72ac0 100644 --- a/platform/mv3/extension/dashboard.html +++ b/platform/mv3/extension/dashboard.html @@ -21,6 +21,7 @@
@@ -95,29 +96,14 @@

- + + +
-

-
-

-
-
-
-
-
- -
-
-
-
-
- +

+
search
+
@@ -146,6 +132,32 @@
+
+
+
+ + + home + + +
+ + +
diff --git a/platform/mv3/extension/js/filter-lists.js b/platform/mv3/extension/js/filter-lists.js new file mode 100644 index 000000000..a52dcb433 --- /dev/null +++ b/platform/mv3/extension/js/filter-lists.js @@ -0,0 +1,402 @@ +/******************************************************************************* + + uBlock Origin Lite - a comprehensive, MV3-compliant content blocker + Copyright (C) 2014-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 { i18n, i18n$ } from './i18n.js'; +import { localRead, localWrite, sendMessage } from './ext.js'; + +/******************************************************************************/ + +export const rulesetMap = new Map(); + +let cachedRulesetData = {}; +let hideUnusedSet = new Set([ 'regions' ]); + +/******************************************************************************/ + +function renderNumber(value) { + return value.toLocaleString(); +} + +function renderRuleCounts() { + let rulesetCount = 0; + let filterCount = 0; + let ruleCount = 0; + for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"][data-rulesetid]') ) { + if ( qs$(liEntry, 'input[type="checkbox"]:checked') === null ) { continue; } + rulesetCount += 1; + const stats = rulesetStats(liEntry.dataset.rulesetid); + if ( stats === undefined ) { continue; } + ruleCount += stats.ruleCount; + filterCount += stats.filterCount; + } + dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats') + .replace('{{ruleCount}}', ruleCount.toLocaleString()) + .replace('{{filterCount}}', filterCount.toLocaleString()) + ); + + dom.cl.toggle(dom.body, 'noMoreRuleset', + rulesetCount === cachedRulesetData.maxNumberOfEnabledRulesets + ); +} + +/******************************************************************************/ + +function updateNodes(listEntries) { + listEntries = listEntries || qs$('#lists'); + for ( const listEntry of qsa$(listEntries, '.listEntry[data-nodeid]') ) { + const totalCount = qsa$(listEntry, '.listEntry[data-rulesetid] input').length; + const checkedCount = qsa$(listEntry, '.listEntry[data-rulesetid] input:checked').length; + dom.text(qs$(listEntry, '.detailbar .count'), `${checkedCount}/${totalCount}`); + const checkbox = qs$(listEntry, ':scope > .detailbar .checkbox'); + if ( checkbox === null ) { continue; } + dom.prop(qs$(checkbox, 'input'), 'checked', checkedCount !== 0); + dom.cl.toggle(checkbox, 'partial', + checkedCount !== 0 && checkedCount !== totalCount + ); + } +} + +/******************************************************************************/ + +function rulesetStats(rulesetId) { + const hasOmnipotence = cachedRulesetData.defaultFilteringMode > 1; + const rulesetDetails = rulesetMap.get(rulesetId); + if ( rulesetDetails === undefined ) { return; } + const { rules, filters } = rulesetDetails; + let ruleCount = rules.plain + rules.regex; + if ( hasOmnipotence ) { + ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders; + } + const filterCount = filters.accepted; + return { ruleCount, filterCount }; +} + +/******************************************************************************/ + +function isAdminRuleset(listkey) { + const { adminRulesets = [] } = cachedRulesetData; + for ( const id of adminRulesets ) { + const pos = id.indexOf(listkey); + if ( pos === 0 ) { return true; } + if ( pos !== 1 ) { continue; } + const c = id.charAt(0); + if ( c === '+' || c === '-' ) { return true; } + } + return false; +} + +/******************************************************************************/ + +export function renderFilterLists(rulesetData) { + cachedRulesetData = rulesetData; + const { enabledRulesets, rulesetDetails } = cachedRulesetData; + + const shouldUpdate = rulesetMap.size !== 0; + + rulesetDetails.forEach(rule => rulesetMap.set(rule.id, rule)); + + const listStatsTemplate = i18n$('perRulesetStats'); + + const initializeListEntry = (ruleset, listEntry) => { + const on = enabledRulesets.includes(ruleset.id); + dom.prop(qs$(listEntry, ':scope > .detailbar input'), 'checked', on); + if ( ruleset.homeURL ) { + dom.attr(qs$(listEntry, 'a.support'), 'href', ruleset.homeURL); + } + dom.cl.toggle(listEntry, 'isDefault', ruleset.id === 'default'); + const stats = rulesetStats(ruleset.id); + listEntry.title = listStatsTemplate + .replace('{{ruleCount}}', renderNumber(stats.ruleCount)) + .replace('{{filterCount}}', renderNumber(stats.filterCount)); + const fromAdmin = isAdminRuleset(ruleset.id); + dom.cl.toggle(listEntry, 'fromAdmin', fromAdmin); + const disabled = stats.ruleCount === 0 || fromAdmin; + dom.attr( + qs$(listEntry, '.input.checkbox input'), + 'disabled', + disabled ? '' : null + ); + return listEntry; + }; + + // Update already rendered DOM lists + if ( shouldUpdate ) { + for ( const listEntry of qsa$('#lists .listEntry[data-rulesetid]') ) { + const rulesetid = listEntry.dataset.rulesetid; + const ruleset = rulesetMap.get(rulesetid); + initializeListEntry(ruleset, listEntry); + } + updateNodes(); + renderRuleCounts(); + return; + } + + const createListEntry = (listDetails, depth) => { + if ( listDetails.lists === undefined ) { + return dom.clone('#templates .listEntry[data-role="leaf"]'); + } + if ( depth !== 0 ) { + return dom.clone('#templates .listEntry[data-role="node"]'); + } + return dom.clone('#templates .listEntry[data-role="rootnode"]'); + }; + + const createListEntries = (parentkey, listTree, depth = 0) => { + const listEntries = dom.clone('#templates .listEntries'); + const treeEntries = Object.entries(listTree); + if ( depth !== 0 ) { + const reEmojis = /\p{Emoji}+/gu; + treeEntries.sort((a ,b) => { + const ap = a[1].preferred === true; + const bp = b[1].preferred === true; + if ( ap !== bp ) { return ap ? -1 : 1; } + const as = (a[1].title || a[0]).replace(reEmojis, ''); + const bs = (b[1].title || b[0]).replace(reEmojis, ''); + return as.localeCompare(bs); + }); + } + for ( const [ listkey, listDetails ] of treeEntries ) { + const listEntry = createListEntry(listDetails, depth); + if ( listDetails.lists === undefined ) { + listEntry.dataset.rulesetid = listkey; + } else { + listEntry.dataset.nodeid = listkey; + dom.cl.toggle(listEntry, 'hideUnused', hideUnusedSet.has(listkey)); + } + qs$(listEntry, ':scope > .detailbar .listname').append( + i18n.patchUnicodeFlags(listDetails.name) + ); + if ( listDetails.lists !== undefined ) { + listEntry.append(createListEntries(listkey, listDetails.lists, depth+1)); + dom.cl.toggle(listEntry, 'expanded', true/*listIsExpanded(listkey)*/); + //updateListNode(listEntry); + } else { + initializeListEntry(listDetails, listEntry); + } + listEntries.append(listEntry); + } + return listEntries; + }; + + // Visually split the filter lists in groups + const groups = new Map([ + [ + 'default', + rulesetDetails.filter(ruleset => + ruleset.id === 'default' + ), + ], [ + 'annoyances', + rulesetDetails.filter(ruleset => + ruleset.group === 'annoyances' + ), + ], [ + 'misc', + rulesetDetails.filter(ruleset => + ruleset.id !== 'default' && + ruleset.group === undefined && + typeof ruleset.lang !== 'string' + ), + ], [ + 'regions', + rulesetDetails.filter(ruleset => + ruleset.group === 'regions' + ), + ], + ]); + + dom.cl.toggle(dom.body, 'hideUnused', mustHideUnusedLists('*')); + + // Build list tree + const listTree = {}; + const groupNames = new Map(); + for ( const [ groupKey, groupRulesets ] of groups ) { + let groupName = groupNames.get(groupKey); + if ( groupName === undefined ) { + groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1)); + groupNames.set(groupKey, groupName); + } + const groupDetails = { + name: groupName, + lists: {}, + }; + listTree[groupKey] = groupDetails; + for ( const ruleset of groupRulesets ) { + if ( ruleset.parent !== undefined ) { + let lists = groupDetails.lists; + for ( const parent of ruleset.parent.split('|') ) { + if ( lists[parent] === undefined ) { + lists[parent] = { name: parent, lists: {} }; + } + lists = lists[parent].lists; + } + lists[ruleset.id] = ruleset; + } else { + groupDetails.lists[ruleset.id] = ruleset; + } + } + } + // Move lonely sublist to list level + const promoteLonelySublist = (parent, depth = 0) => { + if ( Boolean(parent.lists) === false ) { return parent; } + const childKeys = Object.keys(parent.lists); + for ( const childKey of childKeys ) { + const child = promoteLonelySublist(parent.lists[childKey], depth + 1); + if ( child === parent.lists[childKey] ) { continue; } + parent.lists[child.id] = child; + delete parent.lists[childKey]; + } + if ( depth === 0 ) { return parent; } + if ( childKeys.length > 1 ) { return parent; } + return parent.lists[childKeys[0]] + }; + for ( const key of Object.keys(listTree) ) { + promoteLonelySublist(listTree[key]); + } + const listEntries = createListEntries('root', listTree); + + updateNodes(listEntries); + + dom.clear('#lists'); + qs$('#lists').append(listEntries); + + renderRuleCounts(); +} + +/******************************************************************************/ + +// Collapsing of unused lists. + +function mustHideUnusedLists(which) { + const hideAll = hideUnusedSet.has('*'); + if ( which === '*' ) { return hideAll; } + return hideUnusedSet.has(which) !== hideAll; +} + +function toggleHideUnusedLists(which) { + const doesHideAll = hideUnusedSet.has('*'); + if ( which === '*' ) { + const mustHide = doesHideAll === false; + hideUnusedSet.clear(); + if ( mustHide ) { + hideUnusedSet.add(which); + } + dom.cl.toggle('#lists', 'hideUnused', mustHide); + dom.cl.toggle('.listEntry[data-nodeid]', 'hideUnused', mustHide); + } else { + const doesHide = hideUnusedSet.has(which); + if ( doesHide ) { + hideUnusedSet.delete(which); + } else { + hideUnusedSet.add(which); + } + const mustHide = doesHide === doesHideAll; + const groupSelector = `.listEntry[data-nodeid="${which}"]`; + dom.cl.toggle(groupSelector, 'hideUnused', mustHide); + } + + localWrite('hideUnusedFilterLists', Array.from(hideUnusedSet)); +} + +dom.on('#lists', 'click', '.listEntry[data-nodeid] > .detailbar, .listExpander', ev => { + toggleHideUnusedLists( + dom.attr(ev.target.closest('[data-nodeid]'), 'data-nodeid') + ); +}); + +// Initialize from saved state. +localRead('hideUnusedFilterLists').then(value => { + if ( Array.isArray(value) === false ) { return; } + hideUnusedSet = new Set(value); + for ( const listEntry of qsa$('[data-nodeid]') ) { + dom.cl.toggle(listEntry, 'hideUnused', + hideUnusedSet.has(listEntry.dataset.nodeid) + ); + } +}); + +/******************************************************************************/ + +const searchFilterLists = ( ) => { + const pattern = dom.prop('.searchfield input', 'value') || ''; + dom.cl.toggle('#lists', 'searchMode', pattern !== ''); + if ( pattern === '' ) { return; } + const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + for ( const listEntry of qsa$('#lists [data-role="leaf"]') ) { + const rulesetid = listEntry.dataset.rulesetid; + const rulesetDetails = rulesetMap.get(rulesetid); + if ( rulesetDetails === undefined ) { continue; } + let haystack = perListHaystack.get(rulesetDetails); + if ( haystack === undefined ) { + haystack = [ + rulesetDetails.name, + listEntry.dataset.nodeid, + rulesetDetails.tags || '', + ].join(' ').trim(); + perListHaystack.set(rulesetDetails, haystack); + } + dom.cl.toggle(listEntry, 'searchMatch', re.test(haystack)); + } + for ( const listEntry of qsa$('#lists .listEntry:not([data-role="leaf"])') ) { + dom.cl.toggle(listEntry, 'searchMatch', + qs$(listEntry, '.listEntries .listEntry.searchMatch') !== null + ); + } +}; + +const perListHaystack = new WeakMap(); + +dom.on('.searchfield input', 'input', searchFilterLists); + +/******************************************************************************/ + +async function applyEnabledRulesets() { + const enabledRulesets = []; + for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"][data-rulesetid]') ) { + const checked = qs$(liEntry, 'input[type="checkbox"]:checked') !== null; + if ( checked === false ) { continue; } + const { rulesetid } = liEntry.dataset; + if ( dom.cl.has(liEntry, 'fromAdmin') ) { continue; } + enabledRulesets.push(rulesetid); + } + + await sendMessage({ + what: 'applyRulesets', + enabledRulesets, + }); +} + +dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', ev => { + const input = ev.target; + const listEntry = input.closest('.listEntry'); + if ( listEntry === null ) { return; } + if ( listEntry.dataset.nodeid !== undefined ) { + let checkAll = input.checked || + dom.cl.has(qs$(listEntry, ':scope > .detailbar .checkbox'), 'partial'); + for ( const input of qsa$(listEntry, '.listEntries input') ) { + input.checked = checkAll; + } + } + renderRuleCounts(); + updateNodes(); + applyEnabledRulesets(); +}); diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js index d19a9f28e..f7d811e7d 100644 --- a/platform/mv3/extension/js/ruleset-manager.js +++ b/platform/mv3/extension/js/ruleset-manager.js @@ -101,9 +101,14 @@ async function pruneInvalidRegexRules(realm, rulesIn) { toCheck.push(true); continue; } + if ( pruneInvalidRegexRules.invalidRegexes.has(regex) ) { + toCheck.push(false); + continue; + } toCheck.push( dnr.isRegexSupported({ regex, isCaseSensitive }).then(result => { if ( result.isSupported ) { return true; } + pruneInvalidRegexRules.invalidRegexes.add(regex); rejectedRegexRules.push(`\t${regex} ${result.reason}`); return false; }) @@ -122,6 +127,7 @@ async function pruneInvalidRegexRules(realm, rulesIn) { return rulesIn.filter((v, i) => isValid[i]); } +pruneInvalidRegexRules.invalidRegexes = new Set(); /******************************************************************************/ diff --git a/platform/mv3/extension/js/settings.js b/platform/mv3/extension/js/settings.js index a970430f3..81f55d03c 100644 --- a/platform/mv3/extension/js/settings.js +++ b/platform/mv3/extension/js/settings.js @@ -19,201 +19,21 @@ Home: https://github.com/gorhill/uBlock */ -import { browser, localRead, localWrite, sendMessage } from './ext.js'; -import { dom, qs$, qsa$ } from './dom.js'; -import { i18n, i18n$ } from './i18n.js'; +import { browser, sendMessage } from './ext.js'; +import { dom, qs$ } from './dom.js'; import punycode from './punycode.js'; +import { renderFilterLists } from './filter-lists.js'; /******************************************************************************/ -const rulesetMap = new Map(); let cachedRulesetData = {}; -let hideUnusedSet = new Set([ 'regions' ]); /******************************************************************************/ -function renderNumber(value) { - return value.toLocaleString(); -} - function hashFromIterable(iter) { return Array.from(iter).sort().join('\n'); } -function isAdminRuleset(listkey) { - const { adminRulesets = [] } = cachedRulesetData; - for ( const id of adminRulesets ) { - const pos = id.indexOf(listkey); - if ( pos === 0 ) { return true; } - if ( pos !== 1 ) { continue; } - const c = id.charAt(0); - if ( c === '+' || c === '-' ) { return true; } - } - return false; -} - -/******************************************************************************/ - -function rulesetStats(rulesetId) { - const hasOmnipotence = cachedRulesetData.defaultFilteringMode > 1; - const rulesetDetails = rulesetMap.get(rulesetId); - if ( rulesetDetails === undefined ) { return; } - const { rules, filters } = rulesetDetails; - let ruleCount = rules.plain + rules.regex; - if ( hasOmnipotence ) { - ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders; - } - const filterCount = filters.accepted; - return { ruleCount, filterCount }; -} - -/******************************************************************************/ - -function renderFilterLists() { - const { enabledRulesets, rulesetDetails } = cachedRulesetData; - const listGroupTemplate = qs$('#templates .groupEntry'); - const listEntryTemplate = qs$('#templates .listEntry'); - const listStatsTemplate = i18n$('perRulesetStats'); - const groupNames = new Map([ [ 'user', '' ] ]); - - const liFromListEntry = function(ruleset, li, hideUnused) { - if ( !li ) { - li = dom.clone(listEntryTemplate); - } - const on = enabledRulesets.includes(ruleset.id); - dom.cl.toggle(li, 'checked', on); - dom.cl.toggle(li, 'unused', hideUnused && !on); - qs$(li, 'input[type="checkbox"]').checked = on; - if ( dom.attr(li, 'data-listkey') !== ruleset.id ) { - dom.attr(li, 'data-listkey', ruleset.id); - qs$(li, '.listname').append(i18n.patchUnicodeFlags(ruleset.name)); - dom.cl.remove(li, 'toRemove'); - if ( ruleset.homeURL ) { - dom.cl.add(li, 'support'); - dom.attr(qs$(li, 'a.support'), 'href', ruleset.homeURL); - } else { - dom.cl.remove(li, 'support'); - } - if ( ruleset.instructionURL ) { - dom.cl.add(li, 'mustread'); - dom.attr(qs$(li, 'a.mustread'), 'href', ruleset.instructionURL); - } else { - dom.cl.remove(li, 'mustread'); - } - dom.cl.toggle(li, 'isDefault', ruleset.id === 'default'); - } - const stats = rulesetStats(ruleset.id); - li.title = listStatsTemplate - .replace('{{ruleCount}}', renderNumber(stats.ruleCount)) - .replace('{{filterCount}}', renderNumber(stats.filterCount)); - const fromAdmin = isAdminRuleset(ruleset.id); - dom.cl.toggle(li, 'fromAdmin', fromAdmin); - const disabled = stats.ruleCount === 0 || fromAdmin; - dom.attr( - qs$(li, '.input.checkbox input'), - 'disabled', - disabled ? '' : null - ); - dom.cl.remove(li, 'discard'); - return li; - }; - - const listEntryCountFromGroup = function(groupRulesets) { - if ( Array.isArray(groupRulesets) === false ) { return ''; } - let count = 0, - total = 0; - for ( const ruleset of groupRulesets ) { - if ( enabledRulesets.includes(ruleset.id) ) { - count += 1; - } - total += 1; - } - return total !== 0 ? - `(${count.toLocaleString()}/${total.toLocaleString()})` : - ''; - }; - - const liFromListGroup = function(groupKey, groupRulesets) { - let liGroup = qs$(`#lists > .groupEntry[data-groupkey="${groupKey}"]`); - if ( liGroup === null ) { - liGroup = dom.clone(listGroupTemplate); - let groupName = groupNames.get(groupKey); - if ( groupName === undefined ) { - groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1)); - groupNames.set(groupKey, groupName); - } - if ( groupName !== '' ) { - dom.text(qs$(liGroup, '.geName'), groupName); - } - } - if ( qs$(liGroup, '.geName:empty') === null ) { - dom.text( - qs$(liGroup, '.geCount'), - listEntryCountFromGroup(groupRulesets) - ); - } - const hideUnused = mustHideUnusedLists(groupKey); - dom.cl.toggle(liGroup, 'hideUnused', hideUnused); - const ulGroup = qs$(liGroup, '.listEntries'); - if ( !groupRulesets ) { return liGroup; } - groupRulesets.sort(function(a, b) { - return (a.name || '').localeCompare(b.name || ''); - }); - for ( let i = 0; i < groupRulesets.length; i++ ) { - const liEntry = liFromListEntry( - groupRulesets[i], - ulGroup.children[i], - hideUnused - ); - if ( liEntry.parentElement === null ) { - ulGroup.appendChild(liEntry); - } - } - return liGroup; - }; - - // Visually split the filter lists in groups - const ulLists = qs$('#lists'); - const groups = new Map([ - [ - 'default', - rulesetDetails.filter(ruleset => - ruleset.id === 'default' - ), - ], - [ - 'annoyances', - rulesetDetails.filter(ruleset => - ruleset.group === 'annoyances' - ), - ], - [ - 'misc', - rulesetDetails.filter(ruleset => - ruleset.id !== 'default' && - ruleset.group === undefined && - typeof ruleset.lang !== 'string' - ), - ], - [ - 'regions', - rulesetDetails.filter(ruleset => - typeof ruleset.lang === 'string' - ), - ], - ]); - - dom.cl.toggle(dom.body, 'hideUnused', mustHideUnusedLists('*')); - - for ( const [ groupKey, groupRulesets ] of groups ) { - const liGroup = liFromListGroup(groupKey, groupRulesets); - dom.attr(liGroup, 'data-groupkey', groupKey); - if ( liGroup.parentElement === null ) { - ulLists.appendChild(liGroup); - } - } -} - /******************************************************************************/ function renderWidgets() { @@ -235,27 +55,6 @@ function renderWidgets() { dom.attr(input, 'disabled', ''); } } - - // Compute total counts - let rulesetCount = 0; - let filterCount = 0; - let ruleCount = 0; - for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) { - if ( qs$(liEntry, 'input[type="checkbox"]:checked') === null ) { continue; } - rulesetCount += 1; - const stats = rulesetStats(liEntry.dataset.listkey); - if ( stats === undefined ) { continue; } - ruleCount += stats.ruleCount; - filterCount += stats.filterCount; - } - dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats') - .replace('{{ruleCount}}', ruleCount.toLocaleString()) - .replace('{{filterCount}}', filterCount.toLocaleString()) - ); - - dom.cl.toggle(dom.body, 'noMoreRuleset', - rulesetCount === cachedRulesetData.maxNumberOfEnabledRulesets - ); } /******************************************************************************/ @@ -300,7 +99,7 @@ async function onFilteringModeChange(ev) { default: break; } - renderFilterLists(); + renderFilterLists(cachedRulesetData); renderWidgets(); } @@ -367,89 +166,6 @@ self.addEventListener('beforeunload', changeTrustedSites); /******************************************************************************/ -async function applyEnabledRulesets() { - const enabledRulesets = []; - for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) { - const checked = qs$(liEntry, 'input[type="checkbox"]:checked') !== null; - dom.cl.toggle(liEntry, 'checked', checked); - if ( checked === false ) { continue; } - const { listkey } = liEntry.dataset; - if ( isAdminRuleset(listkey) ) { continue; } - enabledRulesets.push(listkey); - } - - await sendMessage({ - what: 'applyRulesets', - enabledRulesets, - }); - - renderWidgets(); -} - -dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', ( ) => { - applyEnabledRulesets(); -}); - -/******************************************************************************/ - -// Collapsing of unused lists. - -function mustHideUnusedLists(which) { - const hideAll = hideUnusedSet.has('*'); - if ( which === '*' ) { return hideAll; } - return hideUnusedSet.has(which) !== hideAll; -} - -function toggleHideUnusedLists(which) { - const doesHideAll = hideUnusedSet.has('*'); - let groupSelector; - let mustHide; - if ( which === '*' ) { - mustHide = doesHideAll === false; - groupSelector = ''; - hideUnusedSet.clear(); - if ( mustHide ) { - hideUnusedSet.add(which); - } - dom.cl.toggle(dom.body, 'hideUnused', mustHide); - dom.cl.toggle('.groupEntry[data-groupkey]', 'hideUnused', mustHide); - } else { - const doesHide = hideUnusedSet.has(which); - if ( doesHide ) { - hideUnusedSet.delete(which); - } else { - hideUnusedSet.add(which); - } - mustHide = doesHide === doesHideAll; - groupSelector = `.groupEntry[data-groupkey="${which}"]`; - dom.cl.toggle(groupSelector, 'hideUnused', mustHide); - } - - for ( const elem of qsa$(`#lists ${groupSelector} .listEntry[data-listkey] input[type="checkbox"]:not(:checked)`) ) { - dom.cl.toggle( - elem.closest('.listEntry[data-listkey]'), - 'unused', - mustHide - ); - } - - localWrite('hideUnusedFilterLists', Array.from(hideUnusedSet)); -} - -dom.on('#lists', 'click', '.groupEntry[data-groupkey] > .geDetails', ev => { - toggleHideUnusedLists( - dom.attr(ev.target.closest('[data-groupkey]'), 'data-groupkey') - ); -}); - -// Initialize from saved state. -localRead('hideUnusedFilterLists').then(value => { - if ( Array.isArray(value) === false ) { return; } - hideUnusedSet = new Set(value); -}); - -/******************************************************************************/ - function listen() { const bc = new self.BroadcastChannel('uBOL'); bc.onmessage = listen.onmessage; @@ -512,7 +228,7 @@ listen.onmessage = ev => { } if ( render === false ) { return; } - renderFilterLists(); + renderFilterLists(cachedRulesetData); renderWidgets(); }; @@ -523,10 +239,8 @@ sendMessage({ }).then(data => { if ( !data ) { return; } cachedRulesetData = data; - rulesetMap.clear(); - cachedRulesetData.rulesetDetails.forEach(rule => rulesetMap.set(rule.id, rule)); try { - renderFilterLists(); + renderFilterLists(cachedRulesetData); renderWidgets(); } catch(ex) { } diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 097191010..501af6f70 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -1068,8 +1068,10 @@ async function rulesetFromURLs(assetDetails) { id: assetDetails.id, name: assetDetails.name, group: assetDetails.group, + parent: assetDetails.parent, enabled: assetDetails.enabled, lang: assetDetails.lang, + tags: assetDetails.tags, homeURL: assetDetails.homeURL, filters: { total: results.network.filterCount, @@ -1121,7 +1123,7 @@ async function main() { // Get assets.json content const assets = await fs.readFile( - `./assets.json`, + `./assets.dev.json`, { encoding: 'utf8' } ).then(text => JSON.parse(text) @@ -1155,55 +1157,6 @@ async function main() { ], }); - // Regional rulesets - const excludedLists = [ - 'ara-0', - 'EST-0', - ]; - // Merge lists which have same target languages - const langToListsMap = new Map(); - for ( const [ id, asset ] of Object.entries(assets) ) { - if ( asset.content !== 'filters' ) { continue; } - if ( asset.off !== true ) { continue; } - if ( typeof asset.lang !== 'string' ) { continue; } - if ( excludedLists.includes(id) ) { continue; } - let ids = langToListsMap.get(asset.lang); - if ( ids === undefined ) { - langToListsMap.set(asset.lang, ids = []); - } - ids.push(id); - } - for ( const ids of langToListsMap.values() ) { - const urls = []; - for ( const id of ids ) { - const asset = assets[id]; - const contentURL = Array.isArray(asset.contentURL) - ? asset.contentURL[0] - : asset.contentURL; - urls.push(contentURL); - } - const id = ids[0]; - const asset = assets[id]; - await rulesetFromURLs({ - id: id.toLowerCase(), - lang: asset.lang, - name: asset.title, - enabled: false, - urls, - homeURL: asset.supportURL, - }); - } - - await rulesetFromURLs({ - id: 'est-0', - group: 'regions', - lang: 'et', - name: '🇪🇪ee: Eesti saitidele kohandatud filter', - enabled: false, - urls: [ 'https://ubol-et.adblock.ee/list.txt' ], - homeURL: 'https://github.com/sander85/uBOL-et', - }); - // Handpicked rulesets from assets.json const handpicked = [ 'block-lan', @@ -1290,6 +1243,62 @@ async function main() { homeURL: 'https://github.com/StevenBlack/hosts#readme', }); + // Regional rulesets + const excludedLists = [ + 'ara-0', + 'EST-0', + ]; + // Merge lists which have same target languages + const langToListsMap = new Map(); + for ( const [ id, asset ] of Object.entries(assets) ) { + if ( asset.content !== 'filters' ) { continue; } + if ( asset.off !== true ) { continue; } + if ( asset.group !== 'regions' ) { continue; } + if ( excludedLists.includes(id) ) { continue; } + // Not all "regions" lists have a set language + const bundleId = asset.lang || + createHash('sha256').update(randomBytes(16)).digest('hex').slice(0,16); + let ids = langToListsMap.get(bundleId); + if ( ids === undefined ) { + langToListsMap.set(bundleId, ids = []); + } + ids.push(id); + } + for ( const ids of langToListsMap.values() ) { + const urls = []; + for ( const id of ids ) { + const asset = assets[id]; + const contentURL = Array.isArray(asset.contentURL) + ? asset.contentURL[0] + : asset.contentURL; + urls.push(contentURL); + } + const id = ids[0]; + const asset = assets[id]; + const rulesetDetails = { + id: id.toLowerCase(), + group: 'regions', + parent: asset.parent, + lang: asset.lang, + name: asset.title, + tags: asset.tags, + enabled: false, + urls, + homeURL: asset.supportURL, + }; + await rulesetFromURLs(rulesetDetails); + } + + await rulesetFromURLs({ + id: 'est-0', + group: 'regions', + lang: 'et', + name: '🇪🇪ee: Eesti saitidele kohandatud filter', + enabled: false, + urls: [ 'https://ubol-et.adblock.ee/list.txt' ], + homeURL: 'https://github.com/sander85/uBOL-et', + }); + writeFile( `${rulesetDir}/ruleset-details.json`, `${JSON.stringify(rulesetDetails, null, 1)}\n` diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index 2e87695bf..d3e7732a6 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -440,9 +440,9 @@ function finalizeRuleset(context, network) { } }; mergeRules(rulesetMap, 'resourceTypes'); + mergeRules(rulesetMap, 'removeParams'); mergeRules(rulesetMap, 'initiatorDomains'); mergeRules(rulesetMap, 'requestDomains'); - mergeRules(rulesetMap, 'removeParams'); mergeRules(rulesetMap, 'responseHeaders'); // Patch id diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh index 3bb3c9ce4..58a050531 100755 --- a/tools/make-mv3.sh +++ b/tools/make-mv3.sh @@ -108,7 +108,7 @@ if [ "$QUICK" != "yes" ]; then cp platform/mv3/*.js "$TMPDIR"/ cp platform/mv3/*.mjs "$TMPDIR"/ cp platform/mv3/extension/js/utils.js "$TMPDIR"/js/ - cp "$UBO_DIR"/assets/assets.json "$TMPDIR"/ + cp "$UBO_DIR"/assets/assets.dev.json "$TMPDIR"/ cp "$UBO_DIR"/assets/resources/*.js "$TMPDIR"/ cp -R platform/mv3/scriptlets "$TMPDIR"/ mkdir -p "$TMPDIR"/web_accessible_resources