Compare commits

...

10 Commits
v0.11.1 ... dev

23 changed files with 378 additions and 366 deletions

View File

@ -1,5 +1,5 @@
# notion-enhancer/desktop # notion-enhancer/desktop
an enhancer/customiser for the all-in-one productivity workspace notion.so (app) Customise the all-in-one productivity workspace Notion.
[read the docs online](https://notion-enhancer.github.io/) [read the docs online](https://notion-enhancer.github.io/)

View File

@ -2,7 +2,7 @@
"name": "notion-enhancer", "name": "notion-enhancer",
"version": "0.11.1", "version": "0.11.1",
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)", "author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
"description": "An enhancer/customiser for the all-in-one productivity workspace Notion", "description": "Customise the all-in-one productivity workspace Notion.",
"homepage": "https://notion-enhancer.github.io", "homepage": "https://notion-enhancer.github.io",
"repository": "github:notion-enhancer/desktop", "repository": "github:notion-enhancer/desktop",
"bugs": "https://github.com/notion-enhancer/desktop/issues", "bugs": "https://github.com/notion-enhancer/desktop/issues",

View File

@ -93,27 +93,42 @@ const backupApp = async () => {
await fsp.rename(archive + ".bak", archive); await fsp.rename(archive + ".bak", archive);
return true; return true;
}, },
enhanceApp = async (debug = false) => { enhanceApp = async (debug = false, directoryMode = false) => {
const app = getResourcePath("app"), const app = getResourcePath("app"),
archive = getResourcePath("app.asar"); archive = getResourcePath("app.asar");
if (!existsSync(archive)) return false; // directory mode acts on pre-extracted sources
if (existsSync(app)) await fsp.rm(app, { recursive: true, force: true }); // as part of the notion-repackaged build process
await fsp.mkdir(app); if (directoryMode) {
// extract archive to folder and apply patches if (!existsSync(app)) return false;
for (let file of asar.listPackage(archive)) { for (let file of await fsp.readdir(app, { recursive: true })) {
file = file.replace(/^\//g, ""); file = file.replace(/^\//g, "");
const stat = asar.statFile(archive, file), const appPath = resolve(app, file),
isFolder = !!stat.files, stat = await fsp.stat(appPath);
isSymlink = !!stat.link, if (stat.isFile()) {
isExecutable = stat.executable, const content = await fsp.readFile(appPath);
appPath = resolve(app, file); await fsp.writeFile(appPath, patch(file, content));
if (isFolder) { }
await fsp.mkdir(appPath); }
} else if (isSymlink) { } else {
await fsp.symlink(appPath, resolve(app, link)); if (!existsSync(archive)) return false;
} else { if (existsSync(app)) await fsp.rm(app, { recursive: true, force: true });
await fsp.writeFile(appPath, patch(file, extractFile(file))); await fsp.mkdir(app);
if (isExecutable) await fsp.chmod(appPath, "755"); // extract archive to folder and apply patches
for (let file of asar.listPackage(archive)) {
file = file.replace(/^\//g, "");
const stat = asar.statFile(archive, file),
isFolder = !!stat.files,
isSymlink = !!stat.link,
isExecutable = stat.executable,
appPath = resolve(app, file);
if (isFolder) {
await fsp.mkdir(appPath);
} else if (isSymlink) {
await fsp.symlink(appPath, resolve(app, link));
} else {
await fsp.writeFile(appPath, patch(file, extractFile(file)));
if (isExecutable) await fsp.chmod(appPath, "755");
}
} }
} }
// insert the notion-enhancer/src folder into notion's node_modules // insert the notion-enhancer/src folder into notion's node_modules
@ -126,10 +141,12 @@ const backupApp = async () => {
excludes = ["bin", "type", "scripts", "engines", "dependencies"]; excludes = ["bin", "type", "scripts", "engines", "dependencies"];
for (const key of excludes) delete manifest[key]; for (const key of excludes) delete manifest[key];
await fsp.writeFile(insertManifest, JSON.stringify(manifest)); await fsp.writeFile(insertManifest, JSON.stringify(manifest));
// re-package enhanced sources into executable archive if (!directoryMode) {
await asar.createPackage(app, archive); // re-package enhanced sources into executable archive
// cleanup extracted files unless in debug mode await asar.createPackage(app, archive);
if (!debug) await fsp.rm(app, { recursive: true }); // cleanup extracted files unless in debug mode
if (!debug) await fsp.rm(app, { recursive: true });
}
return true; return true;
}; };

View File

@ -36,11 +36,15 @@ const patches = {
(content = replaceIfNotFound( (content = replaceIfNotFound(
{ string: content, mode: "prepend" }, { string: content, mode: "prepend" },
...args ...args
)),
append = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "append" },
...args
)); ));
// https://github.com/notion-enhancer/notion-enhancer/issues/160: // https://github.com/notion-enhancer/notion-enhancer/issues/160:
// enable the notion:// protocol, windows-style tab layouts, and // run the app in windows mode on linux (instead of macos mode)
// quitting the app when the last window is closed on linux
const isWindows = const isWindows =
/(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g, /(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g,
isWindowsOrLinux = '["win32","linux"].includes(process.platform)'; isWindowsOrLinux = '["win32","linux"].includes(process.platform)';
@ -50,15 +54,6 @@ const patches = {
// so the notion-enhancer can be require()-d into it // so the notion-enhancer can be require()-d into it
replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`); replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`);
// bypass webRequest filter to load enhancer menu
replace(/(\w)\.top!==\w\?(\w)\(\{cancel:!0\}\)/, "$1.top!==$1?$2({})");
// https://github.com/notion-enhancer/desktop/issues/291
// bypass csp issues by intercepting the notion:// protocol
const protocolHandler = /try\{const \w=await \w\.assetCache\.handleRequest\(\w\);/,
protocolInterceptor = `{const n="notion://www.notion.so/__notion-enhancer/";if(e.url.startsWith(n))return require("electron").net.fetch(\`file://\${require("path").join(__dirname,"..","..","node_modules","notion-enhancer",e.url.slice(n.length))}\`)}`;
prepend(protocolHandler, protocolInterceptor);
// expose the app's config + cache + preferences to the global namespace // expose the app's config + cache + preferences to the global namespace
// e.g. to enable development mode or check if keep in background is enabled // e.g. to enable development mode or check if keep in background is enabled
prepend(/\w\.exports=JSON\.parse\('\{"env":"production"/, "globalThis.__notionConfig="); prepend(/\w\.exports=JSON\.parse\('\{"env":"production"/, "globalThis.__notionConfig=");

View File

@ -21,11 +21,12 @@ const platform = IS_ELECTRON
version = IS_ELECTRON version = IS_ELECTRON
? require("notion-enhancer/package.json").version ? require("notion-enhancer/package.json").version
: chrome.runtime.getManifest().version, : chrome.runtime.getManifest().version,
// forms a url to a notion-enhancer asset or source file // packages a url to access notion-enhancer assets and sources,
// that can be accessed reliably over http // proxies via api in desktop app to bypass service worker cache
enhancerUrl = (target) => enhancerUrl = (target = "") =>
IS_ELECTRON IS_ELECTRON
? `notion://www.notion.so/__notion-enhancer/${target.replace(/^\//, "")}` ? "https://www.notion.so/api/__notion-enhancer/" +
target.replace(/^\//, "")
: chrome.runtime.getURL(target), : chrome.runtime.getURL(target),
// require a file from the root of notion's app/ folder, // require a file from the root of notion's app/ folder,
// only available in an electron main process // only available in an electron main process
@ -96,10 +97,6 @@ const readFile = (file) => {
{ resolve } = require("path"); { resolve } = require("path");
return fsp.readFile(resolve(`${__dirname}/../${file}`), "utf-8"); return fsp.readFile(resolve(`${__dirname}/../${file}`), "utf-8");
} }
// prefer using versions of files cached by the app
// or routed through the notion-enhancer's url interception
const notionProtocol = "notion://www.notion.so/";
file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol);
} else file = file.startsWith("http") ? file : enhancerUrl(file); } else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.text()); return fetch(file).then((res) => res.text());
}, },
@ -112,8 +109,6 @@ const readFile = (file) => {
const { resolve } = require("path"); const { resolve } = require("path");
return require(resolve(`${__dirname}/../${file}`)); return require(resolve(`${__dirname}/../${file}`));
} }
const notionProtocol = "notion://www.notion.so/";
file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol);
} else file = file.startsWith("http") ? file : enhancerUrl(file); } else file = file.startsWith("http") ? file : enhancerUrl(file);
return fetch(file).then((res) => res.json()); return fetch(file).then((res) => res.json());
}; };

