diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index c48776edc..28ceec5d0 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -62,6 +62,18 @@ ], "run_at": "document_idle", "all_frames": false + }, + { + "matches": [ + "https://github.com/uBlockOrigin/*", + "https://ublockorigin.github.io/*", + "https://*.reddit.com/r/uBlockOrigin/*" + ], + "js": [ + "/js/scriptlets/updater.js" + ], + "run_at": "document_idle", + "all_frames": false } ], "content_security_policy": "script-src 'self'; object-src 'self'", diff --git a/src/css/3p-filters.css b/src/css/3p-filters.css index 5749b3fbb..014dd202b 100644 --- a/src/css/3p-filters.css +++ b/src/css/3p-filters.css @@ -204,6 +204,10 @@ body.working #actions button { #lists .listEntry.checked.cached:not(.obsolete) > .detailbar .iconbar .cache { display: inline-flex; } +#lists .listEntry.cached.recent:not(.obsolete) > .detailbar .iconbar .cache { + color: var(--dashboard-happy-green); + fill: var(--dashboard-happy-green); + } #lists .iconbar .obsolete { color: var(--info2-ink); fill: var(--info2-ink); diff --git a/src/css/themes/default.css b/src/css/themes/default.css index 44609817e..c37bd28f0 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -35,6 +35,7 @@ --green-40: 84 255 189; --green-50: 63 225 176; --green-60: 42 195 162; + --green-65: 21 165 149; --green-70: 0 135 135; --green-80: 0 94 94; --ink-10: 57 52 115; @@ -239,6 +240,8 @@ --dashboard-tab-focus-surface-rgb: var(--primary-90); --dashboard-highlight-surface-rgb: var(--primary-90); + --dashboard-happy-green: rgb(var(--green-65)); + /* popup panel */ --popup-cell-cname-ink: #0054d7; /* h260 S:100 Luv:40 */; --popup-cell-label-mixed-surface: #c29100; /* TODO: fix */ diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index edc223b67..ffef2fcf4 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -29,6 +29,7 @@ import { dom, qs$, qsa$ } from './dom.js'; const lastUpdateTemplateString = i18n$('3pLastUpdate'); const obsoleteTemplateString = i18n$('3pExternalListObsolete'); const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; +const recentlyUpdated = 1 * 60 * 60 * 1000; // 1 hour let listsetDetails = {}; @@ -154,6 +155,8 @@ const renderFilterLists = ( ) => { if ( asset.cached === true ) { dom.cl.add(listEntry, 'cached'); dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString); + const timeSinceLastUpdate = Date.now() - asset.writeTime; + dom.cl.toggle(listEntry, 'recent', timeSinceLastUpdate < recentlyUpdated); } else { dom.cl.remove(listEntry, 'cached'); } @@ -308,7 +311,7 @@ const updateAssetStatus = details => { dom.attr(qs$(listEntry, '.status.cache'), 'title', lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now())) ); - + dom.cl.add(listEntry, 'recent'); } updateAncestorListNodes(listEntry, ancestor => { updateListNode(ancestor); @@ -413,7 +416,8 @@ const updateListNode = listNode => { let totalFilterCount = 0; let isCached = false; let isObsolete = false; - let writeTime = 0; + let latestWriteTime = 0; + let oldestWriteTime = Number.MAX_SAFE_INTEGER; for ( const listLeaf of checkedListLeaves ) { const listkey = listLeaf.dataset.key; const listDetails = listsetDetails.available[listkey]; @@ -422,7 +426,8 @@ const updateListNode = listNode => { const assetCache = listsetDetails.cache[listkey] || {}; isCached = isCached || dom.cl.has(listLeaf, 'cached'); isObsolete = isObsolete || dom.cl.has(listLeaf, 'obsolete'); - writeTime = Math.max(writeTime, assetCache.writeTime || 0); + latestWriteTime = Math.max(latestWriteTime, assetCache.writeTime || 0); + oldestWriteTime = Math.min(oldestWriteTime, assetCache.writeTime || Number.MAX_SAFE_INTEGER); } dom.cl.toggle(listNode, 'checked', checkedListLeaves.length !== 0); dom.cl.toggle(qs$(listNode, ':scope > .detailbar .checkbox'), @@ -449,8 +454,9 @@ const updateListNode = listNode => { dom.cl.toggle(listNode, 'obsolete', isObsolete); if ( isCached ) { dom.attr(qs$(listNode, ':scope > .detailbar .cache'), 'title', - lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(writeTime)) + lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(latestWriteTime)) ); + dom.cl.toggle(listNode, 'recent', (Date.now() - oldestWriteTime) < recentlyUpdated); } if ( qs$(listNode, '.listEntry.isDefault') !== null ) { dom.cl.add(listNode, 'isDefault'); diff --git a/src/js/dashboard.js b/src/js/dashboard.js index a88675957..31ab2e930 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -149,10 +149,18 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) { dom.on('.tabButton', 'click', onTabClickHandler); // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - dom.on(window, 'beforeunload', ( ) => { + dom.on(self, 'beforeunload', ( ) => { if ( discardUnsavedData(true) ) { return; } event.preventDefault(); event.returnValue = ''; }); + + // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + dom.on(self, 'hashchange', ( ) => { + const pane = self.location.hash.slice(1); + if ( pane === '' ) { return; } + loadDashboardPanel(pane); + }); + } })(); diff --git a/src/js/messaging.js b/src/js/messaging.js index 3ad22bbcf..053fad433 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -2057,6 +2057,21 @@ const onMessage = function(request, sender, callback) { }); break; + case 'updateLists': + const listkeys = request.listkeys.split(',').filter(s => s !== ''); + if ( listkeys.length === 0 ) { return; } + for ( const listkey of listkeys ) { + io.purge(listkey); + io.remove(`compiled/${listkey}`); + } + µb.scheduleAssetUpdater(0); + µb.openNewTab({ + url: 'dashboard.html#3p-filters.html', + select: true, + }); + io.updateStart({ delay: 100 }); + break; + default: return vAPI.messaging.UNHANDLED; } diff --git a/src/js/scriptlets/updater.js b/src/js/scriptlets/updater.js new file mode 100644 index 000000000..06f9be435 --- /dev/null +++ b/src/js/scriptlets/updater.js @@ -0,0 +1,99 @@ +/******************************************************************************* + + uBlock Origin - a browser extension to block requests. + 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 +*/ + +/* global HTMLDocument */ + +'use strict'; + +/******************************************************************************/ + +// Injected into specific webpages, those which have been pre-selected +// because they are known to contain `https://ublockorigin.github.io/update-lists?` links. + +/******************************************************************************/ + +(( ) => { +// >>>>> start of local scope + +/******************************************************************************/ + +if ( document instanceof HTMLDocument === false ) { return; } + +// Maybe uBO has gone away meanwhile. +if ( typeof vAPI !== 'object' || vAPI === null ) { return; } + +function updateStockLists(target) { + if ( vAPI instanceof Object === false ) { + document.removeEventListener('click', updateStockLists); + return; + } + try { + const updateURL = new URL(target.href); + if ( updateURL.hostname !== 'ublockorigin.github.io') { return; } + if ( updateURL.pathname !== '/uAssets/update-lists.html') { return; } + const listkeys = updateURL.searchParams.get('listkeys') || ''; + if ( listkeys === '' ) { return true; } + vAPI.messaging.send('scriptlets', { + what: 'updateLists', + listkeys, + }); + return true; + } catch (_) { + } +} + +// https://github.com/easylist/EasyListHebrew/issues/89 +// Ensure trusted events only. + +document.addEventListener('click', ev => { + if ( ev.button !== 0 || ev.isTrusted === false ) { return; } + const target = ev.target.closest('a'); + if ( target instanceof HTMLAnchorElement === false ) { return; } + if ( updateStockLists(target) === true ) { + ev.stopPropagation(); + ev.preventDefault(); + } +}); + +/******************************************************************************/ + +// <<<<< end of local scope +})(); + + + + + + + + +/******************************************************************************* + + DO NOT: + - Remove the following code + - Add code beyond the following code + Reason: + - https://github.com/gorhill/uBlock/pull/3721 + - uBO never uses the return value from injected content scripts + +**/ + +void 0;