feat: load api/styles into menu, chore: separate mod types into folders
@ -145,7 +145,7 @@ const unpackApp = async () => {
|
||||
// create package.json
|
||||
// prettier-ignore
|
||||
const manifestPath = getResourcePath("app/node_modules/notion-enhancer/package.json"),
|
||||
jsManifest = { ...manifest, main: "electron/init.cjs" };
|
||||
jsManifest = { ...manifest, main: "init.js" };
|
||||
// remove cli-specific fields
|
||||
delete jsManifest.bin;
|
||||
delete jsManifest.type;
|
||||
|
@ -10,8 +10,9 @@
|
||||
// the variables at the top of the file should be placed in core/variables.css
|
||||
// as a reference for theme developers, but not loaded into notion.
|
||||
|
||||
// the css body below should be passed through https://css.github.io/csso/csso.html
|
||||
// and then saved to core/theme.css. repeat this process for both light and dark modes
|
||||
// the css body below should be passed through https://css-minifier.com/ and
|
||||
// https://css.github.io/csso/csso.html, then saved to core/theme.css.
|
||||
// repeat this process for both light and dark modes.
|
||||
|
||||
// not yet themed: notion's new svg icons
|
||||
|
||||
@ -452,11 +453,11 @@ const generateBackgroundStyles = () => {
|
||||
if (!innerText || innerText.includes(" ")) continue;
|
||||
const pageVar = `--theme--bg-${innerText}`,
|
||||
pageColor = getComputedPropertyValue(page, "background-color"),
|
||||
groupVar = `--theme--bg_dim-${innerText}`,
|
||||
groupVar = `--theme--dim-${innerText}`,
|
||||
groupColor = group
|
||||
.getAttribute("style")
|
||||
.match(/background(?:-color)?:\s*([^;]+);?/)[1];
|
||||
// get bg_dim variable values
|
||||
// get dim variable values
|
||||
cssRoot += `${groupVar}: ${groupColor};`;
|
||||
// in light mode pages in board views all have bg "white"
|
||||
// by default, must be styled based on parent
|
||||
@ -476,12 +477,12 @@ const generateBackgroundStyles = () => {
|
||||
refs[`--theme--bg-yellow, rgba(255, 212, 0, 0.14)`].push(
|
||||
`.notion-body${modeSelector} [style*="background: rgba(255, 212, 0, 0.14)"]`
|
||||
);
|
||||
// use bg_dim for callout blocks
|
||||
// use dim for callout blocks
|
||||
for (const el of document.querySelectorAll(
|
||||
'.notion-callout-block > div > [style*="background:"]'
|
||||
)) {
|
||||
if (!el.innerText || el.innerText.includes(" ")) continue;
|
||||
const cssVar = `--theme--bg_dim-${el.innerText}`,
|
||||
const cssVar = `--theme--dim-${el.innerText}`,
|
||||
colorVal = getComputedPropertyValue(el, "background-color"),
|
||||
styleAttr = el
|
||||
.getAttribute("style")
|
||||
@ -771,6 +772,18 @@ const prismTokens = [
|
||||
color: var(${cssVar}, ${colorVal}) !important;
|
||||
}`;
|
||||
}
|
||||
// patch: remove backgrounds from prism tokens
|
||||
if (!darkMode) {
|
||||
cssBody += `
|
||||
.notion-body${modeSelector} .token.operator,
|
||||
.notion-body${modeSelector} .token.entity,
|
||||
.notion-body${modeSelector} .token.url,
|
||||
.notion-body${modeSelector} .language-css .token.string,
|
||||
.notion-body${modeSelector} .style .token.string {
|
||||
background: transparent !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
};
|
||||
generateCodeStyles();
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const dependencies = {
|
||||
"htm.min.js": "https://unpkg.com/htm@3.1.1/mini/index.module.js",
|
||||
"htm.min.js": "https://unpkg.com/htm@3.1.1/mini/index.js",
|
||||
"twind.min.js": "https://unpkg.com/@twind/cdn@1.0.4/cdn.global.js",
|
||||
"lucide.min.js": "https://unpkg.com/lucide@0.104.0/dist/umd/lucide.min.js",
|
||||
"jscolor.min.js":
|
||||
|
@ -22,14 +22,17 @@ const readFile = async (file) => {
|
||||
},
|
||||
reloadApp = () => chrome.runtime.sendMessage({ action: "reload" });
|
||||
|
||||
const initDatabase = (namespace) => {
|
||||
const initDatabase = (namespace, fallbacks = {}) => {
|
||||
if (Array.isArray(namespace)) namespace = namespace.join("__");
|
||||
namespace = namespace ? namespace + "__" : "";
|
||||
return {
|
||||
get: async (key) => {
|
||||
const fallback = fallbacks[key];
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
return new Promise((res, _rej) => {
|
||||
chrome.storage.local.get(key, (value) => res(value));
|
||||
chrome.storage.local.get([key], ({ [key]: value }) =>
|
||||
res(value ?? fallback)
|
||||
);
|
||||
});
|
||||
},
|
||||
set: async (key, value) => {
|
@ -9,31 +9,26 @@
|
||||
const fs = require("fs"),
|
||||
os = require("os"),
|
||||
path = require("path"),
|
||||
platform = process.platform;
|
||||
notionRequire = (target) => require(`../../../${target}`);
|
||||
|
||||
const notionRequire = (target) => require(`../../../${target}`),
|
||||
notionPath = (target) => path.resolve(`${__dirname}/../../../${target}`);
|
||||
|
||||
const enhancerRequire = (target) => require(`notion-enhancer/${target}`),
|
||||
enhancerPath = (target) => path.resolve(`${__dirname}/../${target}`),
|
||||
const platform = process.platform,
|
||||
enhancerVersion = require("notion-enhancer/package.json").version,
|
||||
enhancerUrl = (target) =>
|
||||
`notion://www.notion.so/__notion-enhancer/${target}`,
|
||||
enhancerVersion = enhancerRequire("package.json").version,
|
||||
enhancerConfig = path.resolve(`${os.homedir()}/.notion-enhancer.db`);
|
||||
`notion://www.notion.so/__notion-enhancer/${target.replace(/^\//, "")}`;
|
||||
|
||||
const readFile = (file) => {
|
||||
// prettier-ignore
|
||||
file = file.replace(/^https:\/\/www\.notion\.so\//, "notion://www.notion.so/");
|
||||
const useFetch = file.startsWith("http") || file.startsWith("notion://");
|
||||
if (useFetch) return fetch(file).then((res) => res.text());
|
||||
return fs.readFileSync(enhancerPath(file));
|
||||
return fs.readFileSync(path.resolve(`${__dirname}/../${file}`), "utf-8");
|
||||
},
|
||||
readJson = (file) => {
|
||||
// prettier-ignore
|
||||
file = file.replace(/^https:\/\/www\.notion\.so\//, "notion://www.notion.so/");
|
||||
const useFetch = file.startsWith("http") || file.startsWith("notion://");
|
||||
if (useFetch) return fetch(file).then((res) => res.json());
|
||||
return require(enhancerPath(file));
|
||||
return require(path.resolve(`${__dirname}/../${file}`));
|
||||
},
|
||||
reloadApp = () => {
|
||||
const { app } = require("electron"),
|
||||
@ -43,13 +38,13 @@ const readFile = (file) => {
|
||||
};
|
||||
|
||||
let __db;
|
||||
const initDatabase = (namespace) => {
|
||||
const initDatabase = (namespace, fallbacks = {}) => {
|
||||
if (Array.isArray(namespace)) namespace = namespace.join("__");
|
||||
namespace = namespace ? namespace + "__" : "";
|
||||
|
||||
const table = "settings",
|
||||
sqlite = require("better-sqlite3"),
|
||||
db = __db ?? sqlite(enhancerConfig),
|
||||
db = __db ?? sqlite(path.resolve(`${os.homedir()}/.notion-enhancer.db`)),
|
||||
init = db.prepare(`CREATE TABLE IF NOT EXISTS ${table} (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
@ -72,8 +67,9 @@ const initDatabase = (namespace) => {
|
||||
|
||||
return {
|
||||
get: (key) => {
|
||||
const fallback = fallbacks[key];
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
return select.get(key)?.value;
|
||||
return select.get(key)?.value ?? fallback;
|
||||
},
|
||||
set: (key, value) => {
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
@ -94,14 +90,10 @@ const initDatabase = (namespace) => {
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, {
|
||||
platform,
|
||||
notionRequire,
|
||||
notionPath,
|
||||
enhancerRequire,
|
||||
enhancerPath,
|
||||
platform,
|
||||
enhancerUrl,
|
||||
enhancerVersion,
|
||||
enhancerConfig,
|
||||
readFile,
|
||||
readJson,
|
||||
reloadApp,
|
115
src/api/events.js
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let documentObserver,
|
||||
mutationListeners = [];
|
||||
const mutationQueue = [],
|
||||
addMutationListener = (selector, callback) => {
|
||||
mutationListeners.push([selector, callback]);
|
||||
},
|
||||
removeMutationListener = (callback) => {
|
||||
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
|
||||
},
|
||||
onSelectorMutated = (mutation, selector) =>
|
||||
mutation.target?.matches(`${selector}, ${selector} *`) ||
|
||||
[...(mutation.addedNodes || [])].some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
(node?.matches(`${selector}, ${selector} *`) ||
|
||||
node?.querySelector(selector))
|
||||
),
|
||||
handleMutations = () => {
|
||||
while (mutationQueue.length) {
|
||||
const mutation = mutationQueue.shift();
|
||||
for (const [selector, callback] of mutationListeners) {
|
||||
if (onSelectorMutated(mutation, selector)) callback(mutation);
|
||||
}
|
||||
}
|
||||
},
|
||||
attachObserver = () => {
|
||||
if (document.readyState !== "complete") return;
|
||||
documentObserver ??= new MutationObserver((mutations, _observer) => {
|
||||
if (!mutationQueue.length) requestIdleCallback(handleMutations);
|
||||
mutationQueue.push(...mutations);
|
||||
});
|
||||
documentObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
document.addEventListener("readystatechange", attachObserver);
|
||||
attachObserver();
|
||||
|
||||
let keyListeners = [];
|
||||
// accelerators approximately match electron accelerators.
|
||||
// logic used when recording hotkeys in menu matches logic used
|
||||
// when triggering hotkeys ∴ detection should be reliable.
|
||||
// default hotkeys using "alt" may trigger an altcode or
|
||||
// accented character on some keyboard layouts (not recommended).
|
||||
const modifierAliases = [
|
||||
["metaKey", ["meta", "os", "win", "cmd", "command"]],
|
||||
["ctrlKey", ["ctrl", "control"]],
|
||||
["shiftKey", ["shift"]],
|
||||
["altKey", ["alt"]],
|
||||
],
|
||||
addKeyListener = (accelerator, callback, waitForKeyup = false) => {
|
||||
if (typeof accelerator === "string") accelerator = accelerator.split("+");
|
||||
accelerator = accelerator.map((key) => key.toLowerCase());
|
||||
keyListeners.push([accelerator, callback, waitForKeyup]);
|
||||
},
|
||||
removeKeyListener = (callback) => {
|
||||
keyListeners = keyListeners.filter(([, c]) => c !== callback);
|
||||
},
|
||||
handleKeypress = (event, keyListeners) => {
|
||||
for (const [accelerator, callback] of keyListeners) {
|
||||
const acceleratorModifiers = [],
|
||||
combinationTriggered =
|
||||
accelerator.every((key) => {
|
||||
for (const [modifier, aliases] of modifierAliases) {
|
||||
if (aliases.includes(key)) {
|
||||
acceleratorModifiers.push(modifier);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (key === "plus") key = "+";
|
||||
const keyPressed = [
|
||||
event.key.toLowerCase(),
|
||||
event.code.toLowerCase(),
|
||||
].includes(key);
|
||||
return keyPressed;
|
||||
}) &&
|
||||
modifierAliases.every(([modifier]) => {
|
||||
// required && used -> matches accelerator
|
||||
// !required && !used -> matches accelerator
|
||||
// (required && !used) || (!required && used) -> no match
|
||||
// differentiates e.g.ctrl + x from ctrl + shift + x
|
||||
return acceleratorModifiers.includes(modifier) === event[modifier];
|
||||
});
|
||||
if (combinationTriggered) callback(event);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keyup", (event) => {
|
||||
const keyupListeners = keyListeners.filter(
|
||||
([, , waitForKeyup]) => waitForKeyup
|
||||
);
|
||||
handleKeypress(event, keyupListeners);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const keydownListeners = keyListeners.filter(
|
||||
([, , waitForKeyup]) => !waitForKeyup
|
||||
);
|
||||
handleKeypress(event, keydownListeners);
|
||||
});
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, {
|
||||
addMutationListener,
|
||||
removeMutationListener,
|
||||
addKeyListener,
|
||||
removeKeyListener,
|
||||
});
|
153
src/api/interface.js
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { twind, htm } = globalThis,
|
||||
{ readFile } = globalThis.__enhancerApi;
|
||||
|
||||
let iconColour, iconMonochrome;
|
||||
(async () => {
|
||||
iconColour = await readFile("/assets/colour.svg");
|
||||
iconMonochrome = await readFile("/assets/monochrome.svg");
|
||||
})();
|
||||
|
||||
const kebabToPascalCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
|
||||
hToString = (type, props, ...children) =>
|
||||
`<${type}${Object.entries(props)
|
||||
.map(([attr, value]) => ` ${attr}="${value}"`)
|
||||
.join("")}>${children
|
||||
.flat(Infinity)
|
||||
.map(([tag, attrs, children]) => hToString(tag, attrs, children))
|
||||
.join("")}</${type}>`;
|
||||
|
||||
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||
const encodeSvg = (svg) =>
|
||||
svg
|
||||
.replace(
|
||||
"<svg",
|
||||
~svg.indexOf("xmlns") ? "<svg" : '<svg xmlns="http://www.w3.org/2000/svg"'
|
||||
)
|
||||
.replace(/"/g, "'")
|
||||
.replace(/%/g, "%25")
|
||||
.replace(/#/g, "%23")
|
||||
.replace(/{/g, "%7B")
|
||||
.replace(/}/g, "%7D")
|
||||
.replace(/</g, "%3C")
|
||||
.replace(/>/g, "%3E")
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
twind.install({
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["var(--theme--font-sans)"],
|
||||
mono: ["var(--theme--font-code)"],
|
||||
},
|
||||
colors: {
|
||||
"fg-primary": "var(--theme--fg-primary)",
|
||||
"fg-secondary": "var(--theme--fg-secondary)",
|
||||
"fg-border": "var(--theme--fg-border)",
|
||||
"fg-gray": "var(--theme--fg-gray)",
|
||||
"fg-brown": "var(--theme--fg-brown)",
|
||||
"fg-orange": "var(--theme--fg-orange)",
|
||||
"fg-yellow": "var(--theme--fg-yellow)",
|
||||
"fg-green": "var(--theme--fg-green)",
|
||||
"fg-blue": "var(--theme--fg-blue)",
|
||||
"fg-purple": "var(--theme--fg-purple)",
|
||||
"fg-pink": "var(--theme--fg-pink)",
|
||||
"fg-red": "var(--theme--fg-red)",
|
||||
"bg-primary": "var(--theme--bg-primary)",
|
||||
"bg-secondary": "var(--theme--bg-secondary)",
|
||||
"bg-overlay": "var(--theme--bg-overlay)",
|
||||
"bg-hover": "var(--theme--bg-hover)",
|
||||
"bg-light_gray": "var(--theme--bg-light_gray)",
|
||||
"bg-gray": "var(--theme--bg-gray)",
|
||||
"bg-brown": "var(--theme--bg-brown)",
|
||||
"bg-orange": "var(--theme--bg-orange)",
|
||||
"bg-yellow": "var(--theme--bg-yellow)",
|
||||
"bg-green": "var(--theme--bg-green)",
|
||||
"bg-blue": "var(--theme--bg-blue)",
|
||||
"bg-purple": "var(--theme--bg-purple)",
|
||||
"bg-pink": "var(--theme--bg-pink)",
|
||||
"bg-red": "var(--theme--bg-red)",
|
||||
"dim-light_gray": "var(--theme--dim-light_gray)",
|
||||
"dim-gray": "var(--theme--dim-gray)",
|
||||
"dim-brown": "var(--theme--dim-brown)",
|
||||
"dim-orange": "var(--theme--dim-orange)",
|
||||
"dim-yellow": "var(--theme--dim-yellow)",
|
||||
"dim-green": "var(--theme--dim-green)",
|
||||
"dim-blue": "var(--theme--dim-blue)",
|
||||
"dim-purple": "var(--theme--dim-purple)",
|
||||
"dim-pink": "var(--theme--dim-pink)",
|
||||
"dim-red": "var(--theme--dim-red)",
|
||||
"accent-primary": "var(--theme--accent-primary)",
|
||||
"accent-primary_hover": "var(--theme--accent-primary_hover)",
|
||||
"accent-primary_contrast": "var(--theme--accent-primary_contrast)",
|
||||
"accent-primary_transparent": "var(--theme--accent-primary_transparent)",
|
||||
"accent-secondary": "var(--theme--accent-secondary)",
|
||||
"accent-secondary_contrast": "var(--theme--accent-secondary_contrast)",
|
||||
"accent-secondary_transparent":
|
||||
"var(--theme--accent-secondary_transparent)",
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
[
|
||||
/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/,
|
||||
([, icon, mode]) => {
|
||||
let svg;
|
||||
// manually register i-notion-enhancer: renders the colour
|
||||
// version by default, renders the monochrome version when
|
||||
// mask mode is requested via i-notion-enhancer?mask
|
||||
if (icon === "notion-enhancer") {
|
||||
svg = mode === "mask" ? iconMonochrome : iconColour;
|
||||
} else {
|
||||
icon = kebabToPascalCase(icon);
|
||||
if (!globalThis.lucide[icon]) return;
|
||||
svg = hToString(...globalThis.lucide[icon]);
|
||||
}
|
||||
// https://antfu.me/posts/icons-in-pure-css
|
||||
const dataUri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`;
|
||||
if (mode === "auto") mode = undefined;
|
||||
mode ??= svg.includes("currentColor") ? "mask" : "bg";
|
||||
return mode === "mask"
|
||||
? {
|
||||
mask: `${dataUri} no-repeat`,
|
||||
"mask-size": "100% 100%",
|
||||
"background-color": "currentColor",
|
||||
color: "inherit",
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
}
|
||||
: {
|
||||
background: `${dataUri} no-repeat`,
|
||||
"background-size": "100% 100%",
|
||||
"background-color": "transparent",
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
};
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// construct elements via tagged tagged
|
||||
// e.g. html`<div class=${className}></div>`
|
||||
const h = (type, props, ...children) => {
|
||||
const elem = document.createElement(type);
|
||||
for (const prop in props) {
|
||||
if (["string", "number", "boolean"].includes(typeof props[prop])) {
|
||||
elem.setAttribute(prop, props[prop]);
|
||||
} else elem[prop] = props[prop];
|
||||
}
|
||||
for (const child of children) elem.append(child);
|
||||
return elem;
|
||||
},
|
||||
html = htm.bind(h);
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, { html });
|
@ -6,35 +6,33 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
let _core, _mods;
|
||||
const getCore = async () => {
|
||||
_core ??= await globalThis.__enhancerApi.readJson("core/mod.json");
|
||||
_core._src = "core";
|
||||
return _core;
|
||||
},
|
||||
getMods = async () => {
|
||||
let _mods;
|
||||
const getMods = async () => {
|
||||
const { readJson } = globalThis.__enhancerApi;
|
||||
// prettier-ignore
|
||||
_mods ??= (await Promise.all([
|
||||
getCore(),
|
||||
...(await readJson("mods/registry.json")).map(async (modFolder) => {
|
||||
const modManifest = await readJson(`mods/${modFolder}/mod.json`);
|
||||
return {...modManifest, _src: `mods/${modFolder}` };
|
||||
}),
|
||||
]));
|
||||
_mods ??= await Promise.all(
|
||||
// prettier-ignore
|
||||
(await readJson("registry.json")).map(async (_src) => {
|
||||
const modManifest = await readJson(`${_src}/mod.json`);
|
||||
return { ...modManifest, _src };
|
||||
})
|
||||
);
|
||||
return _mods;
|
||||
},
|
||||
getCore = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.find(({ _src }) => _src === "core");
|
||||
},
|
||||
getThemes = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("theme"));
|
||||
return mods.find(({ _src }) => _src.startsWith("themes/"));
|
||||
},
|
||||
getExtensions = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("extension"));
|
||||
return mods.find(({ _src }) => _src.startsWith("extensions/"));
|
||||
},
|
||||
getIntegrations = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("integration"));
|
||||
return mods.find(({ _src }) => _src.startsWith("integrations/"));
|
||||
};
|
||||
|
||||
const getProfile = async () => {
|
||||
@ -43,13 +41,27 @@ const getProfile = async () => {
|
||||
return currentProfile ?? "default";
|
||||
},
|
||||
isEnabled = async (id) => {
|
||||
if (id === (await getCore()).id) return true;
|
||||
const { platform } = globalThis.__enhancerApi,
|
||||
mod = (await getMods()).find((mod) => mod.id === id);
|
||||
if (mod._src === "core") return true;
|
||||
if (mod.platforms && !mod.platforms.includes(platform)) return false;
|
||||
const { initDatabase } = globalThis.__enhancerApi,
|
||||
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
||||
return Boolean(await enabledMods.get(id));
|
||||
},
|
||||
optionDefaults = async (id) => {
|
||||
const mod = (await getMods()).find((mod) => mod.id === id),
|
||||
optionEntries = mod.options
|
||||
.map((opt) => {
|
||||
if (
|
||||
["toggle", "text", "number", "hotkey", "color"].includes(opt.type)
|
||||
)
|
||||
return [opt.key, opt.value];
|
||||
if (opt.type === "select") return [opt.key, opt.values[0]];
|
||||
return undefined;
|
||||
})
|
||||
.filter((opt) => opt);
|
||||
return Object.fromEntries(optionEntries);
|
||||
};
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
@ -61,4 +73,5 @@ Object.assign(globalThis.__enhancerApi, {
|
||||
getIntegrations,
|
||||
getProfile,
|
||||
isEnabled,
|
||||
optionDefaults,
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(async () => {
|
||||
await import("./api.mjs");
|
||||
await import("../common/loader.mjs");
|
||||
})();
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* notion-enhancer
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
function focusMenu() {
|
||||
chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT }, (tabs) => {
|
||||
const url = chrome.runtime.getURL("repo/menu/menu.html"),
|
||||
menu = tabs.find((tab) => tab.url.startsWith(url));
|
||||
if (menu) {
|
||||
chrome.tabs.highlight({ tabs: menu.index });
|
||||
} else chrome.tabs.create({ url });
|
||||
});
|
||||
}
|
||||
chrome.browserAction.onClicked.addListener(focusMenu);
|
||||
|
||||
function reload() {
|
||||
chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT }, (tabs) => {
|
||||
const menu = chrome.runtime.getURL("repo/menu/menu.html");
|
||||
tabs.forEach((tab) => {
|
||||
const url = new URL(tab.url),
|
||||
matches =
|
||||
url.host.endsWith(".notion.so") ||
|
||||
url.host.endsWith(".notion.site") ||
|
||||
tab.url.startsWith(menu);
|
||||
if (matches) chrome.tabs.reload(tab.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
switch (request.action) {
|
||||
case "focusMenu":
|
||||
focusMenu();
|
||||
break;
|
||||
case "focusNotion":
|
||||
focusNotion();
|
||||
break;
|
||||
case "reload":
|
||||
reload();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
});
|
@ -1,99 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import "../vendor/twind.min.js";
|
||||
import "../vendor/lucide.min.js";
|
||||
import htm from "../vendor/htm.min.js";
|
||||
|
||||
const { readFile } = globalThis.__enhancerApi,
|
||||
enhancerIconColour = await readFile("/assets/colour.svg"),
|
||||
enhancerIconMonochrome = await readFile("/assets/monochrome.svg");
|
||||
|
||||
const kebabToPascalCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
|
||||
hToString = (type, props, ...children) =>
|
||||
`<${type}${Object.entries(props)
|
||||
.map(([attr, value]) => ` ${attr}="${value}"`)
|
||||
.join("")}>${children
|
||||
.flat(Infinity)
|
||||
.map(([tag, attrs, children]) => hToString(tag, attrs, children))
|
||||
.join("")}</${type}>`;
|
||||
|
||||
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||
const encodeSvg = (svg) =>
|
||||
svg
|
||||
.replace(
|
||||
"<svg",
|
||||
~svg.indexOf("xmlns") ? "<svg" : '<svg xmlns="http://www.w3.org/2000/svg"'
|
||||
)
|
||||
.replace(/"/g, "'")
|
||||
.replace(/%/g, "%25")
|
||||
.replace(/#/g, "%23")
|
||||
.replace(/{/g, "%7B")
|
||||
.replace(/}/g, "%7D")
|
||||
.replace(/</g, "%3C")
|
||||
.replace(/>/g, "%3E")
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
// https://antfu.me/posts/icons-in-pure-css
|
||||
const presetIcons = () => ({
|
||||
rules: [
|
||||
[
|
||||
/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/,
|
||||
([, icon, mode]) => {
|
||||
let svg;
|
||||
// manually register i-notion-enhancer: renders the colour
|
||||
// version by default, renders the monochrome version when
|
||||
// mask mode is requested via i-notion-enhancer?mask
|
||||
if (icon === "notion-enhancer") {
|
||||
svg = mode === "mask" ? enhancerIconMonochrome : enhancerIconColour;
|
||||
} else {
|
||||
icon = kebabToPascalCase(icon);
|
||||
if (!globalThis.lucide[icon]) return;
|
||||
svg = hToString(...globalThis.lucide[icon]);
|
||||
}
|
||||
const dataUri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`;
|
||||
if (mode === "auto") mode = undefined;
|
||||
mode ??= svg.includes("currentColor") ? "mask" : "bg";
|
||||
return mode === "mask"
|
||||
? {
|
||||
mask: `${dataUri} no-repeat`,
|
||||
"mask-size": "100% 100%",
|
||||
"background-color": "currentColor",
|
||||
color: "inherit",
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
}
|
||||
: {
|
||||
background: `${dataUri} no-repeat`,
|
||||
"background-size": "100% 100%",
|
||||
"background-color": "transparent",
|
||||
height: "1em",
|
||||
width: "1em",
|
||||
};
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const { twind } = globalThis;
|
||||
twind.install({ presets: [presetIcons()] });
|
||||
|
||||
// constructs elements via html`tagged templates`
|
||||
const h = (type, props, ...children) => {
|
||||
const elem = document.createElement(type);
|
||||
for (const prop in props) {
|
||||
if (["string", "number", "boolean"].includes(typeof props[prop])) {
|
||||
elem.setAttribute(prop, props[prop]);
|
||||
} else elem[prop] = props[prop];
|
||||
}
|
||||
for (const child of children) elem.append(child);
|
||||
return elem;
|
||||
},
|
||||
html = htm.bind(h);
|
||||
|
||||
export { html, twind };
|
@ -1,156 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
let mutationListeners = [];
|
||||
const documentMutations = [],
|
||||
selectorMutated = (mutation, selector) =>
|
||||
mutation.target?.matches(`${selector}, ${selector} *`) ||
|
||||
[...(mutation.addedNodes || [])].some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
(node?.matches(`${selector}, ${selector} *`) ||
|
||||
node?.querySelector(selector))
|
||||
),
|
||||
handleMutations = () => {
|
||||
while (documentMutations.length) {
|
||||
const mutation = documentMutations.shift();
|
||||
for (const [selector, callback] of mutationListeners) {
|
||||
if (selectorMutated(mutation, selector)) callback(mutation);
|
||||
}
|
||||
}
|
||||
};
|
||||
const documentObserver = new MutationObserver((mutations, _observer) => {
|
||||
if (!documentMutations.length) requestIdleCallback(handleMutations);
|
||||
documentMutations.push(...mutations);
|
||||
}),
|
||||
attachObserver = () => {
|
||||
if (document.readyState !== "complete") return;
|
||||
documentObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
document.addEventListener("readystatechange", attachObserver);
|
||||
attachObserver();
|
||||
|
||||
const addMutationListener = (selector, callback) => {
|
||||
mutationListeners.push([selector, callback]);
|
||||
},
|
||||
removeMutationListener = (callback) => {
|
||||
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
|
||||
};
|
||||
|
||||
export { addMutationListener, removeMutationListener };
|
||||
|
||||
// let _hotkeyListenersActivated = false,
|
||||
// _hotkeyEventListeners = [],
|
||||
// _documentObserver,
|
||||
// _documentObserverListeners = [];
|
||||
// const _documentObserverEvents = [];
|
||||
|
||||
// /**
|
||||
// * wait until a page is loaded and ready for modification
|
||||
// * @param {array=} selectors - wait for the existence of elements that match these css selectors
|
||||
// * @returns {Promise} a promise that will resolve when the page is ready
|
||||
// */
|
||||
// export const whenReady = (selectors = []) => {
|
||||
// return new Promise((res, _rej) => {
|
||||
// const onLoad = () => {
|
||||
// const interval = setInterval(isReady, 100);
|
||||
// function isReady() {
|
||||
// const ready = selectors.every((selector) => document.querySelector(selector));
|
||||
// if (!ready) return;
|
||||
// clearInterval(interval);
|
||||
// res(true);
|
||||
// }
|
||||
// isReady();
|
||||
// };
|
||||
// if (document.readyState !== "complete") {
|
||||
// document.addEventListener("readystatechange", (_event) => {
|
||||
// if (document.readyState === "complete") onLoad();
|
||||
// });
|
||||
// } else onLoad();
|
||||
// });
|
||||
// };
|
||||
|
||||
// const triggerHotkeyListener = (event, hotkey) => {
|
||||
// const inInput = document.activeElement.nodeName === "INPUT" && !hotkey.listenInInput;
|
||||
// if (inInput) return;
|
||||
// const modifiers = {
|
||||
// metaKey: ["meta", "os", "win", "cmd", "command"],
|
||||
// ctrlKey: ["ctrl", "control"],
|
||||
// shiftKey: ["shift"],
|
||||
// altKey: ["alt"],
|
||||
// },
|
||||
// pressed = hotkey.keys.every((key) => {
|
||||
// key = key.toLowerCase();
|
||||
// for (const modifier in modifiers) {
|
||||
// const pressed = modifiers[modifier].includes(key) && event[modifier];
|
||||
// if (pressed) {
|
||||
// // mark modifier as part of hotkey
|
||||
// modifiers[modifier] = [];
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// if (key === "space") key = " ";
|
||||
// if (key === "plus") key = "+";
|
||||
// if (key === event.key.toLowerCase()) return true;
|
||||
// });
|
||||
// if (!pressed) return;
|
||||
// // test for modifiers not in hotkey
|
||||
// // e.g. to differentiate ctrl+x from ctrl+shift+x
|
||||
// for (const modifier in modifiers) {
|
||||
// const modifierPressed = event[modifier],
|
||||
// modifierNotInHotkey = modifiers[modifier].length > 0;
|
||||
// if (modifierPressed && modifierNotInHotkey) return;
|
||||
// }
|
||||
// hotkey.callback(event);
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * register a hotkey listener to the page
|
||||
// * @param {array|string} keys - the combination of keys that will trigger the hotkey.
|
||||
// * key codes can be tested at http://keycode.info/ and are case-insensitive.
|
||||
// * available modifiers are 'alt', 'ctrl', 'meta', and 'shift'.
|
||||
// * can be provided as a + separated string.
|
||||
// * @param {function} callback - called whenever the keys are pressed
|
||||
// * @param {object=} opts - fine-tuned control over when the hotkey should be triggered
|
||||
// * @param {boolean=} opts.listenInInput - whether the hotkey callback should be triggered
|
||||
// * when an input is focused
|
||||
// * @param {boolean=} opts.keydown - whether to listen for the hotkey on keydown.
|
||||
// * by default, hotkeys are triggered by the keyup event.
|
||||
// */
|
||||
// export const addHotkeyListener = (
|
||||
// keys,
|
||||
// callback,
|
||||
// { listenInInput = false, keydown = false } = {}
|
||||
// ) => {
|
||||
// if (typeof keys === "string") keys = keys.split("+");
|
||||
// _hotkeyEventListeners.push({ keys, callback, listenInInput, keydown });
|
||||
|
||||
// if (!_hotkeyListenersActivated) {
|
||||
// _hotkeyListenersActivated = true;
|
||||
// document.addEventListener("keyup", (event) => {
|
||||
// for (const hotkey of _hotkeyEventListeners.filter(({ keydown }) => !keydown)) {
|
||||
// triggerHotkeyListener(event, hotkey);
|
||||
// }
|
||||
// });
|
||||
// document.addEventListener("keydown", (event) => {
|
||||
// for (const hotkey of _hotkeyEventListeners.filter(({ keydown }) => keydown)) {
|
||||
// triggerHotkeyListener(event, hotkey);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// /**
|
||||
// * remove a listener added with web.addHotkeyListener
|
||||
// * @param {function} callback
|
||||
// */
|
||||
// export const removeHotkeyListener = (callback) => {
|
||||
// _hotkeyEventListeners = _hotkeyEventListeners.filter(
|
||||
// (listener) => listener.callback !== callback
|
||||
// );
|
||||
// };
|
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
(async () => {
|
||||
const signedIn = localStorage["LRU:KeyValueStore2:current-user-id"],
|
||||
pageLoaded = /(^\/$)|((-|\/)[0-9a-f]{32}((\?.+)|$))/.test(
|
||||
location.pathname
|
||||
);
|
||||
if (!signedIn || !pageLoaded) return;
|
||||
|
||||
await import("./api.js");
|
||||
await import("./dom.mjs");
|
||||
await import("./events.mjs");
|
||||
const { getMods, getProfile, isEnabled, enhancerUrl, initDatabase } =
|
||||
globalThis.__enhancerApi;
|
||||
for (const mod of await getMods()) {
|
||||
if (!(await isEnabled(mod.id))) continue;
|
||||
|
||||
// clientStyles
|
||||
for (let stylesheet of mod.clientStyles ?? []) {
|
||||
const $stylesheet = document.createElement("link");
|
||||
$stylesheet.rel = "stylesheet";
|
||||
$stylesheet.href = enhancerUrl(`${mod._src}/${stylesheet}`);
|
||||
document.head.appendChild($stylesheet);
|
||||
}
|
||||
|
||||
// clientScripts
|
||||
for (let script of mod.clientScripts ?? []) {
|
||||
const db = initDatabase([await getProfile(), mod.id]);
|
||||
script = await import(enhancerUrl(`${mod._src}/${script}`));
|
||||
script.default(globalThis.__enhancerApi, db);
|
||||
}
|
||||
}
|
||||
})();
|
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
const kebabToPascalCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
|
||||
camelToSentenceCase = (string) =>
|
||||
string[0].toUpperCase() +
|
||||
string.replace(/[A-Z]/g, (match) => " " + match.toLowerCase()).slice(1);
|
||||
|
||||
const hToString = (type, props, ...children) =>
|
||||
`<${type}${Object.entries(props)
|
||||
.map(([attr, value]) => ` ${attr}="${value}"`)
|
||||
.join("")}>${children
|
||||
.flat(Infinity)
|
||||
.map(([tag, attrs, children]) => hToString(tag, attrs, children))
|
||||
.join("")}</${type}>`;
|
||||
|
||||
// /**
|
||||
// * log-based shading of an rgb color, from
|
||||
// * https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
|
||||
// * @param {number} shade - a decimal amount to shade the color.
|
||||
// * 1 = white, 0 = the original color, -1 = black
|
||||
// * @param {string} color - the rgb color
|
||||
// * @returns {string} the shaded color
|
||||
// */
|
||||
// export const rgbLogShade = (shade, color) => {
|
||||
// const int = parseInt,
|
||||
// round = Math.round,
|
||||
// [a, b, c, d] = color.split(","),
|
||||
// t = shade < 0 ? 0 : shade * 255 ** 2,
|
||||
// p = shade < 0 ? 1 + shade : 1 - shade;
|
||||
// return (
|
||||
// "rgb" +
|
||||
// (d ? "a(" : "(") +
|
||||
// round((p * int(a[3] == "a" ? a.slice(5) : a.slice(4)) ** 2 + t) ** 0.5) +
|
||||
// "," +
|
||||
// round((p * int(b) ** 2 + t) ** 0.5) +
|
||||
// "," +
|
||||
// round((p * int(c) ** 2 + t) ** 0.5) +
|
||||
// (d ? "," + d : ")")
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * pick a contrasting color e.g. for text on a variable color background
|
||||
// * using the hsp (perceived brightness) constants from http://alienryderflex.com/hsp.html
|
||||
// * @param {number} r - red (0-255)
|
||||
// * @param {number} g - green (0-255)
|
||||
// * @param {number} b - blue (0-255)
|
||||
// * @returns {string} the contrasting rgb color, white or black
|
||||
// */
|
||||
// export const rgbContrast = (r, g, b) => {
|
||||
// return Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) > 165.75
|
||||
// ? "rgb(0,0,0)"
|
||||
// : "rgb(255,255,255)";
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * parse the current location search params into a usable form
|
||||
// * @returns {Map<string, string>} a map of the url search params
|
||||
// */
|
||||
// export const queryParams = () => new URLSearchParams(window.location.search);
|
||||
|
||||
// /**
|
||||
// * replace special html characters with escaped versions
|
||||
// * @param {string} str
|
||||
// * @returns {string} escaped string
|
||||
// */
|
||||
// export const escape = (str) =>
|
||||
// str
|
||||
// .replace(/&/g, "&")
|
||||
// .replace(/</g, "<")
|
||||
// .replace(/>/g, ">")
|
||||
// .replace(/'/g, "'")
|
||||
// .replace(/"/g, """)
|
||||
// .replace(/\\/g, "\");
|
||||
|
||||
// /**
|
||||
// * copy text to the clipboard
|
||||
// * @param {string} str - the string to copy
|
||||
// * @returns {Promise<void>}
|
||||
// */
|
||||
// export const copyToClipboard = async (str) => {
|
||||
// try {
|
||||
// await navigator.clipboard.writeText(str);
|
||||
// } catch {
|
||||
// const $el = document.createElement("textarea");
|
||||
// $el.value = str;
|
||||
// $el.setAttribute("readonly", "");
|
||||
// $el.style.position = "absolute";
|
||||
// $el.style.left = "-9999px";
|
||||
// document.body.appendChild($el);
|
||||
// $el.select();
|
||||
// document.execCommand("copy");
|
||||
// document.body.removeChild($el);
|
||||
// }
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * read text from the clipboard
|
||||
// * @returns {Promise<string>}
|
||||
// */
|
||||
// export const readFromClipboard = () => {
|
||||
// return navigator.clipboard.readText();
|
||||
// };
|
||||
|
||||
globalThis.__enhancerUtils ??= {};
|
||||
Object.assign(globalThis.__enhancerUtils, {
|
||||
hToString,
|
||||
kebabToPascalCase,
|
||||
camelToSentenceCase,
|
||||
});
|
@ -5,61 +5,11 @@
|
||||
*/
|
||||
|
||||
@import url("./variables.css");
|
||||
@import url("./theme.css");
|
||||
|
||||
.notion-enhancer--menu-button {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: background 20ms ease-in;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
margin: 1px 4px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.notion-enhancer--menu-button:hover {
|
||||
background: var(--theme--bg-hover);
|
||||
}
|
||||
.notion-enhancer--menu-button > :nth-child(1) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.notion-enhancer--menu-modal {
|
||||
z-index: 999;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 100ms ease-in;
|
||||
/* style="display:none" is set to prevent pop-in fouc */
|
||||
display: auto !important;
|
||||
}
|
||||
.notion-enhancer--menu-modal[data-open="true"] {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
.notion-enhancer--menu-modal > :nth-child(1) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--theme--bg-overlay);
|
||||
}
|
||||
.notion-enhancer--menu-modal > :nth-child(2) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
}
|
||||
.notion-enhancer--menu-modal > :nth-child(2) > iframe {
|
||||
background: var(--theme--bg-secondary);
|
||||
box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px,
|
||||
@ -80,12 +30,3 @@
|
||||
transform: scale(1);
|
||||
transition: transform 80ms ease-in;
|
||||
}
|
||||
|
||||
/* patch: remove backgrounds from prism tokens */
|
||||
.notion-light-theme .token.operator,
|
||||
.notion-light-theme .token.entity,
|
||||
.notion-light-theme .token.url,
|
||||
.notion-light-theme .language-css .token.string,
|
||||
.notion-light-theme .style .token.string {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
@ -4,45 +4,97 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { html } from "../common/dom.mjs";
|
||||
import { addMutationListener } from "../common/events.mjs";
|
||||
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`;
|
||||
|
||||
export default async () => {
|
||||
const { enhancerUrl } = globalThis.__enhancerApi,
|
||||
menuButtonIconStyle = "";
|
||||
export default async (api, db) => {
|
||||
const { enhancerUrl, platform } = api,
|
||||
{ html, addMutationListener, addKeyListener } = api,
|
||||
openMenuHotkey = await db.get("openMenuHotkey"),
|
||||
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
|
||||
loadThemeOverrides = await db.get("loadThemeOverrides"),
|
||||
customStyles = await db.get("customStyles");
|
||||
|
||||
const icon = `i-notion-enhancer${
|
||||
menuButtonIconStyle === "monochrome" ? "?mask" : " text-[16px]"
|
||||
}`;
|
||||
// appearance
|
||||
|
||||
const menuModal = html`<div
|
||||
class="notion-enhancer--menu-modal"
|
||||
style="display:none"
|
||||
if (loadThemeOverrides) {
|
||||
const $themeOverrides = html`<link
|
||||
rel="stylesheet"
|
||||
href=${enhancerUrl("core/theme.css")}
|
||||
/>`;
|
||||
document.head.append($themeOverrides);
|
||||
}
|
||||
|
||||
if (customStyles) {
|
||||
const $cssInsert = html`<style>
|
||||
${customStyles}
|
||||
</style>`;
|
||||
document.head.append($cssInsert);
|
||||
}
|
||||
|
||||
// menu
|
||||
|
||||
let $menuModal, $menuFrame;
|
||||
const getTheme = () => {
|
||||
return document.body.classList.contains("dark") ? "dark" : "light";
|
||||
},
|
||||
openMenu = () => {
|
||||
if (!$menuFrame) return;
|
||||
const msg = { namespace: "notion-enhancer", mode: getTheme() };
|
||||
if (platform !== "browser") $menuFrame.contentWindow.__enhancerApi = api;
|
||||
$menuFrame.contentWindow.postMessage(msg, "*");
|
||||
$menuModal.setAttribute("data-open", true);
|
||||
},
|
||||
closeMenu = () => $menuModal.removeAttribute("data-open");
|
||||
|
||||
const $menuButton = html`<div
|
||||
onclick=${openMenu}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="notion-enhancer--menu-button
|
||||
flex select-none cursor-pointer rounded-[3px]
|
||||
text-[14px] my-px mx-[4px] py-[2px] px-[10px]
|
||||
transition hover:bg-bg-hover"
|
||||
>
|
||||
<div onclick=${() => menuModal.removeAttribute("data-open")}></div>
|
||||
<div>
|
||||
<div class="flex items-center justify-center w-[22px] h-[22px] mr-[8px]">
|
||||
<i
|
||||
class="i-notion-enhancer${menuButtonIconStyle === "monochrome"
|
||||
? "?mask"
|
||||
: " text-[16px]"}"
|
||||
></i>
|
||||
</div>
|
||||
<div>notion-enhancer</div>
|
||||
</div>`;
|
||||
addMutationListener(notionSidebar, () => {
|
||||
if (document.contains($menuButton)) return;
|
||||
document.querySelector(notionSidebar)?.append($menuButton);
|
||||
});
|
||||
document.querySelector(notionSidebar)?.append($menuButton);
|
||||
|
||||
$menuModal = html`<div
|
||||
class="notion-enhancer--menu-modal
|
||||
z-[999] fixed inset-0 w-screen h-screen
|
||||
transition pointer-events-none opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-bg-overlay" onclick=${closeMenu}></div>
|
||||
<div
|
||||
class="fixed inset-0 flex w-screen h-screen
|
||||
items-center justify-center point-events-none"
|
||||
>
|
||||
<iframe
|
||||
title="notion-enhancer menu"
|
||||
src="${enhancerUrl("core/menu.html")}"
|
||||
src="${enhancerUrl("core/menu/index.html")}"
|
||||
onload=${(event) => ($menuFrame = event.target)}
|
||||
></iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.append(menuModal);
|
||||
document.body.append($menuModal);
|
||||
|
||||
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`,
|
||||
menuButton = html`<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="notion-enhancer--menu-button"
|
||||
onclick=${() => menuModal.setAttribute("data-open", true)}
|
||||
>
|
||||
<div><i class=${icon}></i></div>
|
||||
<div>notion-enhancer</div>
|
||||
</div>`,
|
||||
addToSidebar = () => {
|
||||
if (document.contains(menuButton)) return;
|
||||
document.querySelector(notionSidebar)?.append(menuButton);
|
||||
};
|
||||
addMutationListener(notionSidebar, addToSidebar);
|
||||
addToSidebar();
|
||||
addKeyListener(openMenuHotkey, (event) => {
|
||||
event.preventDefault();
|
||||
openMenu();
|
||||
});
|
||||
addKeyListener("Escape", () => {
|
||||
if (document.activeElement?.nodeName === "INPUT") return;
|
||||
closeMenu();
|
||||
});
|
||||
};
|
||||
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>notion-enhancer menu</title>
|
||||
<link rel="stylesheet" href="./menu.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="./menu.mjs" type="module"></script>
|
||||
</body>
|
||||
</html>
|
28
src/core/menu/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>notion-enhancer menu</title>
|
||||
<link rel="stylesheet" href="./menu.css" />
|
||||
</head>
|
||||
<body class="w-screen h-screen bg-bg-primary text-fg-primary font-sans">
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<p>lorem ipsum</p>
|
||||
<script src="./menu.mjs" type="module" defer></script>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +1,11 @@
|
||||
/**
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
::selection {
|
||||
background: var(--theme--accent_blue-selection);
|
||||
background: var(--theme--accent-primary_transparent);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@ -15,11 +15,14 @@
|
||||
}
|
||||
::-webkit-scrollbar-track,
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--theme--scrollbar_track) !important;
|
||||
background: var(--theme--scrollbar-track) !important;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--theme--scrollbar_thumb) !important;
|
||||
background: var(--theme--scrollbar-thumb) !important;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--theme--scrollbar_thumb-hover) !important;
|
||||
background: var(--theme--scrollbar-thumb_hover) !important;
|
||||
}
|
||||
|
||||
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
|
||||
*,::after,::before{box-sizing:border-box}html{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}
|
@ -1,448 +1,41 @@
|
||||
/**
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as api from '../../api/index.mjs';
|
||||
import { notifications, $changelogModal } from './notifications.mjs';
|
||||
import { modComponents, options } from './components.mjs';
|
||||
import * as router from './router.mjs';
|
||||
import './styles.mjs';
|
||||
|
||||
(async () => {
|
||||
const { env, fs, storage, electron, registry, web, components } = api;
|
||||
|
||||
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
for (let script of mod.js?.menu || []) {
|
||||
script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
|
||||
script.default(api, await registry.db(mod.id));
|
||||
let stylesLoaded = false;
|
||||
const importApi = async () => {
|
||||
// chrome extensions run in an isolated execution context
|
||||
// but extension:// pages can access chrome apis
|
||||
// ∴ notion-enhancer api is imported directly
|
||||
if (typeof globalThis.__enhancerApi === "undefined") {
|
||||
await import("../../api/browser.js");
|
||||
}
|
||||
}
|
||||
const errors = await registry.errors();
|
||||
if (errors.length) {
|
||||
console.error('[notion-enhancer] registry errors:');
|
||||
console.table(errors);
|
||||
const $errNotification = await notifications.add({
|
||||
icon: 'alert-circle',
|
||||
message: 'Failed to load mods (check console).',
|
||||
color: 'red',
|
||||
});
|
||||
if (['win32', 'linux', 'darwin'].includes(env.name)) {
|
||||
$errNotification.addEventListener('click', () => electron.browser.openDevTools());
|
||||
// in electron this is not necessary, as a) scripts are
|
||||
// not running in an isolated execution context and b)
|
||||
// the notion:// protocol csp bypass allows scripts to
|
||||
// set iframe globals via $iframe.contentWindow
|
||||
},
|
||||
importStyles = async () => {
|
||||
if (!stylesLoaded) {
|
||||
// clientStyles + twind/htm/etc.
|
||||
await import("../../load.mjs");
|
||||
stylesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
||||
profileName = await registry.profileName(),
|
||||
profileDB = await registry.profileDB();
|
||||
|
||||
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
|
||||
|
||||
globalThis.addEventListener('beforeunload', (_event) => {
|
||||
// trigger input save
|
||||
document.activeElement.blur();
|
||||
});
|
||||
|
||||
const $main = web.html`<main class="main"></main>`,
|
||||
$sidebar = web.html`<article class="sidebar"></article>`,
|
||||
$options = web.html`<div class="options-container">
|
||||
<p class="options-placeholder">Select a mod to view and configure its options.</p>
|
||||
</div>`,
|
||||
$profile = web.html`<button class="profile-trigger">
|
||||
Profile: ${web.escape(profileName)}
|
||||
</button>`;
|
||||
|
||||
// profile
|
||||
|
||||
let _$profileConfig;
|
||||
const openProfileMenu = async () => {
|
||||
if (!_$profileConfig) {
|
||||
const profileNames = [
|
||||
...new Set([
|
||||
...Object.keys(await storage.get(['profiles'], { default: {} })),
|
||||
profileName,
|
||||
]),
|
||||
],
|
||||
$options = profileNames.map(
|
||||
(profile) => web.raw`<option
|
||||
class="select-option"
|
||||
value="${web.escape(profile)}"
|
||||
${profile === profileName ? 'selected' : ''}
|
||||
>${web.escape(profile)}</option>`
|
||||
),
|
||||
$select = web.html`<select class="input">
|
||||
<option class="select-option" value="--">-- new --</option>
|
||||
${$options.join('')}
|
||||
</select>`,
|
||||
$edit = web.html`<input
|
||||
type="text"
|
||||
class="input"
|
||||
value="${web.escape(profileName)}"
|
||||
pattern="/^[A-Za-z0-9_-]+$/"
|
||||
>`,
|
||||
$export = web.html`<button class="profile-export">
|
||||
${await components.feather('download', { class: 'profile-icon-action' })}
|
||||
</button>`,
|
||||
$import = web.html`<label class="profile-import">
|
||||
<input type="file" class="hidden" accept="application/json">
|
||||
${await components.feather('upload', { class: 'profile-icon-action' })}
|
||||
</label>`,
|
||||
$save = web.html`<button class="profile-save">
|
||||
${await components.feather('save', { class: 'profile-icon-text' })} Save
|
||||
</button>`,
|
||||
$delete = web.html`<button class="profile-delete">
|
||||
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
|
||||
</button>`,
|
||||
$error = web.html`<p class="profile-error"></p>`;
|
||||
|
||||
$export.addEventListener('click', async (_event) => {
|
||||
const now = new Date(),
|
||||
$a = web.html`<a
|
||||
class="hidden"
|
||||
download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()}.json"
|
||||
href="data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2)
|
||||
)}"
|
||||
></a>`;
|
||||
web.render(document.body, $a);
|
||||
$a.click();
|
||||
$a.remove();
|
||||
});
|
||||
|
||||
$import.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader();
|
||||
reader.onload = async (progress) => {
|
||||
try {
|
||||
const profileUpload = JSON.parse(progress.currentTarget.result);
|
||||
if (!profileUpload) throw Error;
|
||||
await storage.set(['profiles', $select.value], profileUpload);
|
||||
env.reload();
|
||||
} catch {
|
||||
web.render(web.empty($error), 'Invalid JSON uploaded.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
$select.addEventListener('change', (_event) => {
|
||||
if ($select.value === '--') {
|
||||
$edit.value = '';
|
||||
} else $edit.value = $select.value;
|
||||
});
|
||||
|
||||
$save.addEventListener('click', async (_event) => {
|
||||
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
`The profile "${web.escape($edit.value)}" already exists.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!$edit.value || !$edit.value.match(/^[A-Za-z0-9_-]+$/)) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
'Profile names may not be empty & may only contain letters, numbers, hyphens and underscores.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
await storage.set(['currentprofile'], $edit.value);
|
||||
if ($select.value === '--') {
|
||||
await storage.set(['profiles', $edit.value], {});
|
||||
} else if ($select.value !== $edit.value) {
|
||||
await storage.set(
|
||||
['profiles', $edit.value],
|
||||
await storage.get(['profiles', $select.value], {})
|
||||
);
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
}
|
||||
env.reload();
|
||||
});
|
||||
|
||||
$delete.addEventListener('click', async (_event) => {
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
await storage.set(
|
||||
['currentprofile'],
|
||||
profileNames.find((profile) => profile !== $select.value) || 'default'
|
||||
);
|
||||
env.reload();
|
||||
});
|
||||
|
||||
_$profileConfig = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.html`<p class="options-placeholder">
|
||||
Profiles are used to switch entire configurations.<br>
|
||||
Be careful - deleting a profile deletes all configuration
|
||||
related to it.<br>
|
||||
</p>`,
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$select,
|
||||
web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$edit,
|
||||
web.html`${await components.feather('type', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<p class="profile-actions"></p>`,
|
||||
$export,
|
||||
$import,
|
||||
$save,
|
||||
$delete
|
||||
),
|
||||
$error
|
||||
);
|
||||
},
|
||||
updateTheme = (mode) => {
|
||||
if (mode === "dark") {
|
||||
document.body.classList.add("dark");
|
||||
} else if (mode === "light") {
|
||||
document.body.classList.remove("dark");
|
||||
}
|
||||
web.render(web.empty($options), _$profileConfig);
|
||||
};
|
||||
$profile.addEventListener('click', () => openSidebarMenu('profile'));
|
||||
|
||||
// mods
|
||||
|
||||
const $modLists = {},
|
||||
generators = {
|
||||
options: async (mod) => {
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (const opt of mod.options) {
|
||||
if (!opt.environments.includes(env.name)) continue;
|
||||
web.render($fragment, await options[opt.type](mod, opt));
|
||||
}
|
||||
if (!mod.options.length) {
|
||||
web.render($fragment, web.html`<p class="options-placeholder">No options.</p>`);
|
||||
}
|
||||
return $fragment;
|
||||
},
|
||||
mod: async (mod) => {
|
||||
const $mod = web.html`<div class="mod" data-id="${web.escape(mod.id)}"></div>`,
|
||||
$toggle = modComponents.toggle('', await registry.enabled(mod.id));
|
||||
$toggle.addEventListener('change', async (event) => {
|
||||
if (event.target.checked && mod.tags.includes('theme')) {
|
||||
const mode = mod.tags.includes('light') ? 'light' : 'dark',
|
||||
id = mod.id,
|
||||
mods = await registry.list(
|
||||
async (mod) =>
|
||||
(await registry.enabled(mod.id)) &&
|
||||
mod.tags.includes('theme') &&
|
||||
mod.tags.includes(mode) &&
|
||||
mod.id !== id
|
||||
);
|
||||
for (const mod of mods) {
|
||||
profileDB.set(['_mods', mod.id], false);
|
||||
document.querySelector(
|
||||
`[data-id="${web.escape(mod.id)}"] .toggle-check`
|
||||
).checked = false;
|
||||
}
|
||||
}
|
||||
profileDB.set(['_mods', mod.id], event.target.checked);
|
||||
notifications.onChange();
|
||||
});
|
||||
$mod.addEventListener('click', () => openSidebarMenu(mod.id));
|
||||
return web.render(
|
||||
web.html`<article class="mod-container"></article>`,
|
||||
web.render(
|
||||
$mod,
|
||||
mod.preview
|
||||
? modComponents.preview(
|
||||
mod.preview.startsWith('http')
|
||||
? mod.preview
|
||||
: fs.localPath(`repo/${mod._dir}/${mod.preview}`)
|
||||
)
|
||||
: '',
|
||||
web.render(
|
||||
web.html`<div class="mod-body"></div>`,
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
modComponents.description(mod.description),
|
||||
modComponents.authors(mod.authors),
|
||||
mod.environments.includes(env.name) && !registry.core.includes(mod.id)
|
||||
? $toggle
|
||||
: ''
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
modList: async (category, message = '') => {
|
||||
if (!$modLists[category]) {
|
||||
const $search = web.html`<input type="search" class="search"
|
||||
placeholder="Search ('/' to focus)">`,
|
||||
$list = web.html`<div class="mods-list"></div>`,
|
||||
mods = await registry.list(
|
||||
(mod) => mod.environments.includes(env.name) && mod.tags.includes(category)
|
||||
);
|
||||
web.addHotkeyListener(['/'], () => $search.focus());
|
||||
$search.addEventListener('input', (_event) => {
|
||||
const query = $search.value.toLowerCase();
|
||||
for (const $mod of $list.children) {
|
||||
const matches = !query || $mod.innerText.toLowerCase().includes(query);
|
||||
$mod.classList[matches ? 'remove' : 'add']('hidden');
|
||||
}
|
||||
});
|
||||
for (const mod of mods) {
|
||||
mod.tags = mod.tags.filter((tag) => tag !== category);
|
||||
web.render($list, await generators.mod(mod));
|
||||
mod.tags.unshift(category);
|
||||
}
|
||||
$modLists[category] = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.render(
|
||||
web.html`<label class="search-container"></label>`,
|
||||
$search,
|
||||
web.html`${await components.feather('search', { class: 'input-icon' })}`
|
||||
),
|
||||
message ? web.render(web.html`<p class="main-message"></p>`, message) : '',
|
||||
$list
|
||||
);
|
||||
}
|
||||
return $modLists[category];
|
||||
},
|
||||
};
|
||||
|
||||
async function openModMenu(id) {
|
||||
let $mod;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
$mod = $list.querySelector(`[data-id="${web.escape(id)}"]`);
|
||||
if ($mod) break;
|
||||
}
|
||||
const mod = await registry.get(id);
|
||||
if (!$mod || !mod || $mod.className === 'mod-selected') return;
|
||||
|
||||
$mod.className = 'mod-selected';
|
||||
const fragment = [
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
await generators.options(mod),
|
||||
];
|
||||
web.render(web.empty($options), ...fragment);
|
||||
}
|
||||
|
||||
// views
|
||||
|
||||
const $notionNavItem = web.html`<h1 class="nav-notion">
|
||||
${(await fs.getText('media/colour.svg')).replace(
|
||||
/width="\d+" height="\d+"/,
|
||||
`class="nav-notion-icon"`
|
||||
)}
|
||||
<span>notion-enhancer</span>
|
||||
</h1>`;
|
||||
$notionNavItem.addEventListener('click', env.focusNotion);
|
||||
|
||||
const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`,
|
||||
$extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
|
||||
$themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`,
|
||||
$integrationsNavItem = web.html`<a href="?view=integrations" class="nav-item">integrations</a>`,
|
||||
$changelogNavItem = web.html`<button class="nav-item nav-changelog">
|
||||
${await components.feather('clock', { class: 'nav-changelog-icon' })}
|
||||
</button>`;
|
||||
components.addTooltip($changelogNavItem, '**Update changelog & welcome message**');
|
||||
$changelogNavItem.addEventListener('click', () => {
|
||||
$changelogModal.scrollTop = 0;
|
||||
$changelogModal.classList.add('modal-visible');
|
||||
});
|
||||
|
||||
web.render(
|
||||
document.body,
|
||||
web.render(
|
||||
web.html`<div class="body-container"></div>`,
|
||||
web.render(
|
||||
web.html`<div class="content-container"></div>`,
|
||||
web.render(
|
||||
web.html`<nav class="nav"></nav>`,
|
||||
$notionNavItem,
|
||||
$coreNavItem,
|
||||
$extensionsNavItem,
|
||||
$themesNavItem,
|
||||
$integrationsNavItem,
|
||||
web.html`<a href="https://notion-enhancer.github.io" target="_blank" class="nav-item">docs</a>`,
|
||||
web.html`<a href="https://discord.gg/sFWPXtA" target="_blank" class="nav-item">community</a>`,
|
||||
$changelogNavItem
|
||||
),
|
||||
$main
|
||||
),
|
||||
web.render($sidebar, $profile, $options)
|
||||
)
|
||||
);
|
||||
|
||||
function selectNavItem($item) {
|
||||
for (const $selected of document.querySelectorAll('.nav-item-selected')) {
|
||||
$selected.className = 'nav-item';
|
||||
}
|
||||
$item.className = 'nav-item-selected';
|
||||
}
|
||||
|
||||
await generators.modList(
|
||||
'core',
|
||||
`Core mods provide the basics required for
|
||||
all other extensions and themes to work. They
|
||||
can't be disabled, but they can be configured
|
||||
- just click on a mod to access its options.`
|
||||
);
|
||||
router.addView('core', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($coreNavItem);
|
||||
return web.render($main, await generators.modList('core'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'extension',
|
||||
`Extensions build on the functionality and layout of
|
||||
the Notion client, modifying and interacting with
|
||||
existing interfaces.`
|
||||
);
|
||||
router.addView('extensions', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($extensionsNavItem);
|
||||
return web.render($main, await generators.modList('extension'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'theme',
|
||||
`Themes change Notion's colour scheme.
|
||||
Dark themes will only work when Notion is in dark mode,
|
||||
and light themes will only work when Notion is in light mode.
|
||||
Only one theme of each mode can be enabled at a time.`
|
||||
);
|
||||
router.addView('themes', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($themesNavItem);
|
||||
return web.render($main, await generators.modList('theme'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'integration',
|
||||
web.html`<span class="danger">Integrations are extensions that use an unofficial API
|
||||
to access and modify content. They are used just like
|
||||
normal extensions, but may be more dangerous to use.</span>`
|
||||
);
|
||||
router.addView('integrations', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($integrationsNavItem);
|
||||
return web.render($main, await generators.modList('integration'));
|
||||
});
|
||||
|
||||
router.setDefaultView('extensions');
|
||||
|
||||
router.addQueryListener('id', openSidebarMenu);
|
||||
function openSidebarMenu(id) {
|
||||
if (!id) return;
|
||||
id = web.escape(id);
|
||||
|
||||
const deselectedMods = `.mod-selected:not([data-id="${id}"])`;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
for (const $selected of $list.querySelectorAll(deselectedMods)) {
|
||||
$selected.className = 'mod';
|
||||
}
|
||||
}
|
||||
router.updateQuery(`?id=${id}`);
|
||||
|
||||
if (id === 'profile') {
|
||||
openProfileMenu();
|
||||
} else openModMenu(id);
|
||||
}
|
||||
})();
|
||||
window.addEventListener("message", async (event) => {
|
||||
if (event.data?.namespace !== "notion-enhancer") return;
|
||||
updateTheme(event.data?.mode);
|
||||
await importApi();
|
||||
await importStyles();
|
||||
console.log(globalThis.__enhancerApi);
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
/* ::selection {
|
||||
::selection {
|
||||
background: var(--theme--accent_blue-selection);
|
||||
}
|
||||
|
||||
@ -22,4 +22,4 @@
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--theme--scrollbar_thumb-hover) !important;
|
||||
} */
|
||||
}
|
448
src/core/menuu/menu.mjs
Normal file
@ -0,0 +1,448 @@
|
||||
/**
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as api from '../../api/index.mjs';
|
||||
import { notifications, $changelogModal } from './notifications.mjs';
|
||||
import { modComponents, options } from './components.mjs';
|
||||
import * as router from './router.mjs';
|
||||
import './styles.mjs';
|
||||
|
||||
(async () => {
|
||||
const { env, fs, storage, electron, registry, web, components } = api;
|
||||
|
||||
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
for (let script of mod.js?.menu || []) {
|
||||
script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
|
||||
script.default(api, await registry.db(mod.id));
|
||||
}
|
||||
}
|
||||
const errors = await registry.errors();
|
||||
if (errors.length) {
|
||||
console.error('[notion-enhancer] registry errors:');
|
||||
console.table(errors);
|
||||
const $errNotification = await notifications.add({
|
||||
icon: 'alert-circle',
|
||||
message: 'Failed to load mods (check console).',
|
||||
color: 'red',
|
||||
});
|
||||
if (['win32', 'linux', 'darwin'].includes(env.name)) {
|
||||
$errNotification.addEventListener('click', () => electron.browser.openDevTools());
|
||||
}
|
||||
}
|
||||
|
||||
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
||||
profileName = await registry.profileName(),
|
||||
profileDB = await registry.profileDB();
|
||||
|
||||
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
|
||||
|
||||
globalThis.addEventListener('beforeunload', (_event) => {
|
||||
// trigger input save
|
||||
document.activeElement.blur();
|
||||
});
|
||||
|
||||
const $main = web.html`<main class="main"></main>`,
|
||||
$sidebar = web.html`<article class="sidebar"></article>`,
|
||||
$options = web.html`<div class="options-container">
|
||||
<p class="options-placeholder">Select a mod to view and configure its options.</p>
|
||||
</div>`,
|
||||
$profile = web.html`<button class="profile-trigger">
|
||||
Profile: ${web.escape(profileName)}
|
||||
</button>`;
|
||||
|
||||
// profile
|
||||
|
||||
let _$profileConfig;
|
||||
const openProfileMenu = async () => {
|
||||
if (!_$profileConfig) {
|
||||
const profileNames = [
|
||||
...new Set([
|
||||
...Object.keys(await storage.get(['profiles'], { default: {} })),
|
||||
profileName,
|
||||
]),
|
||||
],
|
||||
$options = profileNames.map(
|
||||
(profile) => web.raw`<option
|
||||
class="select-option"
|
||||
value="${web.escape(profile)}"
|
||||
${profile === profileName ? 'selected' : ''}
|
||||
>${web.escape(profile)}</option>`
|
||||
),
|
||||
$select = web.html`<select class="input">
|
||||
<option class="select-option" value="--">-- new --</option>
|
||||
${$options.join('')}
|
||||
</select>`,
|
||||
$edit = web.html`<input
|
||||
type="text"
|
||||
class="input"
|
||||
value="${web.escape(profileName)}"
|
||||
pattern="/^[A-Za-z0-9_-]+$/"
|
||||
>`,
|
||||
$export = web.html`<button class="profile-export">
|
||||
${await components.feather('download', { class: 'profile-icon-action' })}
|
||||
</button>`,
|
||||
$import = web.html`<label class="profile-import">
|
||||
<input type="file" class="hidden" accept="application/json">
|
||||
${await components.feather('upload', { class: 'profile-icon-action' })}
|
||||
</label>`,
|
||||
$save = web.html`<button class="profile-save">
|
||||
${await components.feather('save', { class: 'profile-icon-text' })} Save
|
||||
</button>`,
|
||||
$delete = web.html`<button class="profile-delete">
|
||||
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
|
||||
</button>`,
|
||||
$error = web.html`<p class="profile-error"></p>`;
|
||||
|
||||
$export.addEventListener('click', async (_event) => {
|
||||
const now = new Date(),
|
||||
$a = web.html`<a
|
||||
class="hidden"
|
||||
download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()}.json"
|
||||
href="data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2)
|
||||
)}"
|
||||
></a>`;
|
||||
web.render(document.body, $a);
|
||||
$a.click();
|
||||
$a.remove();
|
||||
});
|
||||
|
||||
$import.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader();
|
||||
reader.onload = async (progress) => {
|
||||
try {
|
||||
const profileUpload = JSON.parse(progress.currentTarget.result);
|
||||
if (!profileUpload) throw Error;
|
||||
await storage.set(['profiles', $select.value], profileUpload);
|
||||
env.reload();
|
||||
} catch {
|
||||
web.render(web.empty($error), 'Invalid JSON uploaded.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
$select.addEventListener('change', (_event) => {
|
||||
if ($select.value === '--') {
|
||||
$edit.value = '';
|
||||
} else $edit.value = $select.value;
|
||||
});
|
||||
|
||||
$save.addEventListener('click', async (_event) => {
|
||||
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
`The profile "${web.escape($edit.value)}" already exists.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!$edit.value || !$edit.value.match(/^[A-Za-z0-9_-]+$/)) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
'Profile names may not be empty & may only contain letters, numbers, hyphens and underscores.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
await storage.set(['currentprofile'], $edit.value);
|
||||
if ($select.value === '--') {
|
||||
await storage.set(['profiles', $edit.value], {});
|
||||
} else if ($select.value !== $edit.value) {
|
||||
await storage.set(
|
||||
['profiles', $edit.value],
|
||||
await storage.get(['profiles', $select.value], {})
|
||||
);
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
}
|
||||
env.reload();
|
||||
});
|
||||
|
||||
$delete.addEventListener('click', async (_event) => {
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
await storage.set(
|
||||
['currentprofile'],
|
||||
profileNames.find((profile) => profile !== $select.value) || 'default'
|
||||
);
|
||||
env.reload();
|
||||
});
|
||||
|
||||
_$profileConfig = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.html`<p class="options-placeholder">
|
||||
Profiles are used to switch entire configurations.<br>
|
||||
Be careful - deleting a profile deletes all configuration
|
||||
related to it.<br>
|
||||
</p>`,
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$select,
|
||||
web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$edit,
|
||||
web.html`${await components.feather('type', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<p class="profile-actions"></p>`,
|
||||
$export,
|
||||
$import,
|
||||
$save,
|
||||
$delete
|
||||
),
|
||||
$error
|
||||
);
|
||||
}
|
||||
web.render(web.empty($options), _$profileConfig);
|
||||
};
|
||||
$profile.addEventListener('click', () => openSidebarMenu('profile'));
|
||||
|
||||
// mods
|
||||
|
||||
const $modLists = {},
|
||||
generators = {
|
||||
options: async (mod) => {
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (const opt of mod.options) {
|
||||
if (!opt.environments.includes(env.name)) continue;
|
||||
web.render($fragment, await options[opt.type](mod, opt));
|
||||
}
|
||||
if (!mod.options.length) {
|
||||
web.render($fragment, web.html`<p class="options-placeholder">No options.</p>`);
|
||||
}
|
||||
return $fragment;
|
||||
},
|
||||
mod: async (mod) => {
|
||||
const $mod = web.html`<div class="mod" data-id="${web.escape(mod.id)}"></div>`,
|
||||
$toggle = modComponents.toggle('', await registry.enabled(mod.id));
|
||||
$toggle.addEventListener('change', async (event) => {
|
||||
if (event.target.checked && mod.tags.includes('theme')) {
|
||||
const mode = mod.tags.includes('light') ? 'light' : 'dark',
|
||||
id = mod.id,
|
||||
mods = await registry.list(
|
||||
async (mod) =>
|
||||
(await registry.enabled(mod.id)) &&
|
||||
mod.tags.includes('theme') &&
|
||||
mod.tags.includes(mode) &&
|
||||
mod.id !== id
|
||||
);
|
||||
for (const mod of mods) {
|
||||
profileDB.set(['_mods', mod.id], false);
|
||||
document.querySelector(
|
||||
`[data-id="${web.escape(mod.id)}"] .toggle-check`
|
||||
).checked = false;
|
||||
}
|
||||
}
|
||||
profileDB.set(['_mods', mod.id], event.target.checked);
|
||||
notifications.onChange();
|
||||
});
|
||||
$mod.addEventListener('click', () => openSidebarMenu(mod.id));
|
||||
return web.render(
|
||||
web.html`<article class="mod-container"></article>`,
|
||||
web.render(
|
||||
$mod,
|
||||
mod.preview
|
||||
? modComponents.preview(
|
||||
mod.preview.startsWith('http')
|
||||
? mod.preview
|
||||
: fs.localPath(`repo/${mod._dir}/${mod.preview}`)
|
||||
)
|
||||
: '',
|
||||
web.render(
|
||||
web.html`<div class="mod-body"></div>`,
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
modComponents.description(mod.description),
|
||||
modComponents.authors(mod.authors),
|
||||
mod.environments.includes(env.name) && !registry.core.includes(mod.id)
|
||||
? $toggle
|
||||
: ''
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
modList: async (category, message = '') => {
|
||||
if (!$modLists[category]) {
|
||||
const $search = web.html`<input type="search" class="search"
|
||||
placeholder="Search ('/' to focus)">`,
|
||||
$list = web.html`<div class="mods-list"></div>`,
|
||||
mods = await registry.list(
|
||||
(mod) => mod.environments.includes(env.name) && mod.tags.includes(category)
|
||||
);
|
||||
web.addHotkeyListener(['/'], () => $search.focus());
|
||||
$search.addEventListener('input', (_event) => {
|
||||
const query = $search.value.toLowerCase();
|
||||
for (const $mod of $list.children) {
|
||||
const matches = !query || $mod.innerText.toLowerCase().includes(query);
|
||||
$mod.classList[matches ? 'remove' : 'add']('hidden');
|
||||
}
|
||||
});
|
||||
for (const mod of mods) {
|
||||
mod.tags = mod.tags.filter((tag) => tag !== category);
|
||||
web.render($list, await generators.mod(mod));
|
||||
mod.tags.unshift(category);
|
||||
}
|
||||
$modLists[category] = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.render(
|
||||
web.html`<label class="search-container"></label>`,
|
||||
$search,
|
||||
web.html`${await components.feather('search', { class: 'input-icon' })}`
|
||||
),
|
||||
message ? web.render(web.html`<p class="main-message"></p>`, message) : '',
|
||||
$list
|
||||
);
|
||||
}
|
||||
return $modLists[category];
|
||||
},
|
||||
};
|
||||
|
||||
async function openModMenu(id) {
|
||||
let $mod;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
$mod = $list.querySelector(`[data-id="${web.escape(id)}"]`);
|
||||
if ($mod) break;
|
||||
}
|
||||
const mod = await registry.get(id);
|
||||
if (!$mod || !mod || $mod.className === 'mod-selected') return;
|
||||
|
||||
$mod.className = 'mod-selected';
|
||||
const fragment = [
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
await generators.options(mod),
|
||||
];
|
||||
web.render(web.empty($options), ...fragment);
|
||||
}
|
||||
|
||||
// views
|
||||
|
||||
const $notionNavItem = web.html`<h1 class="nav-notion">
|
||||
${(await fs.getText('media/colour.svg')).replace(
|
||||
/width="\d+" height="\d+"/,
|
||||
`class="nav-notion-icon"`
|
||||
)}
|
||||
<span>notion-enhancer</span>
|
||||
</h1>`;
|
||||
$notionNavItem.addEventListener('click', env.focusNotion);
|
||||
|
||||
const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`,
|
||||
$extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
|
||||
$themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`,
|
||||
$integrationsNavItem = web.html`<a href="?view=integrations" class="nav-item">integrations</a>`,
|
||||
$changelogNavItem = web.html`<button class="nav-item nav-changelog">
|
||||
${await components.feather('clock', { class: 'nav-changelog-icon' })}
|
||||
</button>`;
|
||||
components.addTooltip($changelogNavItem, '**Update changelog & welcome message**');
|
||||
$changelogNavItem.addEventListener('click', () => {
|
||||
$changelogModal.scrollTop = 0;
|
||||
$changelogModal.classList.add('modal-visible');
|
||||
});
|
||||
|
||||
web.render(
|
||||
document.body,
|
||||
web.render(
|
||||
web.html`<div class="body-container"></div>`,
|
||||
web.render(
|
||||
web.html`<div class="content-container"></div>`,
|
||||
web.render(
|
||||
web.html`<nav class="nav"></nav>`,
|
||||
$notionNavItem,
|
||||
$coreNavItem,
|
||||
$extensionsNavItem,
|
||||
$themesNavItem,
|
||||
$integrationsNavItem,
|
||||
web.html`<a href="https://notion-enhancer.github.io" target="_blank" class="nav-item">docs</a>`,
|
||||
web.html`<a href="https://discord.gg/sFWPXtA" target="_blank" class="nav-item">community</a>`,
|
||||
$changelogNavItem
|
||||
),
|
||||
$main
|
||||
),
|
||||
web.render($sidebar, $profile, $options)
|
||||
)
|
||||
);
|
||||
|
||||
function selectNavItem($item) {
|
||||
for (const $selected of document.querySelectorAll('.nav-item-selected')) {
|
||||
$selected.className = 'nav-item';
|
||||
}
|
||||
$item.className = 'nav-item-selected';
|
||||
}
|
||||
|
||||
await generators.modList(
|
||||
'core',
|
||||
`Core mods provide the basics required for
|
||||
all other extensions and themes to work. They
|
||||
can't be disabled, but they can be configured
|
||||
- just click on a mod to access its options.`
|
||||
);
|
||||
router.addView('core', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($coreNavItem);
|
||||
return web.render($main, await generators.modList('core'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'extension',
|
||||
`Extensions build on the functionality and layout of
|
||||
the Notion client, modifying and interacting with
|
||||
existing interfaces.`
|
||||
);
|
||||
router.addView('extensions', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($extensionsNavItem);
|
||||
return web.render($main, await generators.modList('extension'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'theme',
|
||||
`Themes change Notion's colour scheme.
|
||||
Dark themes will only work when Notion is in dark mode,
|
||||
and light themes will only work when Notion is in light mode.
|
||||
Only one theme of each mode can be enabled at a time.`
|
||||
);
|
||||
router.addView('themes', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($themesNavItem);
|
||||
return web.render($main, await generators.modList('theme'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'integration',
|
||||
web.html`<span class="danger">Integrations are extensions that use an unofficial API
|
||||
to access and modify content. They are used just like
|
||||
normal extensions, but may be more dangerous to use.</span>`
|
||||
);
|
||||
router.addView('integrations', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($integrationsNavItem);
|
||||
return web.render($main, await generators.modList('integration'));
|
||||
});
|
||||
|
||||
router.setDefaultView('extensions');
|
||||
|
||||
router.addQueryListener('id', openSidebarMenu);
|
||||
function openSidebarMenu(id) {
|
||||
if (!id) return;
|
||||
id = web.escape(id);
|
||||
|
||||
const deselectedMods = `.mod-selected:not([data-id="${id}"])`;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
for (const $selected of $list.querySelectorAll(deselectedMods)) {
|
||||
$selected.className = 'mod';
|
||||
}
|
||||
}
|
||||
router.updateQuery(`?id=${id}`);
|
||||
|
||||
if (id === 'profile') {
|
||||
openProfileMenu();
|
||||
} else openModMenu(id);
|
||||
}
|
||||
})();
|
@ -20,7 +20,7 @@
|
||||
"type": "hotkey",
|
||||
"key": "openMenuHotkey",
|
||||
"description": "Opens the notion-enhancer menu from within Notion.",
|
||||
"value": "CmdOrCtrl+Shift+,"
|
||||
"value": "Ctrl+Shift+Comma"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
@ -52,10 +52,11 @@
|
||||
"type": "toggle",
|
||||
"key": "debugMode",
|
||||
"description": "Activates built-in debugging tools accessible through the application menu.",
|
||||
"targets": ["darwin", "win32", "linux"],
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"clientStyles": ["client.css"],
|
||||
"clientStyles": ["client.css", "variables.css"],
|
||||
"clientScripts": ["client.mjs"],
|
||||
"electronScripts": []
|
||||
}
|
||||
|
1997
src/core/theme.css
@ -42,16 +42,16 @@ body.dark {
|
||||
--theme--bg-purple: rgb(73, 47, 100);
|
||||
--theme--bg-pink: rgb(105, 49, 76);
|
||||
--theme--bg-red: rgb(110, 54, 48);
|
||||
--theme--bg_dim-light_gray: rgb(28, 28, 28);
|
||||
--theme--bg_dim-gray: rgb(32, 32, 32);
|
||||
--theme--bg_dim-brown: rgb(35, 30, 28);
|
||||
--theme--bg_dim-orange: rgb(37, 31, 27);
|
||||
--theme--bg_dim-yellow: rgb(35, 31, 26);
|
||||
--theme--bg_dim-green: rgb(29, 34, 32);
|
||||
--theme--bg_dim-blue: rgb(27, 31, 34);
|
||||
--theme--bg_dim-purple: rgb(31, 29, 33);
|
||||
--theme--bg_dim-pink: rgb(35, 28, 31);
|
||||
--theme--bg_dim-red: rgb(36, 30, 29);
|
||||
--theme--dim-light_gray: rgb(28, 28, 28);
|
||||
--theme--dim-gray: rgb(32, 32, 32);
|
||||
--theme--dim-brown: rgb(35, 30, 28);
|
||||
--theme--dim-orange: rgb(37, 31, 27);
|
||||
--theme--dim-yellow: rgb(35, 31, 26);
|
||||
--theme--dim-green: rgb(29, 34, 32);
|
||||
--theme--dim-blue: rgb(27, 31, 34);
|
||||
--theme--dim-purple: rgb(31, 29, 33);
|
||||
--theme--dim-pink: rgb(35, 28, 31);
|
||||
--theme--dim-red: rgb(36, 30, 29);
|
||||
|
||||
--theme--accent-primary: rgb(35, 131, 226);
|
||||
--theme--accent-primary_hover: rgb(0, 117, 211);
|
||||
@ -139,16 +139,16 @@ body:not(.dark) {
|
||||
--theme--bg-purple: rgb(232, 222, 238);
|
||||
--theme--bg-pink: rgb(245, 224, 233);
|
||||
--theme--bg-red: rgb(255, 226, 221);
|
||||
--theme--bg_dim-light_gray: rgba(249, 249, 245, 0.5);
|
||||
--theme--bg_dim-gray: rgba(247, 247, 245, 0.7);
|
||||
--theme--bg_dim-brown: rgba(250, 246, 245, 0.7);
|
||||
--theme--bg_dim-orange: rgba(252, 245, 242, 0.7);
|
||||
--theme--bg_dim-yellow: rgba(250, 247, 237, 0.7);
|
||||
--theme--bg_dim-green: rgba(244, 248, 243, 0.7);
|
||||
--theme--bg_dim-blue: rgba(241, 248, 251, 0.7);
|
||||
--theme--bg_dim-purple: rgba(249, 246, 252, 0.7);
|
||||
--theme--bg_dim-pink: rgba(251, 245, 251, 0.7);
|
||||
--theme--bg_dim-red: rgba(253, 245, 243, 0.7);
|
||||
--theme--dim-light_gray: rgba(249, 249, 245, 0.5);
|
||||
--theme--dim-gray: rgba(247, 247, 245, 0.7);
|
||||
--theme--dim-brown: rgba(250, 246, 245, 0.7);
|
||||
--theme--dim-orange: rgba(252, 245, 242, 0.7);
|
||||
--theme--dim-yellow: rgba(250, 247, 237, 0.7);
|
||||
--theme--dim-green: rgba(244, 248, 243, 0.7);
|
||||
--theme--dim-blue: rgba(241, 248, 251, 0.7);
|
||||
--theme--dim-purple: rgba(249, 246, 252, 0.7);
|
||||
--theme--dim-pink: rgba(251, 245, 251, 0.7);
|
||||
--theme--dim-red: rgba(253, 245, 243, 0.7);
|
||||
|
||||
--theme--accent-primary: rgb(35, 131, 226);
|
||||
--theme--accent-primary_hover: rgb(0, 117, 211);
|
||||
|
@ -1,43 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
require("./api.cjs");
|
||||
require("../common/api.js");
|
||||
|
||||
module.exports = async (target, __exports, __eval) => {
|
||||
const {
|
||||
getMods,
|
||||
getProfile,
|
||||
isEnabled,
|
||||
enhancerRequire,
|
||||
enhancerUrl,
|
||||
initDatabase,
|
||||
} = globalThis.__enhancerApi;
|
||||
|
||||
// clientScripts
|
||||
if (target === "renderer/preload") {
|
||||
document.addEventListener("readystatechange", (event) => {
|
||||
if (document.readyState !== "complete") return false;
|
||||
const $script = document.createElement("script");
|
||||
$script.type = "module";
|
||||
$script.src = enhancerUrl("common/loader.mjs");
|
||||
document.head.appendChild($script);
|
||||
});
|
||||
}
|
||||
|
||||
// electronScripts
|
||||
for (const mod of await getMods()) {
|
||||
if (!mod.electronScripts || !(await isEnabled(mod.id))) continue;
|
||||
for (const { source, target: targetScript } of mod.electronScripts) {
|
||||
if (`${target}.js` !== targetScript) continue;
|
||||
const script = enhancerRequire(`${mod._src}/${source}`),
|
||||
db = initDatabase([await getProfile(), mod.id]);
|
||||
script(globalThis.__enhancerApi, db, __exports, __eval);
|
||||
}
|
||||
}
|
||||
};
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |