feat: custom userscripts in-app and in-browser!

firefox support will involve some tweaking, manifest v3 not fully supported yet
This commit is contained in:
dragonwocky 2024-04-22 22:58:50 +10:00
parent 607fcee4f8
commit 71f9ecc32b
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
6 changed files with 60 additions and 10 deletions

View File

@ -185,7 +185,7 @@ const renderMenu = async () => {
const importApi = () => {
return (_apiImport ??= (async () => {
const api = globalThis.__enhancerApi;
if (typeof api === "undefined") await import("../../common/system.js");
if (typeof api === "undefined") await import("../../api/system.js");
await import("../../load.mjs").then((i) => i.default);
})());
},

View File

@ -61,6 +61,13 @@
"label": "Advanced",
"_autoremoveIfSectionEmpty": false
},
{
"type": "file",
"label": "Custom JavaScript",
"key": "customScript",
"description": "Executes the uploaded userscript within Notion. Requires <a href='https://developer.chrome.com/docs/extensions/reference/api/userScripts#developer_mode_for_extension_users'>developer mode</a> to be enabled in your browser's extension settings to run in Chromium-based browsers.",
"extensions": ["js"]
},
{
"type": "toggle",
"key": "developerMode",

View File

@ -19,7 +19,7 @@ if (isElectron()) {
const { enhancerUrl } = globalThis.__enhancerApi,
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi,
API_LOADED = new Promise((res, rej) => {
API_LOADED = new Promise((res) => {
const onReady = globalThis.__enhancerReady;
globalThis.__enhancerReady = () => (onReady?.(), res());
});
@ -33,12 +33,18 @@ if (isElectron()) {
contextBridge.exposeInMainWorld("__getEnhancerApi", __getApi);
// load clientStyles, clientScripts
document.addEventListener("readystatechange", () => {
document.addEventListener("readystatechange", async () => {
if (document.readyState !== "complete") return false;
const $script = document.createElement("script");
$script.type = "module";
$script.src = enhancerUrl("load.mjs");
document.head.append($script);
// register user-provided javascript for execution in-app
const { webFrame } = require("electron"),
db = await modDatabase("0f0bf8b6-eae6-4273-b307-8fc43f2ee082"),
customScript = (await db.get("customScript"))?.content;
if (customScript) webFrame.executeJavaScript(customScript);
});
}

View File

@ -43,7 +43,7 @@ export default (async () => {
// the dom must be re-imported
await Promise.all([
IS_ELECTRON || import(enhancerUrl("common/registry.js")),
IS_ELECTRON || import(enhancerUrl("api/registry.js")),
import(enhancerUrl("api/interface.mjs")),
import(enhancerUrl("api/state.js")),
]);

View File

@ -19,15 +19,13 @@
"permissions": [
"tabs",
"storage",
"userScripts",
"clipboardRead",
"clipboardWrite",
"unlimitedStorage"
],
"host_permissions": ["*://*.notion.so/*"],
"web_accessible_resources": [
{
"matches": ["*://*.notion.so/*"],
"resources": ["/*"]
}
{ "matches": ["*://*.notion.so/*"], "resources": ["/*"] }
]
}

View File

@ -71,7 +71,7 @@ const initDatabase = async () => {
value = JSON.parse(__statements.select.get(key)?.value);
} catch {}
} else value = (await chrome.storage.local.get([key]))[key];
return value ?? args.fallbacks[args.key];
return value ?? args.fallbacks?.[args.key];
}
case "set": {
const key = namespaceify(args.key),
@ -89,7 +89,6 @@ const initDatabase = async () => {
? (__transactions.remove(keys), true)
: chrome.storage.local.remove(keys);
}
case "export": {
// returns key/value pairs within scope w/out namespace
// prefix e.g. to streamline importing from one profile and
@ -149,6 +148,39 @@ if (IS_ELECTRON) {
.forEach((tab) => chrome.tabs.reload(tab.id));
};
const userScriptsAvailable = () => {
// manifest v3 userscripts require developer mode to be
// enabled in the browser's extension settings
try {
chrome.userScripts;
return true;
} catch {
return false;
}
},
registerCustomScript = async () => {
if (!userScriptsAvailable()) return;
// enhancer apis are not available in the worker in-browser,
// manual steps are required to get nested values from the db
const key = "customScript",
matches = ["*://*.notion.so/*"],
coreId = "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
profileId =
(await queryDatabase([], "get", { key: "activeProfile" })) ??
(await queryDatabase([], "get", { key: "profileIds" }))?.[0] ??
"default",
customScript = await queryDatabase([profileId, coreId], "get", { key }),
existingScripts = await chrome.userScripts.getScripts({ ids: [key] }),
code = customScript?.content || "";
if (existingScripts[0]) {
if (code === existingScripts[0]?.code) return;
chrome.userScripts.update([{ id: key, matches, js: [{ code }] }]);
} else if (code) {
chrome.userScripts.register([{ id: key, matches, js: [{ code }] }]);
}
};
registerCustomScript();
chrome.action.onClicked.addListener(openEnhancerMenu);
// long-lived connection for rapid two-way messaging
// b/w client and worker, primarily used for db wrapper:
@ -163,6 +195,13 @@ if (IS_ELECTRON) {
const { namespace, query, args } = message.data,
res = await queryDatabase(namespace, query, args);
if (invocation) port.postMessage({ invocation, message: res });
// re-register userscript on updates:
// profile change, db import, file upload, file deletion
const customScriptChanged =
query === "import" ||
(query === "set" &&
["activeProfile", "customScript"].includes(args.key));
if (customScriptChanged) registerCustomScript();
}
if (message === "load-complete") {
if (!openMenuInTabs.has(tabId)) return;