Fixed theming race condition and other bugs.

This commit is contained in:
hackademix 2024-11-10 01:16:00 +01:00
parent 7753e5345c
commit 971697043c
No known key found for this signature in database
GPG Key ID: 231A83AFDA9C2434
2 changed files with 118 additions and 92 deletions

View File

@ -19,116 +19,44 @@
*/ */
{ {
const PARENT_CLASS = "__NoScript_Theme__";
let patchSheet = s => {
const PARENT_SELECTOR = `.${PARENT_CLASS}`;
let rules = s.cssRules;
for (let j = 0, len = rules.length; j < len; j++) {
let rule = rules[j];
if (rule.styleSheet && patchSheet(rule.styleSheet)) {
return true;
}
if (rule.conditionText !== "(prefers-color-scheme: light)") continue;
for (let r of rule.cssRules) {
let {selectorText} = r;
if (selectorText.includes("[data-theme=") || !selectorText.startsWith(PARENT_SELECTOR)) continue;
selectorText = selectorText.replace(PARENT_SELECTOR, `${PARENT_SELECTOR}[data-theme="light"]`);
s.insertRule(`${selectorText} {${r.style.cssText}}`, j);
}
return true;
}
return false;
}
let patchAll = () => {
for (let s of document.styleSheets) {
try {
if (patchSheet(s)) return true;
} catch (e) {
// cross-site stylesheet?
debug(e, s.href); // DEV_ONLY
}
}
return false;
}
if (!patchAll()) {
debug("Couldn't patch sheets while loading, deferring to onload"); // DEV_ONLY
let onload = e => {
if (patchAll()) {
removeEventListener(e.type, onload, true);
}
}
addEventListener("load", onload, true);
}
let contentCSS; let contentCSS;
let root = document.documentElement;
root.classList.add(PARENT_CLASS);
const VINTAGE = "vintageTheme"; const VINTAGE = "vintageTheme";
let update = toTheme => {
if (window.localStorage) try {
localStorage.setItem("theme", toTheme);
} catch (e) {}
return root.dataset.theme = toTheme;
}
let updateFavIcon = isVintage => {
let favIcon = document.querySelector("link[rel=icon]");
if (!favIcon) return;
let {href} = favIcon;
const BASE = new URL("/img/", location.href);
if (!href.startsWith(BASE)) return alert("return");
const SUB = BASE + "vintage/";
let vintageIcon = href.startsWith(SUB);
if (isVintage === vintageIcon) return;
favIcon.href = isVintage ? href.replace(BASE, SUB) : href.replace(SUB, BASE);
}
let refreshVintage = isVintage => {
if (localStorage) try {
localStorage.setItem(VINTAGE, isVintage || "");
} catch (e) {}
document.documentElement.classList.toggle("vintage", isVintage === true);
if (browser.browserAction) {
browser.browserAction.setIcon({path: {64: `/img${isVintage ? "/vintage/" : "/"}ui-maybe64.png` }});
}
updateFavIcon(isVintage);
}
const THEMES = ["dark", "light", "auto"]; const THEMES = ["dark", "light", "auto"];
var Themes = {
globalThis.Themes = {
VINTAGE, VINTAGE,
setup(theme = null) { update() {},
refreshVintage() {},
async setup(theme = null) {
if (theme) { if (theme) {
if (browser && browser.storage) { if (browser && browser.storage) {
browser.storage.local.set({theme}); browser.storage.local.set({theme});
} }
} else { } else {
if (localStorage) { if (self.localStorage) {
theme = localStorage.getItem("theme"); theme = localStorage.getItem("theme");
if (!THEMES.includes(theme)) theme = null; if (!THEMES.includes(theme)) theme = null;
} }
if (!theme && browser && browser.storage) { if (!theme && browser && browser.storage) {
if (document.readyState === "loading") { if (self.document?.readyState === "loading") {
document.documentElement.style.visibility = "hidden"; document.documentElement.style.visibility = "hidden";
} }
return browser.storage.local.get(["theme"]).then(({theme}) => { return browser.storage.local.get(["theme"]).then(({theme}) => {
update(theme); Themes.update(theme);
if (self.document) {
document.documentElement.style.visibility = ""; document.documentElement.style.visibility = "";
}
return theme || "auto"; return theme || "auto";
}); });
} }
} }
return update(theme); return Themes.update(theme);
}, },
async isVintage() { async isVintage() {
let ret; let ret;
if (localStorage) { if (self.localStorage) {
ret = localStorage.getItem(VINTAGE); ret = localStorage.getItem(VINTAGE);
if (ret !== null) return !(ret === "false" || !ret); if (ret !== null) return !(ret === "false" || !ret);
} }
@ -137,13 +65,13 @@
}, },
async setVintage(b) { async setVintage(b) {
refreshVintage(b); Themes.refreshVintage(b);
await browser.storage.local.set({[VINTAGE]: b}); await browser.storage.local.set({[VINTAGE]: b});
return b; return b;
}, },
async getContentCSS() { async getContentCSS() {
contentCSS = contentCSS || (async () => { contentCSS ||= (async () => {
const replaceAsync = async (string, regexp, replacerFunction) => { const replaceAsync = async (string, regexp, replacerFunction) => {
regexp.lastIndex = 0; regexp.lastIndex = 0;
const promises = []; const promises = [];
@ -186,9 +114,12 @@
}; };
(async () => { (async () => {
refreshVintage(await Themes.isVintage()); if (self.document) {
await include("/common/themesDOM.js");
}
await Themes.setup();
Themes.refreshVintage(await Themes.isVintage());
})(); })();
Promise.resolve(Themes.setup());
browser.storage.onChanged.addListener((changes, area) => { browser.storage.onChanged.addListener((changes, area) => {
if (area !== "local") return; if (area !== "local") return;
@ -197,11 +128,11 @@
let {oldValue, newValue} = changes[key]; let {oldValue, newValue} = changes[key];
if (oldValue !== newValue) { if (oldValue !== newValue) {
callback(newValue); callback(newValue);
window.dispatchEvent(new CustomEvent("NoScriptThemeChanged", {detail: {[key]: newValue}})); self.dispatchEvent(new CustomEvent("NoScriptThemeChanged", {detail: {[key]: newValue}}));
} }
} }
} }
ifChanged("theme", update); ifChanged("theme", Themes.update);
ifChanged(VINTAGE, refreshVintage); ifChanged(VINTAGE, Themes.refreshVintage);
}); });
} }

95
src/common/themesDOM.js Normal file
View File

@ -0,0 +1,95 @@
/*
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
*
* Copyright (C) 2005-2024 Giorgio Maone <https://maone.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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 <https://www.gnu.org/licenses/>.
*/
if (self.document) {
const PARENT_CLASS = "__NoScript_Theme__";
const patchSheet = s => {
const PARENT_SELECTOR = `.${PARENT_CLASS}`;
const rules = s.cssRules;
for (let j = 0, len = rules.length; j < len; j++) {
const rule = rules[j];
if (rule.styleSheet && patchSheet(rule.styleSheet)) {
return true;
}
if (rule.conditionText !== "(prefers-color-scheme: light)") continue;
for (let r of rule.cssRules) {
let {selectorText} = r;
if (selectorText.includes("[data-theme=") || !selectorText.startsWith(PARENT_SELECTOR)) continue;
selectorText = selectorText.replace(PARENT_SELECTOR, `${PARENT_SELECTOR}[data-theme="light"]`);
s.insertRule(`${selectorText} {${r.style.cssText}}`, j);
}
return true;
}
return false;
}
const patchAll = () => {
for (const s of document.styleSheets) {
try {
if (patchSheet(s)) return true;
} catch (e) {
// cross-site stylesheet?
debug(e, s.href); // DEV_ONLY
}
}
return false;
}
if (!patchAll()) {
debug("Couldn't patch sheets while loading, deferring to onload"); // DEV_ONLY
const onload = e => {
if (patchAll()) {
removeEventListener(e.type, onload, true);
}
}
addEventListener("load", onload, true);
}
const root = document.documentElement;
root.classList.add(PARENT_CLASS);
Themes.update = toTheme => {
if (window.localStorage) try {
localStorage.setItem("theme", toTheme);
} catch (e) {}
return root.dataset.theme = toTheme;
}
const updateFavIcon = isVintage => {
let favIcon = document.querySelector("link[rel=icon]");
if (!favIcon) return;
let {href} = favIcon;
const BASE = new URL("/img/", location.href);
if (!href.startsWith(BASE)) return alert("return");
const SUB = BASE + "vintage/";
let vintageIcon = href.startsWith(SUB);
if (isVintage === vintageIcon) return;
favIcon.href = isVintage ? href.replace(BASE, SUB) : href.replace(SUB, BASE);
}
Themes.refreshVintage = isVintage => {
if (localStorage) try {
localStorage.setItem(VINTAGE, isVintage || "");
} catch (e) {}
document.documentElement.classList.toggle("vintage", isVintage === true);
browser?.action?.setIcon({path: {64: `/img${isVintage ? "/vintage/" : "/"}ui-maybe64.png` }});
updateFavIcon(isVintage);
}
}