notion-enhancer/src/core/menu/menu.mjs
dragonwocky 71f9ecc32b
feat: custom userscripts in-app and in-browser!
firefox support will involve some tweaking, manifest v3 not fully supported yet
2024-04-22 22:58:50 +10:00

212 lines
7.0 KiB
JavaScript

/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
import { checkForUpdate, isDevelopmentBuild } from "../updateCheck.mjs";
import { Sidebar } from "./islands/Sidebar.mjs";
import { Footer } from "./islands/Footer.mjs";
import { Banner } from "./islands/Banner.mjs";
import { Onboarding } from "./islands/Onboarding.mjs";
import { Telemetry } from "./islands/Telemetry.mjs";
import { View } from "./islands/View.mjs";
import { List } from "./islands/List.mjs";
import { Mod } from "./islands/Mod.mjs";
import { Options } from "./islands/Options.mjs";
import { Profiles } from "./islands/Profiles.mjs";
let _apiImport, //
_renderStarted,
_stateHookedInto,
_hotkeyRegistered;
const categories = [
{
icon: "palette",
id: "themes",
title: "Themes",
description: `Themes override Notion's colour schemes. Dark themes require
Notion to be in dark mode and light themes require Notion to be in light
mode. To switch between dark mode and light mode, go to <mark>Settings &
members → My notifications & settings → My settings → Appearance</mark>.`,
},
{
icon: "zap",
id: "extensions",
title: "Extensions",
description: `Extensions add to the functionality and layout of the Notion
client, interacting with and modifying existing interfaces.`,
},
// {
// icon: "plug",
// id: "integrations",
// title: "Integrations",
// description: `<span class="text-[color:var(--theme--fg-red)]">
// Integrations access and modify Notion content. They interact directly with
// <mark>https://www.notion.so/api/v3</mark>. Use at your own risk.</span>`,
// },
],
sidebar = [
"notion-enhancer",
{
id: "welcome",
title: "Welcome",
icon: "notion-enhancer",
},
{
icon: "message-circle",
title: "Community",
href: "https://discord.gg/sFWPXtA",
},
{
icon: "clock",
title: "Changelog",
href: "https://notion-enhancer.github.io/about/changelog/",
},
{
icon: "book",
title: "Documentation",
href: "https://notion-enhancer.github.io/",
},
{
icon: "github",
title: "Source Code",
href: "https://github.com/notion-enhancer",
},
"Settings",
{
id: "core",
title: "Core",
icon: "sliders-horizontal",
disableUntilAgreedToTerms: true,
},
...categories.map((c) => ({
id: c.id,
title: c.title,
icon: c.icon,
disableUntilAgreedToTerms: true,
})),
];
const renderMenu = async () => {
const { html, setState, useState } = globalThis.__enhancerApi,
{ getMods, isEnabled, setEnabled } = globalThis.__enhancerApi,
[theme, icon] = useState(["theme", "icon"]);
if (!theme || !icon || _renderStarted) return;
if (icon === "Monochrome") sidebar[1].icon += "?mask";
_renderStarted = true;
const mods = await getMods();
for (let i = 0; i < categories.length; i++) {
const { id } = categories[i];
categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`));
categories[i].view = html`<${View} id=${id}>
<${List} ...${categories[i]} />
<//>`;
}
for (let i = 0; i < mods.length; i++) {
const options = mods[i].options?.filter((opt) => opt.type !== "heading");
if (mods[i]._src === "core" || !options?.length) continue;
const _get = () => isEnabled(mods[i].id),
_set = async (enabled) => {
await setEnabled(mods[i].id, enabled);
setState({ rerender: true });
};
mods[i].view = html`<${View} id=${mods[i].id}>
<!-- passing an empty options array hides the settings button -->
<${Mod} ...${{ ...mods[i], options: [], _get, _set }} />
<${Options} mod=${mods[i]} />
<//>`;
}
const $sidebar = html`<${Sidebar}
items=${sidebar}
categories=${categories}
/>`,
$main = html`
<main
class="flex-(~ col) overflow-hidden transition-[height]"
style="height: calc(100% + 65px)"
>
<!-- wrappers necessary for transitions and breakpoints -->
<div class="grow overflow-auto">
<div class="relative h-full w-full">
<${View} id="welcome">
<${Banner}
updateAvailable=${await checkForUpdate()}
isDevelopmentBuild=${await isDevelopmentBuild()}
/>
<${Onboarding} />
<//>
<${View} id="core">
<${Options} mod=${mods.find(({ _src }) => _src === "core")} />
<${Telemetry} />
<${Profiles} />
<//>
${[...categories, ...mods]
.filter(({ view }) => view)
.map(({ view }) => view)}
</div>
</div>
<${Footer} categories=${categories} />
</main>
`;
useState(["footerOpen"], ([footerOpen]) => {
$main.style.height = footerOpen ? "100%" : "calc(100% + 65px)";
});
const $skeleton = document.querySelector("#skeleton");
$skeleton.replaceWith($sidebar, $main);
},
registerHotkey = ([hotkey]) => {
const { addKeyListener, setState, useState } = globalThis.__enhancerApi;
if (!hotkey || _hotkeyRegistered) return;
_hotkeyRegistered = true;
addKeyListener(hotkey, (event) => {
event.preventDefault();
const msg = { channel: "notion-enhancer", action: "open-menu" };
parent?.postMessage(msg, "*");
});
addKeyListener("Escape", () => {
const [popupOpen] = useState(["popupOpen"]);
if (document.activeElement?.tagName === "INPUT") {
document.activeElement.blur();
} else if (!popupOpen) {
const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*");
} else setState({ rerender: true });
});
},
updateTheme = ([theme]) => {
if (theme === "dark") document.body.classList.add("dark");
if (theme === "light") document.body.classList.remove("dark");
};
const importApi = () => {
return (_apiImport ??= (async () => {
const api = globalThis.__enhancerApi;
if (typeof api === "undefined") await import("../../api/system.js");
await import("../../load.mjs").then((i) => i.default);
})());
},
hookIntoState = () => {
if (_stateHookedInto) return;
_stateHookedInto = true;
const { useState } = globalThis.__enhancerApi;
useState(["theme"], updateTheme);
useState(["hotkey"], registerHotkey);
useState(["rerender"], renderMenu);
};
addEventListener("message", async (event) => {
if (event.data?.channel !== "notion-enhancer") return;
await importApi().then(hookIntoState);
const { setState, useState } = globalThis.__enhancerApi;
setState({
rerender: true,
hotkey: event.data?.hotkey ?? useState(["hotkey"])[0],
theme: event.data?.theme ?? useState(["theme"])[0],
icon: event.data?.icon ?? useState(["icon"])[0],
});
});