diff --git a/src/core/client.mjs b/src/core/client.mjs
index 6ff0257..9066492 100644
--- a/src/core/client.mjs
+++ b/src/core/client.mjs
@@ -37,14 +37,15 @@ const doThemeOverride = async (db) => {
`);
};
-const insertMenu = async (db) => {
+const initMenu = async (db) => {
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`,
+ notionSettingsAndMembers = `${notionSidebar} > [role="button"]:nth-child(3)`,
{ html, addKeyListener, addMutationListener } = globalThis.__enhancerApi,
{ platform, enhancerUrl, onMessage } = globalThis.__enhancerApi,
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
openMenuHotkey = await db.get("openMenuHotkey"),
renderPing = {
- namespace: "notion-enhancer",
+ channel: "notion-enhancer",
hotkey: openMenuHotkey,
icon: menuButtonIconStyle,
};
@@ -88,12 +89,15 @@ const insertMenu = async (db) => {
: " text-[16px]"}"
>notion-enhancer
/>`;
- document.body.append($modal);
- addMutationListener(notionSidebar, () => {
- if (document.contains($button)) return;
- document.querySelector(notionSidebar)?.append($button);
- });
- document.querySelector(notionSidebar)?.append($button);
+ const insertMenu = () => {
+ if (!document.contains($modal)) document.body.append($modal);
+ if (!document.querySelector(notionSidebar)?.contains($button)) {
+ document.querySelector(notionSettingsAndMembers)?.after($button);
+ }
+ };
+ addMutationListener(notionSidebar, insertMenu);
+ insertMenu();
+
addMutationListener("body", sendThemePing);
window.addEventListener("focus", sendRenderPing);
@@ -102,7 +106,7 @@ const insertMenu = async (db) => {
$modal.open();
});
window.addEventListener("message", (event) => {
- if (event.data?.namespace !== "notion-enhancer") return;
+ if (event.data?.channel !== "notion-enhancer") return;
if (event.data?.action === "close-menu") $modal.close();
if (event.data?.action === "open-menu") $modal.open();
});
@@ -115,7 +119,7 @@ export default async (api, db) => {
await Promise.all([
overrideThemes(db),
insertCustomStyles(db),
- insertMenu(db),
+ initMenu(db),
sendTelemetryPing(),
]);
api.sendMessage("notion-enhancer", "load-complete");
diff --git a/src/core/menu/islands/Onboarding.mjs b/src/core/menu/islands/Onboarding.mjs
index 2167901..f7b4528 100644
--- a/src/core/menu/islands/Onboarding.mjs
+++ b/src/core/menu/islands/Onboarding.mjs
@@ -29,12 +29,12 @@ function Onboarding() {
<${Description}>
In order for the notion-enhancer to function, it may access, collect,
process and/or store data on your device (including workspace content,
- device metadata, and notion-enhancer configuration) according to its
- privacy policy. Unless otherwise stated for telemetry purposes, the
- notion-enhancer will never transmit any of your data from your device.
- Telemetry can be disabled at any time through the menu.
-
-
+ device metadata, and notion-enhancer configuration) as described in its
+ privacy policy. Unless otherwise stated, the notion-enhancer will never
+ transmit your information from your device. Collection of anonymous
+ telemetry data is enabled by default and can be disabled at any time
+ through the menu.
+
The notion-enhancer is free and open-source software distributed under
the MIT License without warranty of any
kind. In no event shall the authors be liable for any consequences of
@@ -72,7 +72,7 @@ function Onboarding() {
>Check out the usage guide.
/>
<${Tile}
- href="https://notion-enhancer.github.io/getting-started/basic-usage/"
+ href="https://notion-enhancer.github.io/documentation/mods/"
icon="package-plus"
title="Something missing?"
>Build your own extension.
diff --git a/src/core/menu/menu.mjs b/src/core/menu/menu.mjs
index 9212250..a73a57d 100644
--- a/src/core/menu/menu.mjs
+++ b/src/core/menu/menu.mjs
@@ -160,7 +160,7 @@ window.addEventListener("focus", () => {
setState({ focus: true, rerender: true });
});
window.addEventListener("message", (event) => {
- if (event.data?.namespace !== "notion-enhancer") return;
+ if (event.data?.channel !== "notion-enhancer") return;
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
setState({
rerender: true,
@@ -176,13 +176,13 @@ useState(["hotkey"], ([hotkey]) => {
setState({ hotkeyRegistered: true });
addKeyListener(hotkey, (event) => {
event.preventDefault();
- const msg = { namespace: "notion-enhancer", action: "open-menu" };
+ const msg = { channel: "notion-enhancer", action: "open-menu" };
parent?.postMessage(msg, "*");
});
addKeyListener("Escape", () => {
const [popupOpen] = useState(["popupOpen"]);
if (!popupOpen) {
- const msg = { namespace: "notion-enhancer", action: "close-menu" };
+ const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*");
} else setState({ rerender: true });
});
@@ -199,7 +199,7 @@ useState(["rerender"], async () => {
// but extension:// pages can access chrome apis
// => notion-enhancer api is imported directly
if (typeof globalThis.__enhancerApi === "undefined") {
- await import("../../api/system.js");
+ await import("../../shared/system.js");
// in electron this isn't necessary, as a) scripts are
// not running in an isolated execution context and b)
// the notion:// protocol csp bypass allows scripts to
diff --git a/src/core/tweaks/client.css b/src/extensions/tweaks/client.css
similarity index 100%
rename from src/core/tweaks/client.css
rename to src/extensions/tweaks/client.css
diff --git a/src/core/tweaks/client.mjs b/src/extensions/tweaks/client.mjs
similarity index 100%
rename from src/core/tweaks/client.mjs
rename to src/extensions/tweaks/client.mjs
diff --git a/src/core/tweaks/mod.json b/src/extensions/tweaks/mod.json
similarity index 100%
rename from src/core/tweaks/mod.json
rename to src/extensions/tweaks/mod.json
diff --git a/src/init.js b/src/init.js
index de5529a..18a3889 100644
--- a/src/init.js
+++ b/src/init.js
@@ -14,15 +14,15 @@ const isElectron = () => {
};
if (isElectron()) {
- require("./api/system.js");
- require("./api/mods.js");
+ require("./shared/system.js");
+ require("./shared/registry.js");
const { enhancerUrl } = globalThis.__enhancerApi,
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
// calling require("electron") in a process require()-d
// from these paths throws "websocket connection to __ failed"
- // and triggers infinite loading => ignore for now, but
- // requires further investigation later
+ // and triggers infinite loading => ignore for now, but will
+ // require further investigation later
const ignoredPaths = [
"shared/sqliteTypes",
"shared/TimeSource",
@@ -37,7 +37,7 @@ if (isElectron()) {
module.exports = async (target, __exports, __eval) => {
if (ignoredPaths.includes(target)) return;
- if (target === "main/main") require("./worker.js");
+ if (target.startsWith("main/")) require("./worker.js");
// clientStyles
// clientScripts
@@ -63,9 +63,6 @@ if (isElectron()) {
}
};
} else {
- // clientStyles
- // clientScripts
- import(chrome.runtime.getURL("/api/system.js")).then(() => {
- import(chrome.runtime.getURL("/load.mjs"));
- });
+ import(chrome.runtime.getURL("/shared/system.js")) //
+ .then(() => import(chrome.runtime.getURL("/load.mjs")));
}
diff --git a/src/load.mjs b/src/load.mjs
index 303e979..81efb4e 100644
--- a/src/load.mjs
+++ b/src/load.mjs
@@ -20,10 +20,12 @@ export default (async () => {
import(enhancerUrl("vendor/twind.min.js")),
import(enhancerUrl("vendor/lucide.min.js")),
import(enhancerUrl("vendor/htm.min.js")),
- import(enhancerUrl("api/events.js")),
- import(enhancerUrl("api/mods.js")),
]);
- await import(enhancerUrl("api/interface.js"));
+ await Promise.all([
+ import(enhancerUrl("shared/events.js")),
+ import(enhancerUrl("shared/registry.js")),
+ import(enhancerUrl("shared/markup.js")),
+ ]);
const { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
for (const mod of await getMods()) {
diff --git a/src/api/events.js b/src/shared/events.js
similarity index 100%
rename from src/api/events.js
rename to src/shared/events.js
diff --git a/src/api/interface.js b/src/shared/markup.js
similarity index 100%
rename from src/api/interface.js
rename to src/shared/markup.js
diff --git a/src/api/mods.js b/src/shared/registry.js
similarity index 100%
rename from src/api/mods.js
rename to src/shared/registry.js
diff --git a/src/api/system.js b/src/shared/system.js
similarity index 75%
rename from src/api/system.js
rename to src/shared/system.js
index 04b0ed4..06a0e0b 100644
--- a/src/api/system.js
+++ b/src/shared/system.js
@@ -6,7 +6,8 @@
"use strict";
-const IS_ELECTRON = typeof module !== "undefined";
+const IS_ELECTRON = typeof module !== "undefined",
+ IS_RENDERER = IS_ELECTRON && process.type === "renderer";
// expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox'
// and 'chromium' (inc. chromium-based browsers like edge and brave)
@@ -26,16 +27,18 @@ const platform = IS_ELECTRON
IS_ELECTRON
? `notion://www.notion.so/__notion-enhancer/${target.replace(/^\//, "")}`
: chrome.runtime.getURL(target),
- // should only be used from an electron main process, does nothing elsewhere
- notionRequire = (target) => IS_ELECTRON && require(`../../../${target}`);
+ // require a file from the root of notion's app/ folder,
+ // only available in an electron main process
+ notionRequire = (target) =>
+ IS_ELECTRON && !IS_RENDERER ? require(`../../../${target}`) : undefined;
let __port;
const onMessage = (channel, listener) => {
// from worker to client
- if (IS_ELECTRON) {
+ if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.on(channel, listener);
- } else {
+ } else if (!IS_ELECTRON) {
__port ??= chrome.runtime.connect();
__port.onMessage.addListener((msg) => {
if (msg?.channel !== channel || msg?.invocation) return;
@@ -45,19 +48,21 @@ const onMessage = (channel, listener) => {
},
sendMessage = (channel, message) => {
// to worker from client
- if (IS_ELECTRON) {
+ if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
ipcRenderer.send(channel, message);
- } else {
+ } else if (!IS_ELECTRON) {
__port ??= chrome.runtime.connect();
__port.postMessage({ channel, message });
}
},
invokeInWorker = (channel, message) => {
- if (IS_ELECTRON) {
+ // sends a payload to the worker/main
+ // process and waits for a response
+ if (IS_RENDERER) {
const { ipcRenderer } = require("electron");
return ipcRenderer.invoke(channel, message);
- } else {
+ } else if (!IS_ELECTRON) {
// polyfills the electron.ipcRenderer.invoke method in
// the browser: uses a long-lived ipc connection to
// pass messages and handle responses asynchronously
@@ -98,15 +103,35 @@ const readFile = (file) => {
if (IS_ELECTRON) {
if (!file.startsWith("http")) {
const { resolve } = require("path");
- return require(resolve(`${__dirname}/../${file}`), "utf-8");
+ return require(resolve(`${__dirname}/../${file}`));
}
const notionProtocol = "notion://www.notion.so/";
file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol);
} else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.json());
+ };
+
+const initDatabase = (namespace, fallbacks = {}) => {
+ // all db operations are performed via ipc:
+ // with nodeintegration disabled, sqlite cannot
+ // be require()-d from the renderer process
+ const query = (query, args = {}) =>
+ IS_ELECTRON && !IS_RENDERER
+ ? globalThis.__enhancerApi.queryDatabase(namespace, query, args)
+ : invokeInWorker("notion-enhancer:db", {
+ action: "query-database",
+ data: { namespace, query, args },
+ });
+ return {
+ get: (key) => query("get", { key, fallbacks }),
+ set: (key, value) => query("set", { key, value }),
+ remove: (keys) => query("remove", { keys }),
+ export: () => query("export"),
+ import: (obj) => query("import", { obj }),
+ };
},
reloadApp = () => {
- if (IS_ELECTRON && require("electron").app) {
+ if (IS_ELECTRON && !IS_RENDERER) {
const { app } = require("electron"),
args = process.argv.slice(1).filter((arg) => arg !== "--startup");
app.relaunch({ args });
@@ -114,26 +139,6 @@ const readFile = (file) => {
} else sendMessage("notion-enhancer", "reload-app");
};
-const initDatabase = (namespace, fallbacks = {}) => {
- // all db operations are performed via ipc:
- // with nodeintegration disabled, sqlite cannot
- // be require()-d from the renderer process
- const operation = (type, args = {}) =>
- invokeInWorker("notion-enhancer:db", {
- namespace,
- fallbacks,
- operation: type,
- args,
- });
- return {
- get: (key) => operation("get", { key }),
- set: (key, value) => operation("set", { key, value }),
- remove: (keys) => operation("remove", { keys }),
- export: () => operation("export"),
- import: (obj) => operation("import", { obj }),
- };
-};
-
globalThis.__enhancerApi ??= {};
Object.assign(globalThis.__enhancerApi, {
platform,
@@ -145,6 +150,6 @@ Object.assign(globalThis.__enhancerApi, {
invokeInWorker,
readFile,
readJson,
- reloadApp,
initDatabase,
+ reloadApp,
});
diff --git a/src/worker.js b/src/worker.js
index 18a1bbc..7e87dfd 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -54,7 +54,7 @@ const initDatabase = async () => {
};
return db;
},
- executeOperation = async (namespace, fallbacks, operation, args) => {
+ queryDatabase = async (namespace, query, args) => {
namespace ??= "";
if (Array.isArray(namespace)) namespace = namespace.join("__");
if (namespace?.length) namespace += "__";
@@ -62,7 +62,7 @@ const initDatabase = async () => {
key.startsWith(namespace) ? key : namespace + key;
await (__db ??= initDatabase());
- switch (operation) {
+ switch (query) {
case "get": {
const key = namespaceify(args.key);
let value;
@@ -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 ?? fallbacks[key];
+ return value ?? args.fallbacks[args.key];
}
case "set": {
const key = namespaceify(args.key),
@@ -114,95 +114,61 @@ const initDatabase = async () => {
};
if (IS_ELECTRON) {
- const { app, ipcMain } = require("electron"),
- reloadApp = () => {
- const args = process.argv.slice(1).filter((arg) => arg !== "--startup");
- app.relaunch({ args });
- app.exit();
- };
-
- ipcMain.handle("notion-enhancer:db", (_event, message) => {
- return executeOperation(
- message.namespace,
- message.fallbacks,
- message.operation,
- message.args
- );
+ const { ipcMain } = require("electron"),
+ { reloadApp } = globalThis.__enhancerApi;
+ ipcMain.handle("notion-enhancer:db", ({}, message) => {
+ if (message?.action !== "query-database") return;
+ const { namespace, query, args } = message.data;
+ return queryDatabase(namespace, query, args);
});
-
- ipcMain.on("notion-enhancer", (_event, message) => {
- if (message === "open-menu") {
- // todo
- } else if (message === "reload-app") {
- reloadApp();
- }
+ ipcMain.on("notion-enhancer", ({ sender }, message) => {
+ if (message === "reload-app") reloadApp();
});
} else {
const notionUrl = "https://www.notion.so/",
isNotionTab = (tab) => tab?.url?.startsWith(notionUrl);
const tabQueue = new Set(),
+ openMenu = { channel: "notion-enhancer", message: "open-menu" },
openEnhancerMenu = async (tab) => {
if (!isNotionTab(tab)) {
- const openTabs = await chrome.tabs.query({
- windowId: chrome.windows.WINDOW_ID_CURRENT,
- });
- tab = openTabs.find(isNotionTab);
+ const windowId = chrome.windows.WINDOW_ID_CURRENT,
+ windowTabs = await chrome.tabs.query({ windowId });
+ tab = windowTabs.find(isNotionTab);
tab ??= await chrome.tabs.create({ url: notionUrl });
}
chrome.tabs.highlight({ tabs: [tab.index] });
if (tab.status === "complete") {
- chrome.tabs.sendMessage(tab.id, {
- channel: "notion-enhancer",
- message: "open-menu",
- });
+ chrome.tabs.sendMessage(tab.id, openMenu);
} else tabQueue.add(tab.id);
},
reloadNotionTabs = async () => {
- const openTabs = await chrome.tabs.query({
- windowId: chrome.windows.WINDOW_ID_CURRENT,
- }),
- notionTabs = openTabs.filter(isNotionTab);
- notionTabs.forEach((tab) => chrome.tabs.reload(tab.id));
+ const windowId = chrome.windows.WINDOW_ID_CURRENT;
+ (await chrome.tabs.query({ windowId }))
+ .filter(isNotionTab)
+ .forEach((tab) => chrome.tabs.reload(tab.id));
};
// listen for invoke: https://developer.chrome.com/docs/extensions/mv3/messaging/
- ipcMain.handle("notion-enhancer:db", (_event, message) => {
- return executeOperation(
- message.namespace,
- message.fallbacks,
- message.operation,
- message.args
- );
- });
-
chrome.action.onClicked.addListener(openEnhancerMenu);
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg, sender) => {
- if (msg?.channel === "notion-enhancer:db") {
- const { invocation } = msg,
- res = executeOperation(
- msg.namespace,
- msg.fallbacks,
- msg.operation,
- msg.args
- );
+ if (msg?.channel !== "notion-enhancer") return;
+ const { message, invocation } = msg;
+ if (message.action === "query-database") {
+ const { namespace, query, args } = message.data,
+ res = queryDatabase(namespace, query, args);
if (invocation) port.postMessage({ invocation, message: res });
}
-
- if (msg?.channel === "notion-enhancer") {
- if (sender.tab && msg.message === "load-complete") {
- if (tabQueue.has(sender.tab.id)) {
- chrome.tabs.sendMessage(sender.tab.id, {
- channel: "notion-enhancer",
- message: "open-menu",
- });
- tabQueue.delete(sender.tab.id);
- }
- } else if (msg.message === "reload-app") {
- reloadNotionTabs();
- }
+ if (message === "load-complete") {
+ if (!tabQueue.has(sender?.tab?.id)) return;
+ chrome.tabs.sendMessage(sender.tab.id, openMenu);
+ tabQueue.delete(sender.tab.id);
}
+ if (message === "reload-app") reloadNotionTabs();
});
});
}
+
+globalThis.__enhancerApi ??= {};
+Object.assign(globalThis.__enhancerApi, { queryDatabase });