View File

@ -37,17 +37,19 @@ const shouldLoadThemeOverrides = async (api, db) => {
const { html } = api, const { html } = api,
customStyles = (await db.get("customStyles"))?.content; customStyles = (await db.get("customStyles"))?.content;
if (!customStyles) return; if (!customStyles) return;
return document.head.append(html`<style> const $customStyles = html`<style
${customStyles} id="__custom"
</style>`); innerHTML=${customStyles}
></style>`;
return document.head.append($customStyles);
}; };
const insertMenu = async (api, db) => { const insertMenu = async (api, db) => {
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`, const inviteMember = `.notion-sidebar-container .notion-sidebar [role="button"]:has(.inviteMember)`,
notionSidebarButtons = `${notionSidebar} > [role="button"]`,
{ html, addMutationListener, removeMutationListener } = api, { html, addMutationListener, removeMutationListener } = api,
{ addKeyListener, platform, enhancerUrl, onMessage } = api, { addKeyListener, platform, enhancerUrl, onMessage } = api,
menuButtonIconStyle = await db.get("menuButtonIconStyle"), menuButtonIconStyle = await db.get("menuButtonIconStyle"),
menuButtonLabel = await db.get("menuButtonLabel"),
openMenuHotkey = await db.get("openMenuHotkey"), openMenuHotkey = await db.get("openMenuHotkey"),
menuPing = { menuPing = {
channel: "notion-enhancer", channel: "notion-enhancer",
@ -85,21 +87,18 @@ const insertMenu = async (api, db) => {
icon="notion-enhancer${menuButtonIconStyle === "Monochrome" icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
? "?mask" ? "?mask"
: " text-[16px]"}" : " text-[16px]"}"
>notion-enhancer >${menuButtonLabel}
<//>`; <//>`;
const appendToDom = () => { const appendToDom = () => {
document.body.append($modal); if (!document.body.contains($modal)) document.body.append($modal);
const btns = document.querySelectorAll(notionSidebarButtons); else if (!document.body.contains($button)) {
for (const $btn of btns) { document.querySelector(inviteMember)?.after($button);
if ($btn.textContent.includes("Settings")) $btn.after($button); } else removeMutationListener(appendToDom);
}
const appended = document.contains($modal) && document.contains($button);
if (appended) removeMutationListener(appendToDom);
}; };
html`<${Tooltip}> html`<${Tooltip}>
<b>Configure the notion-enhancer and its mods</b> <b>Configure the notion-enhancer and its mods</b>
<//>`.attach($button, "right"); <//>`.attach($button, "right");
addMutationListener(notionSidebar, appendToDom); addMutationListener(inviteMember, appendToDom);
addMutationListener(".notion-app-inner", updateMenuTheme, { subtree: false }); addMutationListener(".notion-app-inner", updateMenuTheme, { subtree: false });
appendToDom(); appendToDom();

View File

@ -15,10 +15,10 @@ function MenuButton(
class: `notion-enhancer--menu-button flex select-none class: `notion-enhancer--menu-button flex select-none
cursor-pointer rounded-[6px] text-[14px] font-medium cursor-pointer rounded-[6px] text-[14px] font-medium
transition hover:bg-[color:var(--theme--bg-hover)] transition hover:bg-[color:var(--theme--bg-hover)]
w-full h-[30px] px-[8px] py-[4px] items-center`, w-full h-[30px] px-[10px] py-[4px] items-center`,
}); });
return html`<div ...${props}> return html`<div ...${props}>
<div class="flex items-center justify-center size-[22px] mr-[8px]"> <div class="flex items-center justify-center text-[18px] mr-[10px]">
<i class="i-${icon}"></i> <i class="i-${icon}"></i>
</div> </div>
<div>${children}</div> <div>${children}</div>

