mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-04 12:49:03 +00:00
refactor: simplifications
- remove registry validation - separate core from mods - use __enhancerApi global for consistent api access - use key-based namespacing instead of nested objects
This commit is contained in:
parent
44702af188
commit
d304f698a8
55
bin.mjs
55
bin.mjs
@ -141,8 +141,10 @@ const printHelp = (commands, options) => {
|
||||
} else {
|
||||
const cmdPad = Math.max(...commands.map(([cmd]) => cmd.length)),
|
||||
optPad = Math.max(...options.map((opt) => opt[0].length)),
|
||||
parseCmd = (cmd) => chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
|
||||
parseOpt = (opt) => chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
|
||||
parseCmd = (cmd) =>
|
||||
chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
|
||||
parseOpt = (opt) =>
|
||||
chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
|
||||
print`{bold.whiteBright ${name} v${version}}\n{grey ${homepage}}
|
||||
\n{bold.whiteBright USAGE}\n${name} <command> [options]
|
||||
\n{bold.whiteBright COMMANDS}\n${commands.map(parseCmd).join("\n")}
|
||||
@ -179,11 +181,26 @@ try {
|
||||
"--path=</path/to/notion/resources>",
|
||||
[String, "manually provide a notion installation location"],
|
||||
],
|
||||
["--overwrite", [Boolean, "for rapid development; unsafely overwrite sources"]],
|
||||
["--no-backup", [Boolean, "skip backup; enhancement will be faster but irreversible"]],
|
||||
["-y, --yes", [Boolean, 'skip prompts; assume "yes" and run non-interactively']],
|
||||
["-n, --no", [Boolean, 'skip prompts; assume "no" and run non-interactively']],
|
||||
["-q, --quiet", [Boolean, 'skip prompts; assume "no" unless -y and hide all output']],
|
||||
[
|
||||
"--overwrite",
|
||||
[Boolean, "for rapid development; unsafely overwrite sources"],
|
||||
],
|
||||
[
|
||||
"--no-backup",
|
||||
[Boolean, "skip backup; enhancement will be faster but irreversible"],
|
||||
],
|
||||
[
|
||||
"-y, --yes",
|
||||
[Boolean, 'skip prompts; assume "yes" and run non-interactively'],
|
||||
],
|
||||
[
|
||||
"-n, --no",
|
||||
[Boolean, 'skip prompts; assume "no" and run non-interactively'],
|
||||
],
|
||||
[
|
||||
"-q, --quiet",
|
||||
[Boolean, 'skip prompts; assume "no" unless -y and hide all output'],
|
||||
],
|
||||
["-d, --debug", [Boolean, "show detailed error messages"]],
|
||||
["-j, --json", [Boolean, "display json output (where applicable)"]],
|
||||
["-h, --help", [Boolean, "display usage information"]],
|
||||
@ -259,16 +276,20 @@ try {
|
||||
await applyEnhancements();
|
||||
stopSpinner();
|
||||
print` {grey * ${messages["version-applied"]}}\n`;
|
||||
} else print` {grey * ${messages["notion-found"]}: ${messages["version-applied"]}}\n`;
|
||||
} else {
|
||||
print` {grey * ${messages["notion-found"]}: ${messages["version-applied"]}}\n`;
|
||||
}
|
||||
return SUCCESS;
|
||||
}
|
||||
if (insertVersion && insertVersion !== manifest.version) {
|
||||
// diff version already applied
|
||||
print` {grey * ${messages["notion-found"]}: ${messages["version-mismatch"]}}\n`;
|
||||
const replaceEnhancements = //
|
||||
["Y", "y"].includes(await promptConfirmation(messages["prompt-version-replace"]));
|
||||
// prettier-ignore
|
||||
const promptReplacement = await promptConfirmation(messages["prompt-version-replace"]);
|
||||
print`\n`;
|
||||
return replaceEnhancements ? await interactiveRestoreBackup() : CANCELLED;
|
||||
return ["Y", "y"].includes(promptReplacement)
|
||||
? await interactiveRestoreBackup()
|
||||
: CANCELLED;
|
||||
} else return INCOMPLETE;
|
||||
},
|
||||
interactiveApplyEnhancements = async () => {
|
||||
@ -304,7 +325,9 @@ try {
|
||||
return FAILURE;
|
||||
} else if (insertVersion) {
|
||||
print` {grey * ${messages["notion-found"]}: ${messages["version-applied"]}}\n`;
|
||||
return (await interactiveRestoreBackup()) === INCOMPLETE ? SUCCESS : FAILURE;
|
||||
return (await interactiveRestoreBackup()) === INCOMPLETE
|
||||
? SUCCESS
|
||||
: FAILURE;
|
||||
}
|
||||
print` {grey * ${messages["notion-found"]}: ${messages["not-applied"]}}\n`;
|
||||
return SUCCESS;
|
||||
@ -312,7 +335,9 @@ try {
|
||||
promptConfigRemoval = async () => {
|
||||
if (existsSync(configPath)) {
|
||||
print` {grey * ${messages["config-found"]}: ${configPath}}\n`;
|
||||
if (["Y", "y"].includes(await promptConfirmation(messages["prompt-config-removal"]))) {
|
||||
// prettier-ignore
|
||||
const promptRemoval = await promptConfirmation(messages["prompt-config-removal"]);
|
||||
if (["Y", "y"].includes(promptRemoval)) {
|
||||
print` `;
|
||||
startSpinner();
|
||||
await removeConfig();
|
||||
@ -371,5 +396,7 @@ try {
|
||||
.splice(1)
|
||||
.map((at) => at.replace(/\s{4}/g, " "))
|
||||
.join("\n")}}`;
|
||||
} else print`{bold.red Error:} ${message} {grey (run with -d for more information)}\n`;
|
||||
} else {
|
||||
print`{bold.red Error:} ${message} {grey (run with -d for more information)}\n`;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ let __notionResources, __enhancerConfig;
|
||||
const nodeRequire = createRequire(import.meta.url),
|
||||
manifest = nodeRequire("../package.json"),
|
||||
platform =
|
||||
process.platform === "linux" && os.release().toLowerCase().includes("microsoft")
|
||||
process.platform === "linux" &&
|
||||
os.release().toLowerCase().includes("microsoft")
|
||||
? "wsl"
|
||||
: process.platform,
|
||||
polyfillWslEnv = (name) => {
|
||||
@ -55,7 +56,6 @@ const nodeRequire = createRequire(import.meta.url),
|
||||
if (stat.isDirectory()) {
|
||||
files = files.concat(await readdirDeep(file));
|
||||
} else if (stat.isSymbolicLink()) {
|
||||
//
|
||||
} else files.push(file);
|
||||
}
|
||||
return files;
|
||||
@ -70,17 +70,19 @@ const setNotionPath = (path) => {
|
||||
if (__notionResources) return resolve(`${__notionResources}/${path}`);
|
||||
polyfillWslEnv("LOCALAPPDATA");
|
||||
polyfillWslEnv("PROGRAMW6432");
|
||||
const potentialPaths = [
|
||||
// [["targeted", "platforms"], "/path/to/notion/resources"]
|
||||
[["darwin"], `/Users/${process.env.USER}/Applications/Notion.app/Contents/Resources`],
|
||||
[["darwin"], "/Applications/Notion.app/Contents/Resources"],
|
||||
[["win32", "wsl"], resolve(`${process.env.LOCALAPPDATA}/Programs/Notion/resources`)],
|
||||
[["win32", "wsl"], resolve(`${process.env.PROGRAMW6432}/Notion/resources`)],
|
||||
// https://aur.archlinux.org/packages/notion-app/
|
||||
[["linux"], "/opt/notion-app"],
|
||||
];
|
||||
for (const [targetPlatforms, testPath] of potentialPaths) {
|
||||
if (!targetPlatforms.includes(platform)) continue;
|
||||
const potentialPaths = {
|
||||
win32: [
|
||||
resolve(`${process.env.LOCALAPPDATA}/Programs/Notion/resources`),
|
||||
resolve(`${process.env.PROGRAMW6432}/Notion/resources`),
|
||||
],
|
||||
darwin: [
|
||||
`/Users/${process.env.USER}/Applications/Notion.app/Contents/Resources`,
|
||||
"/Applications/Notion.app/Contents/Resources",
|
||||
],
|
||||
linux: ["/opt/notion-app"],
|
||||
};
|
||||
potentialPaths["wsl"] = potentialPaths["win32"];
|
||||
for (const testPath of potentialPaths[platform]) {
|
||||
if (!existsSync(testPath)) continue;
|
||||
__notionResources = testPath;
|
||||
return resolve(`${__notionResources}/${path}`);
|
||||
@ -88,7 +90,8 @@ const setNotionPath = (path) => {
|
||||
},
|
||||
// prefer unpacked if both exist
|
||||
getAppPath = () => ["app", "app.asar"].map(getResourcePath).find(existsSync),
|
||||
getBackupPath = () => ["app.bak", "app.asar.bak"].map(getResourcePath).find(existsSync),
|
||||
getBackupPath = () =>
|
||||
["app.bak", "app.asar.bak"].map(getResourcePath).find(existsSync),
|
||||
getConfigPath = () => {
|
||||
if (__enhancerConfig) return __enhancerConfig;
|
||||
const home = platform === "wsl" ? polyfillWslEnv("HOMEPATH") : os.homedir();
|
||||
@ -96,6 +99,7 @@ const setNotionPath = (path) => {
|
||||
return __enhancerConfig;
|
||||
},
|
||||
checkEnhancementVersion = () => {
|
||||
// prettier-ignore
|
||||
const manifestPath = getResourcePath("app/node_modules/notion-enhancer/package.json");
|
||||
if (!existsSync(manifestPath)) return undefined;
|
||||
const insertVersion = nodeRequire(manifestPath).version;
|
||||
@ -127,8 +131,10 @@ const unpackApp = async () => {
|
||||
filter: (_, dest) => !excludedDests.includes(dest),
|
||||
});
|
||||
// call patch-desktop-app.mjs on each file
|
||||
const notionScripts = (await readdirDeep(appPath)).filter((file) => file.endsWith(".js")),
|
||||
scriptUpdates = [];
|
||||
// prettier-ignore
|
||||
const notionScripts = (await readdirDeep(appPath))
|
||||
.filter((file) => file.endsWith(".js")),
|
||||
scriptUpdates = [];
|
||||
for (const file of notionScripts) {
|
||||
const scriptId = file.slice(appPath.length + 1, -3).replace(/\\/g, "/"),
|
||||
scriptContent = await fsp.readFile(file, { encoding: "utf8" }),
|
||||
@ -137,17 +143,18 @@ const unpackApp = async () => {
|
||||
if (changesMade) scriptUpdates.push(fsp.writeFile(file, patchedContent));
|
||||
}
|
||||
// create package.json
|
||||
// prettier-ignore
|
||||
const manifestPath = getResourcePath("app/node_modules/notion-enhancer/package.json"),
|
||||
insertManifest = { ...manifest, main: "electron/init.cjs" };
|
||||
jsManifest = { ...manifest, main: "electron/init.js" };
|
||||
// remove cli-specific fields
|
||||
delete insertManifest.bin;
|
||||
delete insertManifest.type;
|
||||
delete insertManifest.scripts;
|
||||
delete insertManifest.engines;
|
||||
delete insertManifest.packageManager;
|
||||
delete insertManifest.dependencies;
|
||||
delete insertManifest.devDependencies;
|
||||
scriptUpdates.push(fsp.writeFile(manifestPath, JSON.stringify(insertManifest)));
|
||||
delete jsManifest.bin;
|
||||
delete jsManifest.type;
|
||||
delete jsManifest.scripts;
|
||||
delete jsManifest.engines;
|
||||
delete jsManifest.packageManager;
|
||||
delete jsManifest.dependencies;
|
||||
const jsonManifest = JSON.stringify(jsManifest);
|
||||
scriptUpdates.push(fsp.writeFile(manifestPath, jsonManifest));
|
||||
await Promise.all(scriptUpdates);
|
||||
return true;
|
||||
},
|
||||
|
@ -6,7 +6,8 @@
|
||||
|
||||
const patches = {
|
||||
"*": async (scriptId, scriptContent) => {
|
||||
const prevTriggerFound = /require\(['|"]notion-enhancer['|"]\)/.test(scriptContent);
|
||||
const prevTriggerPattern = /require\(['|"]notion-enhancer['|"]\)/,
|
||||
prevTriggerFound = prevTriggerPattern.test(scriptContent);
|
||||
if (prevTriggerFound) return scriptContent;
|
||||
const enhancerTrigger =
|
||||
'\n\n/*notion-enhancer*/require("notion-enhancer")' +
|
||||
@ -18,6 +19,7 @@ const patches = {
|
||||
// https://github.com/notion-enhancer/desktop/issues/160
|
||||
// enable the notion:// url scheme/protocol on linux
|
||||
const searchValue = /process.platform === "win32"/g,
|
||||
// prettier-ignore
|
||||
replaceValue = 'process.platform === "win32" || process.platform === "linux"';
|
||||
if (scriptContent.includes(replaceValue)) return scriptContent;
|
||||
return scriptContent.replace(searchValue, replaceValue);
|
||||
@ -36,7 +38,7 @@ const patches = {
|
||||
fileExt = pathname.split(".").reverse()[0],
|
||||
filePath = \`../node_modules/notion-enhancer/\${req.url.slice(
|
||||
schemePrefix.length,
|
||||
-(search.length + hash.length)
|
||||
-(search.length + hash.length) || undefined
|
||||
)}\`;
|
||||
callback({
|
||||
data: require("fs").createReadStream(require("path").resolve(\`\${__dirname}/\${filePath}\`)),
|
||||
|
@ -9,29 +9,35 @@ import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const dependencies = [
|
||||
["twind.min.js", "https://cdn.twind.style"],
|
||||
["lucide.min.js", "https://unpkg.com/lucide@0.104.0/dist/umd/lucide.min.js"],
|
||||
["jscolor.min.js", "https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.5.1/jscolor.min.js"],
|
||||
];
|
||||
const dependencies = {
|
||||
"twind.min.js": "https://cdn.twind.style",
|
||||
"lucide.min.js": "https://unpkg.com/lucide@0.104.0/dist/umd/lucide.min.js",
|
||||
"jscolor.min.js":
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.5.1/jscolor.min.js",
|
||||
};
|
||||
|
||||
const output = fileURLToPath(new URL("../src/vendor", import.meta.url));
|
||||
const output = fileURLToPath(new URL("../src/vendor", import.meta.url)),
|
||||
write = (file, data) => fsp.writeFile(resolve(`${output}/${file}`), data);
|
||||
if (existsSync(output)) await fsp.rm(output, { recursive: true });
|
||||
await fsp.mkdir(output);
|
||||
for (const [file, source] of dependencies) {
|
||||
const res = await (await fetch(source)).text();
|
||||
await fsp.writeFile(resolve(`${output}/${file}`), res);
|
||||
for (const file in dependencies) {
|
||||
const source = dependencies[file],
|
||||
res = await (await fetch(source)).text();
|
||||
await write(file, res);
|
||||
}
|
||||
|
||||
// build content type lookup script from mime-db to avoid
|
||||
// re-processing entire the database every time a file is
|
||||
// requested via notion://www.notion.so/__notion-enhancer/
|
||||
const mimeTypes = await (await fetch("https://unpkg.com/mime-db@1.52.0/db.json")).json(),
|
||||
contentTypes = [];
|
||||
for (const [type, { extensions, charset }] of Object.entries(mimeTypes)) {
|
||||
let contentTypes = [];
|
||||
for (const [type, { extensions, charset }] of Object.entries(
|
||||
await (await fetch("https://unpkg.com/mime-db@1.52.0/db.json")).json()
|
||||
)) {
|
||||
if (!extensions) continue;
|
||||
const contentType = charset ? `${type}; charset=${charset.toLowerCase()}` : type;
|
||||
const contentType = charset
|
||||
? `${type}; charset=${charset.toLowerCase()}`
|
||||
: type;
|
||||
for (const ext of extensions) contentTypes.push([ext, contentType]);
|
||||
}
|
||||
const encodedContentTypes = `module.exports=new Map(${JSON.stringify(contentTypes)});`;
|
||||
await fsp.writeFile(resolve(`${output}/content-types.min.js`), encodedContentTypes);
|
||||
contentTypes = `module.exports=new Map(${JSON.stringify(contentTypes)});`;
|
||||
await write("content-types.min.js", contentTypes);
|
||||
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,5 +0,0 @@
|
||||
# notion-enhancer/extension
|
||||
|
||||
an enhancer/customiser for the all-in-one productivity workspace notion.so (browser)
|
||||
|
||||
[read the docs online](https://notion-enhancer.github.io/)
|
67
src/browser/api.js
Normal file
67
src/browser/api.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const platform = "browser",
|
||||
enhancerVersion = chrome.runtime.getManifest().version,
|
||||
enhancerUrl = (target) => chrome.runtime.getURL(target);
|
||||
|
||||
const readFile = async (file) => {
|
||||
file = file.startsWith("http") ? file : enhancerUrl(file);
|
||||
const res = await fetch(file);
|
||||
return await res.text();
|
||||
},
|
||||
readJson = async (file) => {
|
||||
file = file.startsWith("http") ? file : enhancerUrl(file);
|
||||
const res = await fetch(file);
|
||||
return await res.json();
|
||||
},
|
||||
reloadApp = () => chrome.runtime.sendMessage({ action: "reload" });
|
||||
|
||||
const initDatabase = (namespace) => {
|
||||
if (Array.isArray(namespace)) namespace = namespace.join("__");
|
||||
namespace = namespace ? namespace + "__" : "";
|
||||
return {
|
||||
get: async (key) => {
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
return new Promise((res, _rej) => {
|
||||
chrome.storage.local.get(key, (value) => res(value));
|
||||
});
|
||||
},
|
||||
set: async (key, value) => {
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
return new Promise((res, _rej) => {
|
||||
chrome.storage.local.set({ [key]: value }, () => res(value));
|
||||
});
|
||||
},
|
||||
dump: async () => {
|
||||
const obj = await new Promise((res, _rej) => {
|
||||
chrome.storage.local.get((value) => res(value));
|
||||
});
|
||||
if (!namespace) return obj;
|
||||
let entries = Object.entries(obj);
|
||||
entries = entries.filter(([key]) => key.startsWith(`${namespace}__`));
|
||||
return Object.fromEntries(entries);
|
||||
},
|
||||
populate: async (obj) => {
|
||||
return new Promise((res, _rej) => {
|
||||
chrome.storage.local.set(obj, () => res(value));
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, {
|
||||
platform,
|
||||
enhancerUrl,
|
||||
enhancerVersion,
|
||||
readFile,
|
||||
readJson,
|
||||
reloadApp,
|
||||
initDatabase,
|
||||
});
|
41
src/browser/env/env.mjs
vendored
41
src/browser/env/env.mjs
vendored
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** environment-specific methods and constants */
|
||||
|
||||
/**
|
||||
* the environment/platform name code is currently being executed in
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const name = 'extension';
|
||||
|
||||
/**
|
||||
* the current version of the enhancer
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const version = chrome.runtime.getManifest().version;
|
||||
|
||||
/**
|
||||
* open the enhancer's menu
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusMenu = () => chrome.runtime.sendMessage({ action: 'focusMenu' });
|
||||
|
||||
/**
|
||||
* focus an active notion tab
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
|
||||
|
||||
/**
|
||||
* reload all notion and enhancer menu tabs to apply changes
|
||||
* @type {function}
|
||||
*/
|
||||
export const reload = () => chrome.runtime.sendMessage({ action: 'reload' });
|
48
src/browser/env/fs.mjs
vendored
48
src/browser/env/fs.mjs
vendored
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** environment-specific file reading */
|
||||
|
||||
/**
|
||||
* transform a path relative to the enhancer root directory into an absolute path
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @returns {string} an absolute filepath
|
||||
*/
|
||||
export const localPath = chrome.runtime.getURL;
|
||||
|
||||
/**
|
||||
* fetch and parse a json file's contents
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @param {object=} opts - the second argument of a fetch() request
|
||||
* @returns {object} the json value of the requested file as a js object
|
||||
*/
|
||||
export const getJSON = (path, opts = {}) =>
|
||||
fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.json());
|
||||
|
||||
/**
|
||||
* fetch a text file's contents
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @param {object=} opts - the second argument of a fetch() request
|
||||
* @returns {string} the text content of the requested file
|
||||
*/
|
||||
export const getText = (path, opts = {}) =>
|
||||
fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.text());
|
||||
|
||||
/**
|
||||
* check if a file exists
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @returns {boolean} whether or not the file exists
|
||||
*/
|
||||
export const isFile = async (path) => {
|
||||
try {
|
||||
await fetch(path.startsWith('http') ? path : localPath(path));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
116
src/browser/env/storage.mjs
vendored
116
src/browser/env/storage.mjs
vendored
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/** environment-specific data persistence */
|
||||
|
||||
const _queue = [],
|
||||
_onChangeListeners = [];
|
||||
|
||||
/**
|
||||
* get persisted data
|
||||
* @param {string[]} path - the path of keys to the value being fetched
|
||||
* @param {unknown=} fallback - a default value if the path is not matched
|
||||
* @returns {Promise} value ?? fallback
|
||||
*/
|
||||
export const get = (path, fallback = undefined) => {
|
||||
if (!path.length) return fallback;
|
||||
return new Promise((res, rej) =>
|
||||
chrome.storage.local.get(async (values) => {
|
||||
let value = values;
|
||||
while (path.length) {
|
||||
if (value === undefined) {
|
||||
value = fallback;
|
||||
break;
|
||||
}
|
||||
value = value[path.shift()];
|
||||
}
|
||||
res(value ?? fallback);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* persist data
|
||||
* @param {string[]} path - the path of keys to the value being set
|
||||
* @param {unknown} value - the data to save
|
||||
* @returns {Promise} resolves when data has been saved
|
||||
*/
|
||||
export const set = (path, value) => {
|
||||
if (!path.length) return undefined;
|
||||
const precursor = _queue[_queue.length - 1] || undefined,
|
||||
interaction = new Promise(async (res, rej) => {
|
||||
if (precursor !== undefined) {
|
||||
await precursor;
|
||||
_queue.shift();
|
||||
}
|
||||
const pathClone = [...path],
|
||||
namespace = path[0];
|
||||
chrome.storage.local.get(async (values) => {
|
||||
let pointer = values,
|
||||
old;
|
||||
while (path.length) {
|
||||
const key = path.shift();
|
||||
if (!path.length) {
|
||||
old = pointer[key];
|
||||
pointer[key] = value;
|
||||
break;
|
||||
}
|
||||
pointer[key] = pointer[key] ?? {};
|
||||
pointer = pointer[key];
|
||||
}
|
||||
chrome.storage.local.set({ [namespace]: values[namespace] }, () => {
|
||||
_onChangeListeners.forEach((listener) =>
|
||||
listener({ path: pathClone, new: value, old })
|
||||
);
|
||||
res(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
_queue.push(interaction);
|
||||
return interaction;
|
||||
};
|
||||
|
||||
/**
|
||||
* create a wrapper for accessing a partition of the storage
|
||||
* @param {string[]} namespace - the path of keys to prefix all storage requests with
|
||||
* @param {function=} get - the storage get function to be wrapped
|
||||
* @param {function=} set - the storage set function to be wrapped
|
||||
* @returns {object} an object with the wrapped get/set functions
|
||||
*/
|
||||
export const db = (namespace, getFunc = get, setFunc = set) => {
|
||||
if (typeof namespace === 'string') namespace = [namespace];
|
||||
return {
|
||||
get: (path = [], fallback = undefined) => getFunc([...namespace, ...path], fallback),
|
||||
set: (path, value) => setFunc([...namespace, ...path], value),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* add an event listener for changes in storage
|
||||
* @param {onStorageChangeCallback} callback - called whenever a change in
|
||||
* storage is initiated from the current process
|
||||
*/
|
||||
export const addChangeListener = (callback) => {
|
||||
_onChangeListeners.push(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove a listener added with storage.addChangeListener
|
||||
* @param {onStorageChangeCallback} callback
|
||||
*/
|
||||
export const removeChangeListener = (callback) => {
|
||||
_onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback onStorageChangeCallback
|
||||
* @param {object} event
|
||||
* @param {string} event.path- the path of keys to the changed value
|
||||
* @param {string=} event.new - the new value being persisted to the store
|
||||
* @param {string=} event.old - the previous value associated with the key
|
||||
*/
|
@ -4,32 +4,34 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
(async () => {
|
||||
const site = location.host.endsWith('.notion.site'),
|
||||
page = location.pathname.split(/[/-]/g).reverse()[0].length === 32,
|
||||
whitelisted = ['/', '/onboarding'].includes(location.pathname),
|
||||
signedIn = localStorage['LRU:KeyValueStore2:current-user-id'];
|
||||
const enhancerApi = await import("./api.js");
|
||||
globalThis.__enhancerApi = enhancerApi;
|
||||
// const site = location.host.endsWith('.notion.site'),
|
||||
// page = location.pathname.split(/[/-]/g).reverse()[0].length === 32,
|
||||
// whitelisted = ['/', '/onboarding'].includes(location.pathname),
|
||||
// signedIn = localStorage['LRU:KeyValueStore2:current-user-id'];
|
||||
|
||||
if (site || page || (whitelisted && signedIn)) {
|
||||
const api = await import(chrome.runtime.getURL('api/index.mjs')),
|
||||
{ fs, registry, web } = api;
|
||||
// if (site || page || (whitelisted && signedIn)) {
|
||||
// const api = await import(chrome.runtime.getURL('api/index.mjs')),
|
||||
// { fs, registry, web } = api;
|
||||
|
||||
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
for (const sheet of mod.css?.client || []) {
|
||||
web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||
}
|
||||
for (let script of mod.js?.client || []) {
|
||||
script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
|
||||
script.default(api, await registry.db(mod.id));
|
||||
}
|
||||
}
|
||||
// for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
// for (const sheet of mod.css?.client || []) {
|
||||
// web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||
// }
|
||||
// for (let script of mod.js?.client || []) {
|
||||
// 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.log('[notion-enhancer] registry errors:');
|
||||
console.table(errors);
|
||||
}
|
||||
}
|
||||
// const errors = await registry.errors();
|
||||
// if (errors.length) {
|
||||
// console.log('[notion-enhancer] registry errors:');
|
||||
// console.table(errors);
|
||||
// }
|
||||
// }
|
||||
})();
|
||||
|
29
src/common/.github/workflows/update-parents.yml
vendored
29
src/common/.github/workflows/update-parents.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: 'update parent repositories'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: update parent
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ['notion-enhancer/extension', 'notion-enhancer/desktop']
|
||||
steps:
|
||||
- name: checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.CI_TOKEN }}
|
||||
submodules: true
|
||||
repository: ${{ matrix.repo }}
|
||||
- name: pull updates
|
||||
run: |
|
||||
git pull --recurse-submodules
|
||||
git submodule update --remote --recursive
|
||||
- name: commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: '[${{ github.event.repository.name }}] ${{ github.event.head_commit.message }}'
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -1,5 +0,0 @@
|
||||
# notion-enhancer/api
|
||||
|
||||
the standard api available within the notion-enhancer
|
||||
|
||||
[read the docs online](https://notion-enhancer.github.io/documentation/api)
|
@ -1,106 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* access to electron renderer apis
|
||||
* @namespace electron
|
||||
*/
|
||||
import * as _api from './index.mjs'; // trick jsdoc
|
||||
|
||||
/**
|
||||
* access to the electron BrowserWindow instance for the current window
|
||||
* see https://www.electronjs.org/docs/latest/api/browser-window
|
||||
* @type {BrowserWindow}
|
||||
* @process electron (renderer process)
|
||||
*/
|
||||
export const browser = globalThis.__enhancerElectronApi?.browser;
|
||||
|
||||
/**
|
||||
* access to the electron webFrame instance for the current page
|
||||
* see https://www.electronjs.org/docs/latest/api/web-frame
|
||||
* @type {webFrame}
|
||||
* @process electron (renderer process)
|
||||
*/
|
||||
export const webFrame = globalThis.__enhancerElectronApi?.webFrame;
|
||||
|
||||
/**
|
||||
* send a message to the main electron process
|
||||
* @param {string} channel - the message identifier
|
||||
* @param {any} data - the data to pass along with the message
|
||||
* @param {string=} namespace - a prefix for the message to categorise
|
||||
* it as e.g. enhancer-related. this should not be changed unless replicating
|
||||
* builtin ipc events.
|
||||
* @process electron (renderer process)
|
||||
*/
|
||||
export const sendMessage = (channel, data, namespace = 'notion-enhancer') => {
|
||||
if (globalThis.__enhancerElectronApi) {
|
||||
globalThis.__enhancerElectronApi.ipcRenderer.sendMessage(channel, data, namespace);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* send a message to the webview's parent renderer process
|
||||
* @param {string} channel - the message identifier
|
||||
* @param {any} data - the data to pass along with the message
|
||||
* @param {string=} namespace - a prefix for the message to categorise
|
||||
* it as e.g. enhancer-related. this should not be changed unless replicating
|
||||
* builtin ipc events.
|
||||
* @process electron (renderer process)
|
||||
*/
|
||||
export const sendMessageToHost = (channel, data, namespace = 'notion-enhancer') => {
|
||||
if (globalThis.__enhancerElectronApi) {
|
||||
globalThis.__enhancerElectronApi.ipcRenderer.sendMessageToHost(channel, data, namespace);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* receive a message from either the main process or
|
||||
* the webview's parent renderer process
|
||||
* @param {string} channel - the message identifier to listen for
|
||||
* @param {function} callback - the message handler, passed the args (event, data)
|
||||
* @param {string=} namespace - a prefix for the message to categorise
|
||||
* it as e.g. enhancer-related. this should not be changed unless replicating
|
||||
* builtin ipc events.
|
||||
* @process electron (renderer process)
|
||||
*/
|
||||
export const onMessage = (channel, callback, namespace = 'notion-enhancer') => {
|
||||
if (globalThis.__enhancerElectronApi) {
|
||||
globalThis.__enhancerElectronApi.ipcRenderer.onMessage(channel, callback, namespace);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* require() notion app files
|
||||
* @param {string} path - within notion/resources/app/ e.g. main/createWindow.js
|
||||
* @process electron (main process)
|
||||
*/
|
||||
export const notionRequire = (path) => {
|
||||
return globalThis.__enhancerElectronApi
|
||||
? globalThis.__enhancerElectronApi.notionRequire(path)
|
||||
: null;
|
||||
};
|
||||
|
||||
/**
|
||||
* get all available app windows excluding the menu
|
||||
* @process electron (main process)
|
||||
*/
|
||||
export const getNotionWindows = () => {
|
||||
return globalThis.__enhancerElectronApi
|
||||
? globalThis.__enhancerElectronApi.getNotionWindows()
|
||||
: null;
|
||||
};
|
||||
|
||||
/**
|
||||
* get the currently focused notion window
|
||||
* @process electron (main process)
|
||||
*/
|
||||
export const getFocusedNotionWindow = () => {
|
||||
return globalThis.__enhancerElectronApi
|
||||
? globalThis.__enhancerElectronApi.getFocusedNotionWindow()
|
||||
: null;
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* environment-specific methods and constants
|
||||
* @namespace env
|
||||
*/
|
||||
|
||||
import * as env from '../env/env.mjs';
|
||||
|
||||
/**
|
||||
* the environment/platform name code is currently being executed in
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const name = env.name;
|
||||
|
||||
/**
|
||||
* the current version of the enhancer
|
||||
* @constant
|
||||
* @type {string}
|
||||
*/
|
||||
export const version = env.version;
|
||||
|
||||
/**
|
||||
* open the enhancer's menu
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusMenu = env.focusMenu;
|
||||
|
||||
/**
|
||||
* focus an active notion tab
|
||||
* @type {function}
|
||||
*/
|
||||
export const focusNotion = env.focusNotion;
|
||||
|
||||
/**
|
||||
* reload all notion and enhancer menu tabs to apply changes
|
||||
* @type {function}
|
||||
*/
|
||||
export const reload = env.reload;
|
@ -1,137 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* helpers for formatting or parsing text
|
||||
* @namespace fmt
|
||||
*/
|
||||
|
||||
import { fs } from './index.mjs';
|
||||
|
||||
/**
|
||||
* transform a heading into a slug (a lowercase alphanumeric string separated by hyphens),
|
||||
* e.g. for use as an anchor id
|
||||
* @param {string} heading - the original heading to be slugified
|
||||
* @param {Set<string>=} slugs - a list of pre-generated slugs to avoid duplicates
|
||||
* @returns {string} the generated slug
|
||||
*/
|
||||
export const slugger = (heading, slugs = new Set()) => {
|
||||
heading = heading
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/[^A-Za-z0-9-_]/g, '')
|
||||
.toLowerCase();
|
||||
let i = 0,
|
||||
slug = heading;
|
||||
while (slugs.has(slug)) {
|
||||
i++;
|
||||
slug = `${heading}-${i}`;
|
||||
}
|
||||
return slug;
|
||||
};
|
||||
|
||||
/**
|
||||
* generate a reasonably random uuidv4 string. uses crypto implementation if available
|
||||
* (from https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid)
|
||||
* @returns {string} a uuidv4
|
||||
*/
|
||||
export const uuidv4 = () => {
|
||||
if (crypto?.randomUUID) return crypto.randomUUID();
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)';
|
||||
};
|
||||
|
||||
const patterns = {
|
||||
alphanumeric: /^[\w\.-]+$/,
|
||||
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
semver:
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i,
|
||||
email:
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i,
|
||||
url: /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,64}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i,
|
||||
color: /^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i,
|
||||
};
|
||||
function test(str, pattern) {
|
||||
const match = str.match(pattern);
|
||||
return !!(match && match.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* test the type of a value. unifies builtin, regex, and environment/api checks
|
||||
* @param {unknown} value - the value to check
|
||||
* @param {string|string[]} type - the type the value should be or a list of allowed values
|
||||
* @returns {boolean} whether or not the value matches the type
|
||||
*/
|
||||
export const is = async (value, type, { extension = '' } = {}) => {
|
||||
extension = !value || !value.endsWith || value.endsWith(extension);
|
||||
if (Array.isArray(type)) {
|
||||
return type.includes(value);
|
||||
}
|
||||
switch (type) {
|
||||
case 'array':
|
||||
return Array.isArray(value);
|
||||
case 'object':
|
||||
return value && typeof value === 'object' && !Array.isArray(value);
|
||||
case 'undefined':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
return typeof value === type && extension;
|
||||
case 'string':
|
||||
return typeof value === type && extension;
|
||||
case 'alphanumeric':
|
||||
case 'uuid':
|
||||
case 'semver':
|
||||
case 'email':
|
||||
case 'url':
|
||||
case 'color':
|
||||
return typeof value === 'string' && test(value, patterns[type]) && extension;
|
||||
case 'file':
|
||||
return typeof value === 'string' && value && (await fs.isFile(value)) && extension;
|
||||
}
|
||||
return false;
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* environment-specific file reading
|
||||
* @namespace fs
|
||||
*/
|
||||
|
||||
import * as fs from '../env/fs.mjs';
|
||||
|
||||
/**
|
||||
* get an absolute path to files within notion
|
||||
* @param {string} path - relative to the root notion/resources/app/ e.g. renderer/search.js
|
||||
* @process electron
|
||||
*/
|
||||
export const notionPath = fs.notionPath;
|
||||
|
||||
/**
|
||||
* transform a path relative to the enhancer root directory into an absolute path
|
||||
* @type {function}
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @returns {string} an absolute filepath
|
||||
*/
|
||||
export const localPath = fs.localPath;
|
||||
|
||||
/**
|
||||
* fetch and parse a json file's contents
|
||||
* @type {function}
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @param {FetchOptions=} opts - the second argument of a fetch() request
|
||||
* @returns {unknown} the json value of the requested file as a js object
|
||||
*/
|
||||
export const getJSON = fs.getJSON;
|
||||
|
||||
/**
|
||||
* fetch a text file's contents
|
||||
* @type {function}
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @param {FetchOptions=} opts - the second argument of a fetch() request
|
||||
* @returns {string} the text content of the requested file
|
||||
*/
|
||||
export const getText = fs.getText;
|
||||
|
||||
/**
|
||||
* check if a file exists
|
||||
* @type {function}
|
||||
* @param {string} path - a url or within-the-enhancer filepath
|
||||
* @returns {boolean} whether or not the file exists
|
||||
*/
|
||||
export const isFile = fs.isFile;
|
File diff suppressed because one or more lines are too long
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// compiles to .cjs for use in electron:
|
||||
// npx -y esbuild insert/api/index.mjs --minify --bundle --format=cjs --outfile=insert/api/index.cjs
|
||||
|
||||
/** environment-specific methods and constants */
|
||||
export * as env from './env.mjs';
|
||||
/** environment-specific file reading */
|
||||
export * as fs from './fs.mjs';
|
||||
/** environment-specific data persistence */
|
||||
export * as storage from './storage.mjs';
|
||||
|
||||
/** access to electron renderer apis */
|
||||
export * as electron from './electron.mjs';
|
||||
|
||||
/** a basic wrapper around notion's unofficial api */
|
||||
export * as notion from './notion.mjs';
|
||||
/** helpers for formatting, validating and parsing values */
|
||||
export * as fmt from './fmt.mjs';
|
||||
/** interactions with the enhancer's repository of mods */
|
||||
export * as registry from './registry.mjs';
|
||||
/** helpers for manipulation of a webpage */
|
||||
export * as web from './web.mjs';
|
||||
/** shared notion-style elements */
|
||||
export * as components from './components/index.mjs';
|
@ -1,224 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { fmt, registry } from './index.mjs';
|
||||
|
||||
const check = async (
|
||||
mod,
|
||||
key,
|
||||
value,
|
||||
types,
|
||||
{
|
||||
extension = '',
|
||||
error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify(
|
||||
value
|
||||
)}`,
|
||||
optional = false,
|
||||
} = {}
|
||||
) => {
|
||||
let test;
|
||||
for (const type of Array.isArray(types) ? [types] : types.split('|')) {
|
||||
if (type === 'file') {
|
||||
test =
|
||||
value && !value.startsWith('http')
|
||||
? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension })
|
||||
: false;
|
||||
} else test = await fmt.is(value, type, { extension });
|
||||
if (test) break;
|
||||
}
|
||||
if (!test) {
|
||||
if (optional && (await fmt.is(value, 'undefined'))) return true;
|
||||
if (error) mod._err(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateEnvironments = async (mod) => {
|
||||
mod.environments = mod.environments ?? registry.supportedEnvs;
|
||||
const isArray = await check(mod, 'environments', mod.environments, 'array');
|
||||
if (!isArray) return false;
|
||||
return mod.environments.map((tag) =>
|
||||
check(mod, 'environments.env', tag, registry.supportedEnvs)
|
||||
);
|
||||
},
|
||||
validateTags = async (mod) => {
|
||||
const isArray = await check(mod, 'tags', mod.tags, 'array');
|
||||
if (!isArray) return false;
|
||||
const categoryTags = ['core', 'extension', 'theme', 'integration'],
|
||||
containsCategory = mod.tags.filter((tag) => categoryTags.includes(tag)).length;
|
||||
if (!containsCategory) {
|
||||
mod._err(
|
||||
`invalid tags (must contain at least one of 'core', 'extension', 'theme' or 'integration'):
|
||||
${JSON.stringify(mod.tags)}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const isTheme = mod.tags.includes('theme'),
|
||||
hasThemeMode = mod.tags.includes('light') || mod.tags.includes('dark'),
|
||||
isBothThemeModes = mod.tags.includes('light') && mod.tags.includes('dark');
|
||||
if (isTheme && (!hasThemeMode || isBothThemeModes)) {
|
||||
mod._err(
|
||||
`invalid tags (themes must be either 'light' or 'dark', not neither or both):
|
||||
${JSON.stringify(mod.tags)}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return mod.tags.map((tag) => check(mod, 'tags.tag', tag, 'string'));
|
||||
},
|
||||
validateAuthors = async (mod) => {
|
||||
const isArray = await check(mod, 'authors', mod.authors, 'array');
|
||||
if (!isArray) return false;
|
||||
return mod.authors.map((author) => [
|
||||
check(mod, 'authors.author.name', author.name, 'string'),
|
||||
check(mod, 'authors.author.email', author.email, 'email', { optional: true }),
|
||||
check(mod, 'authors.author.homepage', author.homepage, 'url'),
|
||||
check(mod, 'authors.author.avatar', author.avatar, 'url'),
|
||||
]);
|
||||
},
|
||||
validateCSS = async (mod) => {
|
||||
const isArray = await check(mod, 'css', mod.css, 'object');
|
||||
if (!isArray) return false;
|
||||
const tests = [];
|
||||
for (const dest of ['frame', 'client', 'menu']) {
|
||||
if (!mod.css[dest]) continue;
|
||||
let test = await check(mod, `css.${dest}`, mod.css[dest], 'array');
|
||||
if (test) {
|
||||
test = mod.css[dest].map((file) =>
|
||||
check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' })
|
||||
);
|
||||
}
|
||||
tests.push(test);
|
||||
}
|
||||
return tests;
|
||||
},
|
||||
validateJS = async (mod) => {
|
||||
const isArray = await check(mod, 'js', mod.js, 'object');
|
||||
if (!isArray) return false;
|
||||
const tests = [];
|
||||
for (const dest of ['frame', 'client', 'menu']) {
|
||||
if (!mod.js[dest]) continue;
|
||||
let test = await check(mod, `js.${dest}`, mod.js[dest], 'array');
|
||||
if (test) {
|
||||
test = mod.js[dest].map((file) =>
|
||||
check(mod, `js.${dest}.file`, file, 'file', { extension: '.mjs' })
|
||||
);
|
||||
}
|
||||
tests.push(test);
|
||||
}
|
||||
if (mod.js.electron) {
|
||||
const isArray = await check(mod, 'js.electron', mod.js.electron, 'array');
|
||||
if (isArray) {
|
||||
for (const file of mod.js.electron) {
|
||||
const isObject = await check(mod, 'js.electron.file', file, 'object');
|
||||
if (!isObject) {
|
||||
tests.push(false);
|
||||
continue;
|
||||
}
|
||||
tests.push([
|
||||
check(mod, 'js.electron.file.source', file.source, 'file', {
|
||||
extension: '.cjs',
|
||||
}),
|
||||
// referencing the file within the electron app
|
||||
// existence can't be validated, so only format is
|
||||
check(mod, 'js.electron.file.target', file.target, 'string', {
|
||||
extension: '.js',
|
||||
}),
|
||||
]);
|
||||
}
|
||||
} else tests.push(false);
|
||||
}
|
||||
return tests;
|
||||
},
|
||||
validateOptions = async (mod) => {
|
||||
const isArray = await check(mod, 'options', mod.options, 'array');
|
||||
if (!isArray) return false;
|
||||
const tests = [];
|
||||
for (const option of mod.options) {
|
||||
const key = 'options.option',
|
||||
optTypeValid = await check(mod, `${key}.type`, option.type, registry.optionTypes);
|
||||
if (!optTypeValid) {
|
||||
tests.push(false);
|
||||
continue;
|
||||
}
|
||||
option.environments = option.environments ?? registry.supportedEnvs;
|
||||
tests.push([
|
||||
check(mod, `${key}.key`, option.key, 'alphanumeric'),
|
||||
check(mod, `${key}.label`, option.label, 'string'),
|
||||
check(mod, `${key}.tooltip`, option.tooltip, 'string', {
|
||||
optional: true,
|
||||
}),
|
||||
check(mod, `${key}.environments`, option.environments, 'array').then((isArray) => {
|
||||
if (!isArray) return false;
|
||||
return option.environments.map((environment) =>
|
||||
check(mod, `${key}.environments.env`, environment, registry.supportedEnvs)
|
||||
);
|
||||
}),
|
||||
]);
|
||||
switch (option.type) {
|
||||
case 'toggle':
|
||||
tests.push(check(mod, `${key}.value`, option.value, 'boolean'));
|
||||
break;
|
||||
case 'select': {
|
||||
let test = await check(mod, `${key}.values`, option.values, 'array');
|
||||
if (test) {
|
||||
test = option.values.map((value) =>
|
||||
check(mod, `${key}.values.value`, value, 'string')
|
||||
);
|
||||
}
|
||||
tests.push(test);
|
||||
break;
|
||||
}
|
||||
case 'text':
|
||||
case 'hotkey':
|
||||
tests.push(check(mod, `${key}.value`, option.value, 'string'));
|
||||
break;
|
||||
case 'number':
|
||||
case 'color':
|
||||
tests.push(check(mod, `${key}.value`, option.value, option.type));
|
||||
break;
|
||||
case 'file': {
|
||||
let test = await check(mod, `${key}.extensions`, option.extensions, 'array');
|
||||
if (test) {
|
||||
test = option.extensions.map((ext) =>
|
||||
check(mod, `${key}.extensions.extension`, ext, 'string')
|
||||
);
|
||||
}
|
||||
tests.push(test);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tests;
|
||||
};
|
||||
|
||||
/**
|
||||
* internally used to validate mod.json files and provide helpful errors
|
||||
* @private
|
||||
* @param {object} mod - a mod's mod.json in object form
|
||||
* @returns {boolean} whether or not the mod has passed validation
|
||||
*/
|
||||
export async function validate(mod) {
|
||||
let conditions = [
|
||||
check(mod, 'name', mod.name, 'string'),
|
||||
check(mod, 'id', mod.id, 'uuid'),
|
||||
check(mod, 'version', mod.version, 'semver'),
|
||||
validateEnvironments(mod),
|
||||
check(mod, 'description', mod.description, 'string'),
|
||||
check(mod, 'preview', mod.preview, 'file|url', { optional: true }),
|
||||
validateTags(mod),
|
||||
validateAuthors(mod),
|
||||
validateCSS(mod),
|
||||
validateJS(mod),
|
||||
validateOptions(mod),
|
||||
];
|
||||
do {
|
||||
conditions = await Promise.all(conditions.flat(Infinity));
|
||||
} while (conditions.some((condition) => Array.isArray(condition)));
|
||||
return conditions.every((passed) => passed);
|
||||
}
|
67
src/common/registry.js
Normal file
67
src/common/registry.js
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
let _core, _mods;
|
||||
const getCore = () => {
|
||||
_core ??= globalThis.__enhancerApi.readJson("/core/mod.json");
|
||||
return _core;
|
||||
},
|
||||
getMods = async () => {
|
||||
const { readJson } = globalThis.__enhancerApi;
|
||||
_mods ??= await Promise.all([
|
||||
getCore(),
|
||||
// prettier-ignore
|
||||
...(await readJson("/mods/registry.json")).map(async (modFolder) => {
|
||||
try {
|
||||
modFolder = `/mods/${modFolder}/mod.json`;
|
||||
const modManifest = await readJson(modFolder);
|
||||
modManifest._src = modFolder;
|
||||
return modManifest;
|
||||
} catch {}
|
||||
}),
|
||||
]).filter((mod) => mod);
|
||||
return _mods;
|
||||
},
|
||||
getThemes = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("theme"));
|
||||
},
|
||||
getExtensions = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("extension"));
|
||||
},
|
||||
getIntegrations = async () => {
|
||||
const mods = await getMods();
|
||||
return mods.filter(({ tags }) => tags.includes("integration"));
|
||||
};
|
||||
|
||||
const getProfile = async () => {
|
||||
const { initDatabase } = globalThis.__enhancerApi,
|
||||
currentProfile = await initDatabase().get("currentProfile");
|
||||
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.platforms && !mod.platforms.includes(platform)) return false;
|
||||
const { initDatabase } = globalThis.__enhancerApi,
|
||||
enabledMods = await initDatabase([await getProfile(), "enabledMods"]);
|
||||
return Boolean(enabledMods.get(id));
|
||||
};
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, {
|
||||
getMods,
|
||||
getCore,
|
||||
getThemes,
|
||||
getExtensions,
|
||||
getIntegrations,
|
||||
getProfile,
|
||||
isEnabled,
|
||||
});
|
@ -1,160 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* interactions with the enhancer's repository of mods
|
||||
* @namespace registry
|
||||
*/
|
||||
|
||||
import { env, fs, storage } from './index.mjs';
|
||||
import { validate } from './registry-validation.mjs';
|
||||
|
||||
/**
|
||||
* mod ids whitelisted as part of the enhancer's core, permanently enabled
|
||||
* @constant
|
||||
* @type {string[]}
|
||||
*/
|
||||
export const core = [
|
||||
'a6621988-551d-495a-97d8-3c568bca2e9e',
|
||||
'0f0bf8b6-eae6-4273-b307-8fc43f2ee082',
|
||||
'36a2ffc9-27ff-480e-84a7-c7700a7d232d',
|
||||
];
|
||||
|
||||
/**
|
||||
* all environments/platforms currently supported by the enhancer
|
||||
* @constant
|
||||
* @type {string[]}
|
||||
*/
|
||||
export const supportedEnvs = ['linux', 'win32', 'darwin', 'extension'];
|
||||
|
||||
/**
|
||||
* all available configuration types
|
||||
* @constant
|
||||
* @type {string[]}
|
||||
*/
|
||||
export const optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey'];
|
||||
|
||||
/**
|
||||
* the name of the active configuration profile
|
||||
* @returns {string}
|
||||
*/
|
||||
export const profileName = () => storage.get(['currentprofile'], 'default');
|
||||
|
||||
/**
|
||||
* the root database for the current profile
|
||||
* @returns {object} the get/set functions for the profile's storage
|
||||
*/
|
||||
export const profileDB = async () => storage.db(['profiles', await profileName()]);
|
||||
|
||||
let _list;
|
||||
const _errors = [];
|
||||
/**
|
||||
* list all available mods in the repo
|
||||
* @param {function} filter - a function to filter out mods
|
||||
* @returns {array} a validated list of mod.json objects
|
||||
*/
|
||||
export const list = async (filter = (mod) => true) => {
|
||||
if (!_list) {
|
||||
// deno-lint-ignore no-async-promise-executor
|
||||
_list = new Promise(async (res, _rej) => {
|
||||
const passed = [];
|
||||
for (const dir of await fs.getJSON('repo/registry.json')) {
|
||||
try {
|
||||
const mod = {
|
||||
...(await fs.getJSON(`repo/${dir}/mod.json`)),
|
||||
_dir: dir,
|
||||
_err: (message) => _errors.push({ source: dir, message }),
|
||||
};
|
||||
if (await validate(mod)) passed.push(mod);
|
||||
} catch {
|
||||
_errors.push({ source: dir, message: 'invalid mod.json' });
|
||||
}
|
||||
}
|
||||
res(passed);
|
||||
});
|
||||
}
|
||||
const filtered = [];
|
||||
for (const mod of await _list) if (await filter(mod)) filtered.push(mod);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
/**
|
||||
* list validation errors encountered when loading the repo
|
||||
* @returns {{ source: string, message: string }[]} error objects with an error message and a source directory
|
||||
*/
|
||||
export const errors = async () => {
|
||||
await list();
|
||||
return _errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* get a single mod from the repo
|
||||
* @param {string} id - the uuid of the mod
|
||||
* @returns {object} the mod's mod.json
|
||||
*/
|
||||
export const get = async (id) => {
|
||||
return (await list((mod) => mod.id === id))[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* checks if a mod is enabled: affected by the core whitelist,
|
||||
* environment and menu configuration
|
||||
* @param {string} id - the uuid of the mod
|
||||
* @returns {boolean} whether or not the mod is enabled
|
||||
*/
|
||||
export const enabled = async (id) => {
|
||||
const mod = await get(id);
|
||||
if (!mod.environments.includes(env.name)) return false;
|
||||
if (core.includes(id)) return true;
|
||||
return (await profileDB()).get(['_mods', id], false);
|
||||
};
|
||||
|
||||
/**
|
||||
* get a default value of a mod's option according to its mod.json
|
||||
* @param {string} id - the uuid of the mod
|
||||
* @param {string} key - the key of the option
|
||||
* @returns {string|number|boolean|undefined} the option's default value
|
||||
*/
|
||||
export const optionDefault = async (id, key) => {
|
||||
const mod = await get(id),
|
||||
opt = mod.options.find((opt) => opt.key === key);
|
||||
if (!opt) return undefined;
|
||||
switch (opt.type) {
|
||||
case 'toggle':
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'color':
|
||||
case 'hotkey':
|
||||
return opt.value;
|
||||
case 'select':
|
||||
return opt.values[0];
|
||||
case 'file':
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* access the storage partition of a mod in the current profile
|
||||
* @param {string} id - the uuid of the mod
|
||||
* @returns {object} an object with the wrapped get/set functions
|
||||
*/
|
||||
export const db = async (id) => {
|
||||
const db = await profileDB();
|
||||
return storage.db(
|
||||
[id],
|
||||
async (path, fallback = undefined) => {
|
||||
if (typeof path === 'string') path = [path];
|
||||
if (path.length === 2) {
|
||||
// profiles -> profile -> mod -> option
|
||||
fallback = (await optionDefault(id, path[1])) ?? fallback;
|
||||
}
|
||||
return db.get(path, fallback);
|
||||
},
|
||||
db.set
|
||||
);
|
||||
};
|
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: api
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* environment-specific data persistence
|
||||
* @namespace storage
|
||||
*/
|
||||
|
||||
import * as storage from '../env/storage.mjs';
|
||||
|
||||
/**
|
||||
* get persisted data
|
||||
* @type {function}
|
||||
* @param {string[]} path - the path of keys to the value being fetched
|
||||
* @param {unknown=} fallback - a default value if the path is not matched
|
||||
* @returns {Promise} value ?? fallback
|
||||
*/
|
||||
export const get = storage.get;
|
||||
|
||||
/**
|
||||
* persist data
|
||||
* @type {function}
|
||||
* @param {string[]} path - the path of keys to the value being set
|
||||
* @param {unknown} value - the data to save
|
||||
* @returns {Promise} resolves when data has been saved
|
||||
*/
|
||||
export const set = storage.set;
|
||||
|
||||
/**
|
||||
* create a wrapper for accessing a partition of the storage
|
||||
* @type {function}
|
||||
* @param {string[]} namespace - the path of keys to prefix all storage requests with
|
||||
* @param {function=} get - the storage get function to be wrapped
|
||||
* @param {function=} set - the storage set function to be wrapped
|
||||
* @returns {object} an object with the wrapped get/set functions
|
||||
*/
|
||||
export const db = storage.db;
|
||||
|
||||
/**
|
||||
* add an event listener for changes in storage
|
||||
* @type {function}
|
||||
* @param {onStorageChangeCallback} callback - called whenever a change in
|
||||
* storage is initiated from the current process
|
||||
*/
|
||||
export const addChangeListener = storage.addChangeListener;
|
||||
|
||||
/**
|
||||
* remove a listener added with storage.addChangeListener
|
||||
* @type {function}
|
||||
* @param {onStorageChangeCallback} callback
|
||||
*/
|
||||
export const removeChangeListener = storage.removeChangeListener;
|
||||
|
||||
/**
|
||||
* @callback onStorageChangeCallback
|
||||
* @param {object} event
|
||||
* @param {string} event.path- the path of keys to the changed value
|
||||
* @param {string=} event.new - the new value being persisted to the store
|
||||
* @param {string=} event.old - the previous value associated with the key
|
||||
*/
|
@ -4,14 +4,45 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
/**
|
||||
* 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 : ")")
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* helpers for manipulation of a webpage
|
||||
* @namespace web
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { fs } from './index.mjs';
|
||||
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)";
|
||||
};
|
||||
|
||||
let _hotkeyListenersActivated = false,
|
||||
_hotkeyEventListeners = [],
|
||||
@ -36,9 +67,9 @@ export const whenReady = (selectors = []) => {
|
||||
}
|
||||
isReady();
|
||||
};
|
||||
if (document.readyState !== 'complete') {
|
||||
document.addEventListener('readystatechange', (_event) => {
|
||||
if (document.readyState === 'complete') onLoad();
|
||||
if (document.readyState !== "complete") {
|
||||
document.addEventListener("readystatechange", (_event) => {
|
||||
if (document.readyState === "complete") onLoad();
|
||||
});
|
||||
} else onLoad();
|
||||
});
|
||||
@ -57,12 +88,12 @@ export const queryParams = () => new URLSearchParams(window.location.search);
|
||||
*/
|
||||
export const escape = (str) =>
|
||||
str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\\/g, '\');
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, """)
|
||||
.replace(/\\/g, "\");
|
||||
|
||||
/**
|
||||
* a tagged template processor for raw html:
|
||||
@ -75,18 +106,18 @@ export const raw = (str, ...templates) => {
|
||||
.map(
|
||||
(chunk) =>
|
||||
chunk +
|
||||
(['string', 'number'].includes(typeof templates[0])
|
||||
(["string", "number"].includes(typeof templates[0])
|
||||
? templates.shift()
|
||||
: escape(JSON.stringify(templates.shift(), null, 2) ?? ''))
|
||||
: escape(JSON.stringify(templates.shift(), null, 2) ?? ""))
|
||||
)
|
||||
.join('');
|
||||
return html.includes('<pre')
|
||||
.join("");
|
||||
return html.includes("<pre")
|
||||
? html.trim()
|
||||
: html
|
||||
.split(/\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length)
|
||||
.join(' ');
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
/**
|
||||
@ -131,7 +162,7 @@ export const empty = ($container) => {
|
||||
export const loadStylesheet = (path) => {
|
||||
const $stylesheet = html`<link
|
||||
rel="stylesheet"
|
||||
href="${path.startsWith('https://') ? path : fs.localPath(path)}"
|
||||
href="${path.startsWith("https://") ? path : fs.localPath(path)}"
|
||||
/>`;
|
||||
render(document.head, $stylesheet);
|
||||
return $stylesheet;
|
||||
@ -146,14 +177,14 @@ export const copyToClipboard = async (str) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(str);
|
||||
} catch {
|
||||
const $el = document.createElement('textarea');
|
||||
const $el = document.createElement("textarea");
|
||||
$el.value = str;
|
||||
$el.setAttribute('readonly', '');
|
||||
$el.style.position = 'absolute';
|
||||
$el.style.left = '-9999px';
|
||||
$el.setAttribute("readonly", "");
|
||||
$el.style.position = "absolute";
|
||||
$el.style.left = "-9999px";
|
||||
document.body.appendChild($el);
|
||||
$el.select();
|
||||
document.execCommand('copy');
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild($el);
|
||||
}
|
||||
};
|
||||
@ -167,13 +198,13 @@ export const readFromClipboard = () => {
|
||||
};
|
||||
|
||||
const triggerHotkeyListener = (event, hotkey) => {
|
||||
const inInput = document.activeElement.nodeName === 'INPUT' && !hotkey.listenInInput;
|
||||
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'],
|
||||
metaKey: ["meta", "os", "win", "cmd", "command"],
|
||||
ctrlKey: ["ctrl", "control"],
|
||||
shiftKey: ["shift"],
|
||||
altKey: ["alt"],
|
||||
},
|
||||
pressed = hotkey.keys.every((key) => {
|
||||
key = key.toLowerCase();
|
||||
@ -185,8 +216,8 @@ const triggerHotkeyListener = (event, hotkey) => {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (key === 'space') key = ' ';
|
||||
if (key === 'plus') key = '+';
|
||||
if (key === "space") key = " ";
|
||||
if (key === "plus") key = "+";
|
||||
if (key === event.key.toLowerCase()) return true;
|
||||
});
|
||||
if (!pressed) return;
|
||||
@ -218,17 +249,17 @@ export const addHotkeyListener = (
|
||||
callback,
|
||||
{ listenInInput = false, keydown = false } = {}
|
||||
) => {
|
||||
if (typeof keys === 'string') keys = keys.split('+');
|
||||
if (typeof keys === "string") keys = keys.split("+");
|
||||
_hotkeyEventListeners.push({ keys, callback, listenInInput, keydown });
|
||||
|
||||
if (!_hotkeyListenersActivated) {
|
||||
_hotkeyListenersActivated = true;
|
||||
document.addEventListener('keyup', (event) => {
|
||||
document.addEventListener("keyup", (event) => {
|
||||
for (const hotkey of _hotkeyEventListeners.filter(({ keydown }) => !keydown)) {
|
||||
triggerHotkeyListener(event, hotkey);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
document.addEventListener("keydown", (event) => {
|
||||
for (const hotkey of _hotkeyEventListeners.filter(({ keydown }) => keydown)) {
|
||||
triggerHotkeyListener(event, hotkey);
|
||||
}
|
44
src/core/mod.json
Normal file
44
src/core/mod.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
|
||||
"name": "notion-enhancer",
|
||||
"version": "0.11.1-dev",
|
||||
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
|
||||
"tags": ["core"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "dragonwocky",
|
||||
"homepage": "https://dragonwocky.me/",
|
||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"type": "heading",
|
||||
"label": "Hotkeys"
|
||||
},
|
||||
{
|
||||
"type": "hotkey",
|
||||
"key": "openMenuHotkey",
|
||||
"description": "Opens the notion-enhancer menu from within Notion.",
|
||||
"value": "CmdOrCtrl+Shift+,"
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"label": "Appearance"
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "loadThemeOverrides",
|
||||
"description": "Loads the styling required for a theme to customise Notion's interface. Turning this off will disable all themes but may increase client performance.",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"key": "customStyles",
|
||||
"description": "Adds the styles from an uploaded .css file to Notion. Use this if you would like to customise the current theme or <a href=\"https://notion-enhancer.github.io/advanced/tweaks\">otherwise tweak Notion's appearance</a>."
|
||||
}
|
||||
],
|
||||
"clientStyles": [],
|
||||
"clientScripts": [],
|
||||
"electronScripts": {}
|
||||
}
|
109
src/electron/api.js
Normal file
109
src/electron/api.js
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs"),
|
||||
os = require("os"),
|
||||
path = require("path"),
|
||||
platform = process.platform;
|
||||
|
||||
const notionRequire = (target) => require(`../../../${target}`),
|
||||
notionPath = (target) => path.resolve(`${__dirname}/../../../${target}`);
|
||||
|
||||
const enhancerRequire = (target) => require(`../${target}`),
|
||||
enhancerPath = (target) => path.resolve(`${__dirname}/../${target}`),
|
||||
enhancerUrl = (target) =>
|
||||
`notion://www.notion.so/__notion-enhancer/${target}`,
|
||||
enhancerVersion = enhancerRequire("package.json").version,
|
||||
enhancerConfig = path.resolve(`${os.homedir()}/.notion-enhancer.db`);
|
||||
|
||||
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));
|
||||
},
|
||||
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));
|
||||
},
|
||||
reloadApp = () => {
|
||||
const { app } = require("electron"),
|
||||
args = process.argv.slice(1).filter((arg) => arg !== "--startup");
|
||||
app.relaunch({ args });
|
||||
app.exit();
|
||||
};
|
||||
|
||||
let __db;
|
||||
const initDatabase = (namespace) => {
|
||||
if (Array.isArray(namespace)) namespace = namespace.join("__");
|
||||
namespace = namespace ? namespace + "__" : "";
|
||||
|
||||
const table = "settings",
|
||||
sqlite = require("better-sqlite3"),
|
||||
db = __db ?? sqlite(enhancerConfig),
|
||||
init = db.prepare(`CREATE TABLE IF NOT EXISTS ${table} (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
mtime INTEGER
|
||||
)`);
|
||||
init.run();
|
||||
__db = db;
|
||||
|
||||
// prettier-ignore
|
||||
const insert = db.prepare(`INSERT INTO ${table} (key, value, mtime) VALUES (?, ?, ?)`),
|
||||
// prettier-ignore
|
||||
update = db.prepare(`UPDATE ${table} SET value = ?, mtime = ? WHERE key = ?`),
|
||||
select = db.prepare(`SELECT * FROM ${table} WHERE key = ? LIMIT 1`),
|
||||
dump = db.prepare(`SELECT * FROM ${table}`),
|
||||
populate = db.transaction((obj) => {
|
||||
for (const key in obj) {
|
||||
if (select.get(key)) update.run(value, key, Date.now());
|
||||
else insert.run(key, value, Date.now());
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get: (key) => {
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
return select.get(key)?.value;
|
||||
},
|
||||
set: (key, value) => {
|
||||
key = key.startsWith(namespace) ? key : namespace + key;
|
||||
if (select.get(key)) return update.run(value, key, Date.now());
|
||||
else return insert.run(key, value, Date.now());
|
||||
},
|
||||
dump: () => {
|
||||
const rows = dump.all();
|
||||
let entries = rows.map(({ key, value }) => [key, value]);
|
||||
if (!namespace) return Object.fromEntries(entries);
|
||||
entries = entries.filter(([key]) => key.startsWith(`${namespace}__`));
|
||||
return Object.fromEntries(entries);
|
||||
},
|
||||
populate,
|
||||
};
|
||||
};
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, {
|
||||
platform,
|
||||
notionRequire,
|
||||
notionPath,
|
||||
enhancerRequire,
|
||||
enhancerPath,
|
||||
enhancerUrl,
|
||||
enhancerVersion,
|
||||
enhancerConfig,
|
||||
readFile,
|
||||
readJson,
|
||||
reloadApp,
|
||||
initDatabase,
|
||||
});
|
35
src/electron/client.js
Normal file
35
src/electron/client.js
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
console.log(123);
|
||||
|
||||
(async () => {
|
||||
// const { getCore, getMods, enhancerPath } = globalThis.__enhancerApi;
|
||||
// console.log(await getMods());
|
||||
// const page = location.pathname.split(/[/-]/g).reverse()[0].length === 32,
|
||||
// whitelisted = ["/", "/onboarding"].includes(location.pathname),
|
||||
// signedIn = localStorage["LRU:KeyValueStore2:current-user-id"];
|
||||
// if (page || (whitelisted && signedIn)) {
|
||||
// const api = await import("./api/index.mjs"),
|
||||
// { fs, registry, web } = api;
|
||||
// for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
// for (const sheet of mod.css?.client || []) {
|
||||
// web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||
// }
|
||||
// for (let script of mod.js?.client || []) {
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
})();
|
@ -1,34 +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';
|
||||
|
||||
(async () => {
|
||||
const page = location.pathname.split(/[/-]/g).reverse()[0].length === 32,
|
||||
whitelisted = ['/', '/onboarding'].includes(location.pathname),
|
||||
signedIn = localStorage['LRU:KeyValueStore2:current-user-id'];
|
||||
|
||||
if (page || (whitelisted && signedIn)) {
|
||||
const api = await import('./api/index.mjs'),
|
||||
{ fs, registry, web } = api;
|
||||
|
||||
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
for (const sheet of mod.css?.client || []) {
|
||||
web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
|
||||
}
|
||||
for (let script of mod.js?.client || []) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
})();
|
@ -1,50 +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";
|
||||
|
||||
module.exports = async (target, __exports, __eval) => {
|
||||
// if (target === "renderer/preload") {
|
||||
const { initDatabase } = require("./node.cjs");
|
||||
|
||||
// require("notion-enhancer/electronApi.cjs");
|
||||
// const api = require("notion-enhancer/api/index.cjs"),
|
||||
// { registry } = api;
|
||||
|
||||
// if (target === "renderer/index") {
|
||||
// document.addEventListener("readystatechange", (event) => {
|
||||
// if (document.readyState !== "complete") return false;
|
||||
// const script = document.createElement("script");
|
||||
// script.type = "module";
|
||||
// script.src = api.fs.localPath("frame.mjs");
|
||||
// document.head.appendChild(script);
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (target === "renderer/preload") {
|
||||
const db = initDatabase("config");
|
||||
// document.addEventListener("readystatechange", (event) => {
|
||||
// if (document.readyState !== "complete") return false;
|
||||
// const script = document.createElement("script");
|
||||
// script.type = "module";
|
||||
// script.src = api.fs.localPath("client.mjs");
|
||||
// document.head.appendChild(script);
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (target === "main/main") {
|
||||
// const { app } = require("electron");
|
||||
// app.whenReady().then(require("notion-enhancer/worker.cjs").listen);
|
||||
// }
|
||||
|
||||
// for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
// for (const { source, target: scriptTarget } of (mod.js ? mod.js.electron : []) || []) {
|
||||
// if (`${target}.js` !== scriptTarget) continue;
|
||||
// const script = require(`notion-enhancer/repo/${mod._dir}/${source}`);
|
||||
// script(api, await registry.db(mod.id), __exports, __eval);
|
||||
// }
|
||||
// }
|
||||
};
|
36
src/electron/init.js
Normal file
36
src/electron/init.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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");
|
||||
require("../common/registry.js");
|
||||
|
||||
module.exports = async (target, __exports, __eval) => {
|
||||
// clientScripts
|
||||
if (target === "renderer/preload") {
|
||||
const { enhancerUrl } = globalThis.__enhancerApi;
|
||||
document.addEventListener("readystatechange", (event) => {
|
||||
if (document.readyState !== "complete") return false;
|
||||
const script = document.createElement("script");
|
||||
script.type = "module";
|
||||
script.src = enhancerUrl("electron/client.js");
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// electronScripts
|
||||
const { getMods, getProfile, initDatabase } = globalThis.__enhancerApi;
|
||||
for (const mod of await getMods()) {
|
||||
if (!mod.electronScripts || !isEnabled(mod.id)) continue;
|
||||
for (const { source, target: targetScript } of mod.electronScripts) {
|
||||
if (`${target}.js` !== targetScript) continue;
|
||||
const script = require(`notion-enhancer/repo/${mod._dir}/${source}`),
|
||||
db = await initDatabase([await getProfile(), mod.id]);
|
||||
script(globalThis.__enhancerApi, db, __exports, __eval);
|
||||
}
|
||||
}
|
||||
};
|
@ -1,67 +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";
|
||||
|
||||
const os = require("os"),
|
||||
path = require("path"),
|
||||
electron = require("electron"),
|
||||
sqlite = require("better-sqlite3");
|
||||
|
||||
const notionRequire = (target) => require(`../../../${target}`),
|
||||
notionPath = (target) => path.resolve(`${__dirname}/../../../${target}`),
|
||||
notionPlatform = process.platform;
|
||||
|
||||
const enhancerRequire = (target) => require(`../${target}`),
|
||||
enhancerPath = (target) => path.resolve(`${__dirname}/../${target}`),
|
||||
enhancerUrl = (target) => `notion://www.notion.so/__notion-enhancer/${target}`,
|
||||
enhancerVersion = enhancerRequire("package.json").version,
|
||||
enhancerConfig = path.resolve(`${os.homedir()}/.notion-enhancer.db`);
|
||||
|
||||
const reloadApp = () => {
|
||||
const args = process.argv.slice(1).filter((arg) => arg !== "--startup");
|
||||
electron.app.relaunch({ args });
|
||||
electron.app.exit();
|
||||
};
|
||||
|
||||
let __db;
|
||||
const initDatabase = (table) => {
|
||||
const db = __db ?? sqlite(enhancerConfig),
|
||||
init = db.prepare(`CREATE TABLE IF NOT EXISTS ${table} (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
mtime INTEGER
|
||||
)`);
|
||||
init.run();
|
||||
__db = db;
|
||||
|
||||
const insert = db.prepare(`INSERT INTO ${table} (key, value, mtime) VALUES (?, ?, ?)`),
|
||||
update = db.prepare(`UPDATE ${table} SET value = ?, mtime = ? WHERE key = ?`),
|
||||
select = db.prepare(`SELECT * FROM ${table} WHERE key = ? LIMIT 1`),
|
||||
dump = db.prepare(`SELECT * FROM ${table}`);
|
||||
|
||||
return {
|
||||
get: (key) => select.get(key)?.value,
|
||||
set: (key, value) => {
|
||||
if (select.get(key)) return update.run(value, key, Date.now());
|
||||
else return insert.run(key, value, Date.now());
|
||||
},
|
||||
dump: () => dump.all(),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
notionRequire,
|
||||
notionPath,
|
||||
notionPlatform,
|
||||
enhancerRequire,
|
||||
enhancerPath,
|
||||
enhancerUrl,
|
||||
enhancerVersion,
|
||||
enhancerConfig,
|
||||
reloadApp,
|
||||
initDatabase,
|
||||
};
|
@ -1,104 +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';
|
||||
module.exports = {};
|
||||
|
||||
const onMessage = (id, callback) => {
|
||||
const { ipcMain } = require('electron');
|
||||
ipcMain.on(`notion-enhancer:${id}`, callback);
|
||||
};
|
||||
|
||||
let enhancerMenu;
|
||||
module.exports.focusMenu = async () => {
|
||||
if (enhancerMenu) return enhancerMenu.show();
|
||||
|
||||
const { fs } = require('notion-enhancer/api/index.cjs'),
|
||||
{ app, session, BrowserWindow } = require('electron'),
|
||||
windowState = require('electron-window-state')({
|
||||
file: 'enhancer-menu-window-state.json',
|
||||
defaultWidth: 1250,
|
||||
defaultHeight: 850,
|
||||
}),
|
||||
{ registry } = require('notion-enhancer/api/index.cjs'),
|
||||
integratedTitlebar = await registry.enabled('a5658d03-21c6-4088-bade-fa4780459133');
|
||||
|
||||
enhancerMenu = new BrowserWindow({
|
||||
show: true,
|
||||
frame: !integratedTitlebar,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
width: windowState.width,
|
||||
height: windowState.height,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
session: session.fromPartition('persist:notion'),
|
||||
preload: require('path').resolve(`${__dirname}/electronApi.cjs`),
|
||||
},
|
||||
});
|
||||
enhancerMenu.loadURL(fs.localPath('repo/menu/menu.html'));
|
||||
windowState.manage(enhancerMenu);
|
||||
|
||||
let appQuit = false;
|
||||
app.once('before-quit', () => {
|
||||
appQuit = true;
|
||||
});
|
||||
|
||||
// handle opening external links
|
||||
// must have target="_blank"
|
||||
enhancerMenu.webContents.on('new-window', (e, url) => {
|
||||
e.preventDefault();
|
||||
require('electron').shell.openExternal(url);
|
||||
});
|
||||
|
||||
const trayID = 'f96f4a73-21af-4e3f-a68f-ab4976b020da',
|
||||
runInBackground =
|
||||
(await registry.enabled(trayID)) &&
|
||||
(await (await registry.db(trayID)).get(['run_in_background']));
|
||||
enhancerMenu.on('close', (e) => {
|
||||
const isLastWindow = BrowserWindow.getAllWindows().length === 1;
|
||||
if (!appQuit && isLastWindow && runInBackground) {
|
||||
enhancerMenu.hide();
|
||||
e.preventDefault();
|
||||
} else enhancerMenu = null;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getNotionWindows = () => {
|
||||
const { BrowserWindow } = require('electron'),
|
||||
windows = BrowserWindow.getAllWindows();
|
||||
if (enhancerMenu) return windows.filter((win) => win.id !== enhancerMenu.id);
|
||||
return windows;
|
||||
};
|
||||
|
||||
module.exports.getFocusedNotionWindow = () => {
|
||||
const { BrowserWindow } = require('electron'),
|
||||
focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (enhancerMenu && focusedWindow && focusedWindow.id === enhancerMenu.id) return null;
|
||||
return focusedWindow;
|
||||
};
|
||||
|
||||
module.exports.focusNotion = () => {
|
||||
const api = require('notion-enhancer/api/index.cjs'),
|
||||
{ createWindow } = api.electron.notionRequire('main/createWindow.js');
|
||||
let window = module.exports.getFocusedNotionWindow() || module.exports.getNotionWindows()[0];
|
||||
if (!window) window = createWindow('/');
|
||||
window.show();
|
||||
};
|
||||
|
||||
module.exports.reload = () => {
|
||||
const { app } = require('electron');
|
||||
app.relaunch({ args: process.argv.slice(1).filter((arg) => arg !== '--startup') });
|
||||
app.quit();
|
||||
};
|
||||
|
||||
module.exports.listen = () => {
|
||||
onMessage('focusMenu', module.exports.focusMenu);
|
||||
onMessage('focusNotion', module.exports.focusNotion);
|
||||
onMessage('reload', module.exports.reload);
|
||||
};
|
4
src/vendor/twind.min.js
vendored
4
src/vendor/twind.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user