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 = () => { const importApi = () => {
return (_apiImport ??= (async () => { return (_apiImport ??= (async () => {
const api = globalThis.__enhancerApi; 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); await import("../../load.mjs").then((i) => i.default);
})()); })());
}, },

View File

@ -61,6 +61,13 @@
"label": "Advanced", "label": "Advanced",
"_autoremoveIfSectionEmpty": false "_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", "type": "toggle",
"key": "developerMode", "key": "developerMode",

View File

@ -19,7 +19,7 @@ if (isElectron()) {
const { enhancerUrl } = globalThis.__enhancerApi, const { enhancerUrl } = globalThis.__enhancerApi,
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi, { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi,
API_LOADED = new Promise((res, rej) => { API_LOADED = new Promise((res) => {
const onReady = globalThis.__enhancerReady; const onReady = globalThis.__enhancerReady;
globalThis.__enhancerReady = () => (onReady?.(), res()); globalThis.__enhancerReady = () => (onReady?.(), res());
}); });
@ -33,12 +33,18 @@ if (isElectron()) {
contextBridge.exposeInMainWorld("__getEnhancerApi", __getApi); contextBridge.exposeInMainWorld("__getEnhancerApi", __getApi);
// load clientStyles, clientScripts // load clientStyles, clientScripts
document.addEventListener("readystatechange", () => { document.addEventListener("readystatechange", async () => {
if (document.readyState !== "complete") return false; if (document.readyState !== "complete") return false;
const $script = document.createElement("script"); const $script = document.createElement("script");
$script.type = "module"; $script.type = "module";
$script.src = enhancerUrl("load.mjs"); $script.src = enhancerUrl("load.mjs");
document.head.append($script); 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 // the dom must be re-imported
await Promise.all([ 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/interface.mjs")),
import(enhancerUrl("api/state.js")), import(enhancerUrl("api/state.js")),
]); ]);

View File

@ -19,15 +19,13 @@
"permissions": [ "permissions": [
"tabs", "tabs",
"storage", "storage",
"userScripts",
"clipboardRead", "clipboardRead",
"clipboardWrite", "clipboardWrite",
"unlimitedStorage" "unlimitedStorage"
], ],
"host_permissions": ["*://*.notion.so/*"], "host_permissions": ["*://*.notion.so/*"],
"web_accessible_resources": [ "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); value = JSON.parse(__statements.select.get(key)?.value);
} catch {} } catch {}
} else value = (await chrome.storage.local.get([key]))[key]; } else value = (await chrome.storage.local.get([key]))[key];
return value ?? args.fallbacks[args.key]; return value ?? args.fallbacks?.[args.key];
} }
case "set": { case "set": {
const key = namespaceify(args.key), const key = namespaceify(args.key),
@ -89,7 +89,6 @@ const initDatabase = async () => {
? (__transactions.remove(keys), true) ? (__transactions.remove(keys), true)
: chrome.storage.local.remove(keys); : chrome.storage.local.remove(keys);
} }
case "export": { case "export": {
// returns key/value pairs within scope w/out namespace // returns key/value pairs within scope w/out namespace
// prefix e.g. to streamline importing from one profile and // prefix e.g. to streamline importing from one profile and
@ -149,6 +148,39 @@ if (IS_ELECTRON) {
.forEach((tab) => chrome.tabs.reload(tab.id)); .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); chrome.action.onClicked.addListener(openEnhancerMenu);
// long-lived connection for rapid two-way messaging // long-lived connection for rapid two-way messaging
// b/w client and worker, primarily used for db wrapper: // b/w client and worker, primarily used for db wrapper:
@ -163,6 +195,13 @@ if (IS_ELECTRON) {
const { namespace, query, args } = message.data, const { namespace, query, args } = message.data,
res = await queryDatabase(namespace, query, args); res = await queryDatabase(namespace, query, args);
if (invocation) port.postMessage({ invocation, message: res }); 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 (message === "load-complete") {
if (!openMenuInTabs.has(tabId)) return; if (!openMenuInTabs.has(tabId)) return;