View File

@ -80,7 +80,14 @@ function Options({ mod }) {
const { html, modDatabase, setState } = globalThis.__enhancerApi; const { html, modDatabase, setState } = globalThis.__enhancerApi;
return filterOptionsForRender(mod.options).map((opt) => { return filterOptionsForRender(mod.options).map((opt) => {
opt.label ??= camelToSentenceCase(opt.key); opt.label ??= camelToSentenceCase(opt.key);
if (opt.type === "heading") return html`<${Heading}>${opt.label}<//>`; if (opt.type === "heading") {
return typeof opt.description === "string"
? html`<div class="mb-[18px]">
<${Heading}>${opt.label}<//>
<${Description} innerHTML=${opt.description} />
</div>`
: html`<${Heading}>${opt.label}<//>`;
}
const _get = async () => (await modDatabase(mod.id)).get(opt.key), const _get = async () => (await modDatabase(mod.id)).get(opt.key),
_set = async (value) => { _set = async (value) => {
await (await modDatabase(mod.id)).set(opt.key, value); await (await modDatabase(mod.id)).set(opt.key, value);

View File

@ -2,7 +2,7 @@
"name": "notion-enhancer", "name": "notion-enhancer",
"version": "0.11.1", "version": "0.11.1",
"id": "0f0bf8b6-eae6-4273-b307-8fc43f2ee082", "id": "0f0bf8b6-eae6-4273-b307-8fc43f2ee082",
"description": "an enhancer/customiser for the all-in-one productivity workspace notion.so", "description": "Customise the all-in-one productivity workspace Notion.",
"tags": ["core"], "tags": ["core"],
"authors": [ "authors": [
{ {
@ -44,10 +44,16 @@
"description": "Loads the styling required for a theme to customise Notion's interface. Disabling this may increase client performance, but will also disable all themes.", "description": "Loads the styling required for a theme to customise Notion's interface. Disabling this may increase client performance, but will also disable all themes.",
"values": ["Auto", "Enabled", "Disabled"] "values": ["Auto", "Enabled", "Disabled"]
}, },
{
"type": "text",
"key": "menuButtonLabel",
"description": "Sets the text to label the notion-enhancer button added to Notion's sidebar with.",
"value": "notion-enhancer"
},
{ {
"type": "select", "type": "select",
"key": "menuButtonIconStyle", "key": "menuButtonIconStyle",
"description": "Sets whether the notion-enhancer icon added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.", "description": "Sets whether the icon beside the notion-enhancer button added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.",
"values": ["Colour", "Monochrome"] "values": ["Colour", "Monochrome"]
}, },
{ {
@ -73,7 +79,7 @@
"key": "developerMode", "key": "developerMode",
"description": "Activates built-in debugging tools accessible through the application menu.", "description": "Activates built-in debugging tools accessible through the application menu.",
"platforms": ["linux", "win32", "darwin"], "platforms": ["linux", "win32", "darwin"],
"value": true "value": false
} }
], ],
"clientStyles": ["variables.css", "../vendor/@unocss-preflight-tailwind.css"], "clientStyles": ["variables.css", "../vendor/@unocss-preflight-tailwind.css"],

View File

@ -0,0 +1,84 @@
/**
* notion-enhancer: indent guides
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
body {
--guide--style: solid;
--guide--color: var(--theme--fg-border);
--guide--opacity: 0;
}
/* add indent guides to nested blocks */
.notion-header-block,
.notion-sub_header-block,
.notion-sub_sub_header-block,
.notion-toggle-block,
.notion-to_do-block,
.notion-bulleted_list-block,
.notion-numbered_list-block {
--guide--offset: 32px;
--guide--indent: 14px;
position: relative;
&:before {
content: "";
position: absolute;
height: calc(100% - var(--guide--offset));
top: var(--guide--offset);
margin-inline-start: var(--guide--indent);
border-left: 1px var(--guide--style) var(--guide--color);
opacity: var(--guide--opacity);
}
}
.notion-header-block {
--guide--offset: 47px;
}
.notion-sub_header-block {
--guide--offset: 40px;
}
.notion-header-block,
.notion-sub_header-block,
.notion-sub_sub_header-block,
.notion-toggle-block {
--guide--indent: 13.4px;
}
/* add indent guides to toc blocks & the outliner */
.notion-table_of_contents-block
[contenteditable="false"]
a
> div:not([style*="margin-left: 0"]),
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
position: relative;
--guide--indent: -16px;
&:before {
content: "";
top: 0;
position: absolute;
height: 100%;
margin-inline-start: var(--guide--indent);
border-left: 1px var(--guide--style) var(--guide--color);
opacity: var(--guide--opacity);
}
}
.notion-enhancer--outliner-heading:not(.pl-\[18px\]) {
--guide--indent: -12px;
}
/* add solid background to drag handles,
otherwise guides show through underneath */
[role="button"]:is([aria-label="Drag"], [aria-label^="Click to add below"]) {
position: relative;
&:before {
content: "";
z-index: -1;
position: absolute;
width: 100%;
height: 100%;
background: var(--theme--bg-primary);
}
}

View File

@ -0,0 +1,66 @@
/**
* notion-enhancer: indent guides
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
export default async function (api, db) {
const { html } = api,
guideStyle = await db.get("guideStyle"),
rainbowMode = await db.get("rainbowMode");
document.body.style.setProperty("--guide--style", guideStyle.toLowerCase());
const nestedTargets = [],
outlineTargets = [];
for (const [listType, selectors] of [
["to-doList", [".notion-to_do-block"]],
["bulletedList", [".notion-bulleted_list-block"]],
["numberedList", [".notion-numbered_list-block"]],
["toggleList", [".notion-toggle-block"]],
[
"toggleHeadings",
[
".notion-header-block",
".notion-sub_header-block",
".notion-sub_sub_header-block",
],
],
]) {
if (await db.get(listType)) nestedTargets.push(...selectors);
}
if (await db.get("tableOfContents"))
outlineTargets.push(".notion-table_of_contents-block");
if (await db.get("outliner"))
outlineTargets.push(".notion-enhancer--outliner-heading");
let css = `${[...nestedTargets, ...outlineTargets].join(",")} {
--guide--opacity: 1;
}`;
if (rainbowMode) {
const opacity = `--guide--opacity: 0.5;`,
selector = `:is(${nestedTargets.join(",")})`,
colours = ["green", "blue", "purple", "pink", "red", "orange", "yellow"];
colours.push(...colours, ...colours, ...colours, "gray");
for (let i = 0; i < colours.length; i++) {
css += `${(selector + " ").repeat(i + 1)} {
--guide--color: var(--theme--fg-${colours[i]});
${opacity}
}`;
}
css += `
.notion-table_of_contents-block [contenteditable="false"] a
> div[style*="margin-left: 24px"],
.notion-enhancer--outliner-heading.pl-\\[36px\\] {
--guide--color: var(--theme--fg-${colours[0]});
${opacity}
}
.notion-table_of_contents-block [contenteditable="false"] a
> div[style*="margin-left: 48px"],
.notion-enhancer--outliner-heading.pl-\\[54px\\] {
--guide--color: var(--theme--fg-${colours[1]});
${opacity}
}`;
}
document.head.append(html`<style innerHTML=${css}></style>`);
}

View File

@ -0,0 +1,79 @@
{
"name": "Indent Guides",
"id": "35815b3b-3916-4dc6-8769-c9c2448f8b57",
"version": "0.3.0",
"description": "Marks list indentation with vertical lines to make it easy to follow.",
"tags": ["extension", "usability", "indentation-lines"],
"authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{
"name": "runargs",
"email": "alnbaldon@gmail.com",
"homepage": "http://github.com/runargs",
"avatar": "https://avatars.githubusercontent.com/u/39810066"
}
],
"options": [
{
"type": "select",
"key": "guideStyle",
"description": "The type of line to use for indent guides.",
"values": ["Solid", "Dashed", "Dotted"]
},
{
"type": "toggle",
"key": "rainbowMode",
"description": "By default, indent guides are coloured based on the current theme. Rainbow mode uses alternating colours at each indent level to better connect corresponding blocks in longer lists.",
"value": false
},
{ "type": "heading", "label": "List Types" },
{
"type": "toggle",
"key": "to-doList",
"description": "Shows indent guides for Notion's to-do list blocks.",
"value": true
},
{
"type": "toggle",
"key": "bulletedList",
"description": "Shows indent guides for Notion's bulleted list blocks.",
"value": true
},
{
"type": "toggle",
"key": "numberedList",
"description": "Shows indent guides for Notion's numbered list blocks.",
"value": true
},
{
"type": "toggle",
"key": "toggleList",
"description": "Shows indent guides for Notion's toggle list blocks.",
"value": true
},
{
"type": "toggle",
"key": "toggleHeadings",
"description": "Shows indent guides for Notion's toggle heading blocks.",
"value": true
},
{
"type": "toggle",
"key": "tableOfContents",
"description": "Shows indent guides for Notion's table of contents blocks.",
"value": true
},
{
"type": "toggle",
"key": "outliner",
"description": "Shows indent guides for the Outliner's table of contents in the side panel.",
"value": true
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"]
}

View File

@ -1,101 +0,0 @@
/**
* notion-enhancer: indentation lines
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
.notion-page-content .notion-bulleted_list-block > div > div:last-child,
.notion-page-content .notion-numbered_list-block > div > div:last-child,
.notion-page-content .notion-to_do-block > div > div:last-child,
.notion-page-content .notion-toggle-block > div > div:last-child,
.notion-page-content [class$='header-block'] > div > div > div:last-child,
.notion-page-content .notion-table_of_contents-block > div > div > a > div > div {
position: relative;
}
.notion-page-content .notion-bulleted_list-block > div > div:last-child::before,
.notion-page-content .notion-numbered_list-block > div > div:last-child::before,
.notion-page-content .notion-to_do-block > div > div:last-child::before,
.notion-page-content .notion-toggle-block > div > div:last-child::before,
.notion-page-content [class$='header-block'] > div > div > div:last-child::before,
.notion-page-content .pseudoSelection > div > div:last-child::before {
content: '';
position: absolute;
height: calc(100% - 2em);
top: 2em;
margin-inline-start: -14.48px;
}
.notion-page-content [class$='header-block'] > div > div > div:last-child::before {
margin-inline-start: -15.48px;
}
.notion-page-content
.notion-table_of_contents-block
> div
> div
> a
> div
> div:not([style*='margin-left: 0px'])
> div::before {
content: '';
position: absolute;
height: 100%;
top: 0;
margin-inline-start: -14.48px;
}
/* add background to block dragger */
.notion-frame
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
[data-block-id],
.notion-peek-renderer
> div
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
[data-block-id] {
background: var(--theme--bg) !important;
border-radius: 3px;
}
.notion-frame
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable,
.notion-peek-renderer
> div
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable {
left: -42px !important;
background: transparent !important;
}
.notion-frame
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable
> .plus,
.notion-peek-renderer
> div
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable
> .plus {
border-radius: 3px;
width: 18px !important;
padding: 0 1px !important;
background: var(--theme--bg) !important;
}
.notion-frame
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable
> .plus:hover,
.notion-peek-renderer
> div
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
+ .notion-focusable
> .plus:hover {
background: var(--theme--ui_interactive-hover) !important;
}

View File

@ -1,109 +0,0 @@
/**
* notion-enhancer: indentation lines
* (c) 2020 Alexa Baldon <alnbaldon@gmail.com> (https://github.com/runargs)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
export default async function ({ web }, db) {
let style = 'solid',
opacity = 1,
rainbow = false;
switch (await db.get(['style'])) {
case 'dashed':
style = 'dashed';
break;
case 'dotted':
style = 'dotted';
break;
case 'soft':
opacity = 0.25;
break;
case 'rainbow':
opacity = 0.7;
rainbow = true;
break;
}
let css = '';
const colors = ['red', 'pink', 'purple', 'blue', 'green', 'yellow'];
colors.push(...colors, ...colors, ...colors, 'gray');
for (const listType of ['bulleted_list', 'numbered_list', 'to_do', 'toggle']) {
if (!(await db.get([listType]))) continue;
css += `
.notion-page-content .notion-${listType}-block > div > div:last-child::before {
border-left: 1px ${style} var(--indentation_lines--color, currentColor);
opacity: ${opacity};
}`;
if (rainbow) {
for (let i = 0; i < colors.length; i++) {
css += `
.notion-page-content ${`.notion-${listType}-block `.repeat(i + 1)}
> div > div:last-child::before {
--indentation_lines--color: var(--theme--text_${colors[i]});
}`;
}
}
}
if (await db.get(['toggle_header'])) {
css += `
.notion-page-content [class$=header-block] > div > div > div:last-child::before {
border-left: 1px ${style} var(--indentation_lines--color, currentColor);
opacity: ${opacity};
}`;
if (rainbow) {
for (let i = 0; i < colors.length; i++) {
css += `
.notion-page-content ${`[class$=header-block] `.repeat(i + 1)}
> div > div > div:last-child::before{
--indentation_lines--color: var(--theme--text_${colors[i]});
}`;
}
}
}
if (await db.get(['table_of_contents'])) {
css += `
.notion-page-content .notion-table_of_contents-block > div > div > a > div
> div:not([style*='margin-left: 0px']) > div::before {
border-left: 1px ${style} var(--indentation_lines--color, currentColor);
opacity: ${opacity};
}`;
if (rainbow) {
css += `
.notion-page-content .notion-table_of_contents-block > div > div > a > div
> div[style*='margin-left: 24px'] > div::before {
--indentation_lines--color: var(--theme--text_${colors[0]});
}
.notion-page-content .notion-table_of_contents-block > div > div > a > div
> div[style*='margin-left: 48px'] > div::before {
--indentation_lines--color: var(--theme--text_${colors[1]});
}`;
}
}
if (await db.get(['outliner'])) {
css += `
.outliner--header:not([style='--outliner--indent:0px;'])::before {
border-left: 1px ${style} var(--indentation_lines--color, currentColor);
opacity: ${opacity};
}`;
if (rainbow) {
css += `
.outliner--header[style='--outliner--indent:18px;']::before {
--indentation_lines--color: var(--theme--text_${colors[0]});
}
.outliner--header[style='--outliner--indent:36px;']::before {
--indentation_lines--color: var(--theme--text_${colors[1]});
}`;
}
}
web.render(document.head, web.html`<style>${css}</style>`);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,72 +0,0 @@
{
"name": "indentation lines",
"id": "35815b3b-3916-4dc6-8769-c9c2448f8b57",
"version": "0.2.0",
"description": "adds vertical relationship lines to make list trees easier to follow.",
"preview": "indentation-lines.jpg",
"tags": ["extension", "usability"],
"authors": [
{
"name": "runargs",
"email": "alnbaldon@gmail.com",
"homepage": "http://github.com/runargs",
"avatar": "https://avatars.githubusercontent.com/u/39810066"
}
],
"css": {
"client": ["client.css"]
},
"js": {
"client": ["client.mjs"]
},
"options": [
{
"type": "select",
"key": "style",
"label": "style",
"values": ["solid", "dashed", "dotted", "soft", "rainbow"]
},
{
"type": "toggle",
"key": "bulleted_list",
"label": "bulleted lists",
"value": true
},
{
"type": "toggle",
"key": "numbered_list",
"label": "numbered lists",
"value": true
},
{
"type": "toggle",
"key": "to_do",
"label": "to-do lists",
"value": true
},
{
"type": "toggle",
"key": "toggle",
"label": "toggle lists",
"value": true
},
{
"type": "toggle",
"key": "toggle_header",
"label": "toggle headers",
"value": true
},
{
"type": "toggle",
"key": "table_of_contents",
"label": "tables of contents",
"value": true
},
{
"type": "toggle",
"key": "outliner",
"label": "outliner (panel extension)",
"value": true
}
]
}

View File

@ -20,13 +20,13 @@
{ {
"type": "toggle", "type": "toggle",
"key": "numberSingleLines", "key": "numberSingleLines",
"description": "Add line numbers to code blocks with only one line.", "description": "Adds line numbers to code blocks with only one line.",
"value": true "value": true
}, },
{ {
"type": "select", "type": "select",
"key": "decorationStyle", "key": "decorationStyle",
"description": "Decorate line numbers with additional styling to distinguish them from code block content.", "description": "Decorates line numbers with additional styling to distinguish them from code block content.",
"values": ["Border", "Background", "None"] "values": ["Border", "Background", "None"]
} }
], ],

View File

@ -40,6 +40,17 @@ export default async (api, db) => {
</section>`, </section>`,
}); });
const replaceFloatingOutline = await db.get("replaceFloatingOutline");
if (replaceFloatingOutline) {
document.head.append(html`<style>
.hide-scrollbar.ignore-scrolling-container:has(
div:empty[style*="width"]
) {
display: none !important;
}
</style>`);
}
let $page, $scroller; let $page, $scroller;
const getHeadings = () => { const getHeadings = () => {
if (!$page) return []; if (!$page) return [];
@ -64,7 +75,7 @@ export default async (api, db) => {
}, },
getBlockOffset = ($block) => { getBlockOffset = ($block) => {
let offset = 0; let offset = 0;
while (!$block.matches("[data-content-editable-root]")) { while (!$block?.matches("[data-content-editable-root]")) {
offset += $block.offsetTop; offset += $block.offsetTop;
$block = $block.offsetParent; $block = $block.offsetParent;
} }
@ -73,10 +84,17 @@ export default async (api, db) => {
updateHeadings = debounce(() => { updateHeadings = debounce(() => {
$toc.innerHTML = ""; $toc.innerHTML = "";
if (!$page) return; if (!$page) return;
let indent = 0,
prev_level = 0;
const $frag = document.createDocumentFragment(); const $frag = document.createDocumentFragment();
for (const $heading of getHeadings()) { for (const $heading of getHeadings()) {
const level = getHeadingLevel($heading);
if (level === 1) indent = 1;
else if (level > prev_level) indent = Math.min(indent + 1, level);
else if (level < prev_level) indent = Math.max(indent - 1, level);
prev_level = level;
$heading._$outline = html`<${Heading} $heading._$outline = html`<${Heading}
indent=${getHeadingLevel($heading)} ...${{ indent }}
onclick=${() => { onclick=${() => {
if (!$scroller) return; if (!$scroller) return;
const top = getBlockOffset($heading) - 24; const top = getBlockOffset($heading) - 24;

View File

@ -3,7 +3,9 @@
"version": "0.5.0", "version": "0.5.0",
"id": "87e077cc-5402-451c-ac70-27cc4ae65546", "id": "87e077cc-5402-451c-ac70-27cc4ae65546",
"description": "Adds a table of contents to the side panel to overview and navigate the current page's headings and subheadings.", "description": "Adds a table of contents to the side panel to overview and navigate the current page's headings and subheadings.",
"tags": ["panel"], "tags": [
"panel"
],
"authors": [ "authors": [
{ {
"name": "dragonwocky", "name": "dragonwocky",
@ -28,7 +30,15 @@
"key": "equationRendering", "key": "equationRendering",
"description": "Attempts to render special symbols from inline equations in headings. Note that position- and size-based formatting will be lost when displaying equations in the Outliner's table of contents. Disable this to display the raw TeX equation instead.", "description": "Attempts to render special symbols from inline equations in headings. Note that position- and size-based formatting will be lost when displaying equations in the Outliner's table of contents. Disable this to display the raw TeX equation instead.",
"value": true "value": true
},
{
"type": "toggle",
"key": "replaceFloatingOutline",
"description": "Disables Notion's builtin floating table of contents for a complete switch to the Outliner.",
"value": true
} }
], ],
"clientScripts": ["client.mjs"] "clientScripts": [
} "client.mjs"
]
}

View File

@ -15,7 +15,7 @@ const pinLabel = "Pin always on top",
export default async function (api, db) { export default async function (api, db) {
const { html, sendMessage } = api, const { html, sendMessage } = api,
{ addMutationListener, removeMutationListener } = api, { addMutationListener } = api,
displayLabel = ($btn) => { displayLabel = ($btn) => {
if ($btn.innerHTML === $btn.ariaLabel) return; if ($btn.innerHTML === $btn.ariaLabel) return;
$btn.style.width = "auto"; $btn.style.width = "auto";
@ -104,6 +104,7 @@ export default async function (api, db) {
} }
}); });
if (!["linux", "win32", "darwin"].includes(api.platform)) return;
const alwaysOnTopButton = await db.get("alwaysOnTopButton"); const alwaysOnTopButton = await db.get("alwaysOnTopButton");
if (alwaysOnTopButton === "Disabled") return; if (alwaysOnTopButton === "Disabled") return;
const notionTopbar = ".notion-topbar", const notionTopbar = ".notion-topbar",

View File

@ -3,7 +3,7 @@
"name": "notion-enhancer", "name": "notion-enhancer",
"version": "0.11.1", "version": "0.11.1",
"author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)", "author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
"description": "An enhancer/customiser for the all-in-one productivity workspace Notion", "description": "Customise the all-in-one productivity workspace Notion.",
"homepage_url": "https://notion-enhancer.github.io", "homepage_url": "https://notion-enhancer.github.io",
"content_scripts": [{ "matches": ["*://*.notion.so/*"], "js": ["/init.js"] }], "content_scripts": [{ "matches": ["*://*.notion.so/*"], "js": ["/init.js"] }],
"background": { "service_worker": "/worker.js" }, "background": { "service_worker": "/worker.js" },

View File

@ -7,6 +7,7 @@
"extensions/outliner", "extensions/outliner",
"extensions/word-counter", "extensions/word-counter",
"extensions/line-numbers", "extensions/line-numbers",
"extensions/indent-guides",
"extensions/right-to-left", "extensions/right-to-left",
"extensions/no-peeking", "extensions/no-peeking",
"extensions/focus", "extensions/focus",

View File

@ -113,15 +113,31 @@ const initDatabase = async () => {
}; };
if (IS_ELECTRON) { if (IS_ELECTRON) {
const { ipcMain } = require("electron"), const { reloadApp, enhancerUrl } = globalThis.__enhancerApi,
{ reloadApp } = globalThis.__enhancerApi; { ipcMain, session, app, net } = require("electron");
ipcMain.handle("notion-enhancer", ({}, message) => { app.on("ready", () => {
if (message?.action !== "query-database") return; // proxies notion-enhancer sources over www.notion.so via https
const { namespace, query, args } = message.data; const { protocol } = session.fromPartition("persist:notion");
return queryDatabase(namespace, query, args); protocol.handle("https", (req) => {
}); if (req.url.startsWith(enhancerUrl())) {
ipcMain.on("notion-enhancer", ({}, message) => { let url = req.url.slice(enhancerUrl().length);
if (message === "reload-app") reloadApp(); url = `file://${require("path").join(__dirname, url)}`;
return net.fetch(url);
} else return net.fetch(req);
});
// webRequest.onHeadersReceived(({ details: { responseHeaders }, callback }) => {
// delete responseHeaders["content-security-policy"];
// return callback({ responseHeaders });
// });
ipcMain.handle("notion-enhancer", ({}, message) => {
if (message?.action !== "query-database") return;
const { namespace, query, args } = message.data;
return queryDatabase(namespace, query, args);
});
ipcMain.on("notion-enhancer", ({}, message) => {
if (message === "reload-app") reloadApp();
});
}); });
} else { } else {
const notionUrl = "https://www.notion.so/", const notionUrl = "https://www.notion.so/",