mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-04 12:49:03 +00:00
feat(menu): indicate updates with popup & notification pings
This commit is contained in:
parent
0daf0a38c2
commit
3cd8ed7703
@ -72,6 +72,7 @@ const encodeSvg = (svg) =>
|
||||
};
|
||||
};
|
||||
twind.install({
|
||||
darkMode: "class",
|
||||
rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
|
||||
variants: [
|
||||
// https://github.com/tw-in-js/twind/blob/main/packages/preset-ext/src/variants.ts
|
||||
@ -558,5 +559,22 @@ const h = (type, props, ...children) => {
|
||||
},
|
||||
html = htm.bind(h);
|
||||
|
||||
const extendProps = (props, extend) => {
|
||||
for (const key in extend) {
|
||||
const { [key]: userProvided } = props;
|
||||
if (typeof extend[key] === "function") {
|
||||
props[key] = (...args) => {
|
||||
extend[key](...args);
|
||||
userProvided?.(...args);
|
||||
};
|
||||
} else if (key === "class") {
|
||||
if (userProvided) props[key] += " ";
|
||||
if (!userProvided) props[key] = "";
|
||||
props[key] += extend[key];
|
||||
} else props[key] = extend[key] ?? userProvided;
|
||||
}
|
||||
return props;
|
||||
};
|
||||
|
||||
globalThis.__enhancerApi ??= {};
|
||||
Object.assign(globalThis.__enhancerApi, { html });
|
||||
Object.assign(globalThis.__enhancerApi, { html, extendProps });
|
||||
|
@ -4,8 +4,46 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { checkForUpdate } from "./update.mjs";
|
||||
|
||||
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`;
|
||||
|
||||
function SidebarButton(
|
||||
{ icon, notifications, themeOverridesLoaded, ...props },
|
||||
...children
|
||||
) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
return html`<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="notion-enhancer--menu-button
|
||||
flex select-none cursor-pointer rounded-[3px]
|
||||
text-[14px] my-px mx-[4px] py-[2px] px-[10px]
|
||||
transition hover:bg-[color:var(--theme--bg-hover)]"
|
||||
...${props}
|
||||
>
|
||||
<div class="flex items-center justify-center w-[22px] h-[22px] mr-[8px]">
|
||||
<i class="i-${icon}"></i>
|
||||
</div>
|
||||
<div>${children}</div>
|
||||
|
||||
<div class="ml-auto my-auto${notifications > 0 ? "" : " hidden"}">
|
||||
<!-- accents are squashed into one variable for theming:
|
||||
use rgb to match notion if overrides not loaded -->
|
||||
<div
|
||||
class="flex justify-center w-[16px] h-[16px] font-semibold
|
||||
text-([10px] [color:var(--theme--accent-secondary\\_contrast)])
|
||||
bg-[color:var(--theme--accent-secondary)] rounded-[3px] mb-[2px]
|
||||
dark:bg-[color:${themeOverridesLoaded
|
||||
? "var(--theme--accent-secondary)"
|
||||
: "rgb(180,65,60)"}]"
|
||||
>
|
||||
<span class="ml-[-0.5px]">${notifications}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export default async (api, db) => {
|
||||
const {
|
||||
html,
|
||||
@ -109,24 +147,14 @@ export default async (api, db) => {
|
||||
</div>`;
|
||||
document.body.append($menuModal);
|
||||
|
||||
const $menuButton = html`<div
|
||||
const $menuButton = html`<${SidebarButton}
|
||||
onclick=${openMenu}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="notion-enhancer--menu-button
|
||||
flex select-none cursor-pointer rounded-[3px]
|
||||
text-[14px] my-px mx-[4px] py-[2px] px-[10px]
|
||||
transition hover:bg-[color:var(--theme--bg-hover)]"
|
||||
>
|
||||
<div class="flex items-center justify-center w-[22px] h-[22px] mr-[8px]">
|
||||
<i
|
||||
class="i-notion-enhancer${menuButtonIconStyle === "Monochrome"
|
||||
? "?mask"
|
||||
: " text-[16px]"}"
|
||||
></i>
|
||||
</div>
|
||||
<div>notion-enhancer</div>
|
||||
</div>`;
|
||||
notifications=${(await checkForUpdate()) ? 1 : 0}
|
||||
icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
|
||||
? "?mask"
|
||||
: " text-[16px]"}"
|
||||
>notion-enhancer
|
||||
<//>`;
|
||||
addMutationListener(notionSidebar, () => {
|
||||
if (document.contains($menuButton)) return;
|
||||
document.querySelector(notionSidebar)?.append($menuButton);
|
||||
|
@ -4,10 +4,8 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps } from "../state.mjs";
|
||||
|
||||
function Button({ icon, variant, tagName, ...props }, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-button shrink-0
|
||||
flex gap-[8px] items-center px-[12px] rounded-[4px]
|
||||
|
@ -4,10 +4,10 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { useState, extendProps } from "../state.mjs";
|
||||
import { useState } from "../state.mjs";
|
||||
|
||||
function Checkbox({ _get, _set, ...props }) {
|
||||
const { html } = globalThis.__enhancerApi,
|
||||
const { html, extendProps } = globalThis.__enhancerApi,
|
||||
$input = html`<input
|
||||
type="checkbox"
|
||||
class="hidden checked:sibling:(px-px
|
||||
|
@ -4,10 +4,8 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps } from "../state.mjs";
|
||||
|
||||
function Description(props, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-description typography
|
||||
leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
|
||||
|
@ -4,10 +4,8 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps } from "../state.mjs";
|
||||
|
||||
function Heading(props, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `notion-enhancer--menu-heading text-[16px]
|
||||
font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
|
||||
|
@ -4,7 +4,7 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps, useState } from "../state.mjs";
|
||||
import { useState } from "../state.mjs";
|
||||
|
||||
const updateHotkey = (event) => {
|
||||
const keys = [];
|
||||
@ -75,7 +75,7 @@ function Input({
|
||||
...props
|
||||
}) {
|
||||
let $filename, $clear;
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
Coloris({ format: "rgb" });
|
||||
|
||||
type ??= "text";
|
||||
|
@ -4,15 +4,15 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { setState, useState, extendProps } from "../state.mjs";
|
||||
import { setState, useState } from "../state.mjs";
|
||||
|
||||
function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
|
||||
const { html } = globalThis.__enhancerApi,
|
||||
const { html, extendProps } = globalThis.__enhancerApi,
|
||||
$popup = html`<div
|
||||
class="notion-enhancer--menu-popup
|
||||
group absolute top-0 left-0 w-full h-full
|
||||
flex flex-col justify-center items-end
|
||||
pointer-events-none z-20"
|
||||
flex-(& col) justify-center items-end z-20
|
||||
pointer-events-none font-normal text-left"
|
||||
>
|
||||
<div class="relative right-[100%]">
|
||||
<div
|
||||
@ -34,10 +34,10 @@ function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
|
||||
onopen?.();
|
||||
};
|
||||
$popup.hide = () => {
|
||||
onbeforeclose?.();
|
||||
$popup.removeAttribute("open");
|
||||
$popup.style.pointerEvents = "auto";
|
||||
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||
onbeforeclose?.();
|
||||
setTimeout(() => {
|
||||
$popup.style.pointerEvents = "";
|
||||
setState({ popupOpen: false });
|
||||
|
@ -4,7 +4,7 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { useState, extendProps } from "../state.mjs";
|
||||
import { useState } from "../state.mjs";
|
||||
import { Popup } from "./Popup.mjs";
|
||||
|
||||
function Option({ value, _get, _set }) {
|
||||
|
@ -4,10 +4,8 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps } from "../state.mjs";
|
||||
|
||||
function Tile({ icon, title, tagName, ...props }, ...children) {
|
||||
const { html } = globalThis.__enhancerApi;
|
||||
const { html, extendProps } = globalThis.__enhancerApi;
|
||||
extendProps(props, {
|
||||
class: `flex items-center gap-[12px] px-[16px] py-[12px]
|
||||
bg-[color:var(--theme--bg-secondary)] hover:bg-[color:var(--theme--bg-hover)]
|
||||
|
@ -4,10 +4,10 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { useState, extendProps } from "../state.mjs";
|
||||
import { useState } from "../state.mjs";
|
||||
|
||||
function Toggle({ _get, _set, ...props }) {
|
||||
const { html } = globalThis.__enhancerApi,
|
||||
const { html, extendProps } = globalThis.__enhancerApi,
|
||||
$input = html`<input
|
||||
type="checkbox"
|
||||
class="hidden checked:sibling:children:(
|
||||
|
@ -4,10 +4,14 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Popup } from "../components/Popup.mjs";
|
||||
import { Button } from "../components/Button.mjs";
|
||||
import { Description } from "../components/Description.mjs";
|
||||
import { useState } from "../state.mjs";
|
||||
|
||||
const updateGuide =
|
||||
"https://notion-enhancer.github.io/getting-started/updating/";
|
||||
|
||||
const rectToStyle = (rect) =>
|
||||
["width", "height", "top", "bottom", "left", "right"]
|
||||
.filter((prop) => rect[prop])
|
||||
@ -63,9 +67,50 @@ function Circle(rect) {
|
||||
></div>`;
|
||||
}
|
||||
|
||||
function Banner() {
|
||||
function Banner({ updateAvailable, isDevelopmentBuild }) {
|
||||
const { html, version, initDatabase } = globalThis.__enhancerApi,
|
||||
$welcome = html`<div
|
||||
$version = html`<button
|
||||
class="text-[12px] py-[2px] px-[6px] mt-[2px]
|
||||
font-medium leading-tight tracking-wide rounded-[3px]
|
||||
relative bg-purple-500 from-white/[0.18] to-white/[0.16]
|
||||
bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
|
||||
>
|
||||
<div
|
||||
class="notion-enhancer--menu-update-indicator
|
||||
absolute h-[12px] w-[12px] right-[-6px] top-[-6px]
|
||||
${updateAvailable ? "" : "hidden"}"
|
||||
>
|
||||
<span
|
||||
class="block rounded-full h-full w-full
|
||||
absolute bg-purple-500/75 animate-ping"
|
||||
></span>
|
||||
<span
|
||||
class="block rounded-full h-full w-full
|
||||
relative bg-purple-500"
|
||||
></span>
|
||||
</div>
|
||||
<span class="relative">v${version}</span>
|
||||
</button>`,
|
||||
$popup = html`<${Popup} trigger=${$version}>
|
||||
<p
|
||||
class="typography py-[2px] px-[8px] text-[14px]"
|
||||
innerHTML=${updateAvailable
|
||||
? `<b>v${updateAvailable}</b> is available! <a href="${updateGuide}">Update now.</a>`
|
||||
: isDevelopmentBuild
|
||||
? "This is a development build of the notion-enhancer. It may be unstable."
|
||||
: "You're up to date!"}
|
||||
/>
|
||||
<//>`;
|
||||
$version.append($popup);
|
||||
if (updateAvailable) {
|
||||
useState(["focus", "view"], ([, view = "welcome"]) => {
|
||||
if (view !== "welcome") return;
|
||||
// delayed appearance = movement attracts eye
|
||||
setTimeout(() => $version.lastElementChild.show(), 400);
|
||||
});
|
||||
}
|
||||
|
||||
const $welcome = html`<div
|
||||
class="relative flex overflow-hidden h-[192px] rounded-t-[4px]
|
||||
border-(& purple-400) bg-purple-500 from-white/20 to-transparent
|
||||
text-white bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
|
||||
@ -77,9 +122,8 @@ function Banner() {
|
||||
<${Star} width="36px" height="36px" top="136px" left="190px" />
|
||||
<${Star} width="48px" height="48px" top="32px" left="336px" />
|
||||
<${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
|
||||
|
||||
<h1
|
||||
class="z-10 pl-[32px] md:pl-[48px] lg:pl-[64px]
|
||||
class="z-10 px-[32px] md:px-[48px] lg:px-[64px]
|
||||
font-bold leading-tight tracking-tight my-auto"
|
||||
>
|
||||
<a href="https://notion-enhancer.github.io/">
|
||||
@ -87,21 +131,14 @@ function Banner() {
|
||||
<span class="text-[28px]">the notion-enhancer</span>
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<div
|
||||
class="flex flex-col absolute bottom-0 right-0
|
||||
pr-[32px] md:pr-[48px] lg:pr-[64px] pb-[24px]"
|
||||
class="absolute bottom-0 right-0 py-[24px]
|
||||
px-[32px] md:px-[48px] lg:px-[64px]"
|
||||
>
|
||||
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
|
||||
<a
|
||||
href="https://github.com/notion-enhancer/notion-enhancer/releases/tag/v${version}"
|
||||
>
|
||||
<span
|
||||
class="text-[12px] py-[2px] px-[6px]
|
||||
font-medium leading-tight tracking-wide"
|
||||
>v${version}
|
||||
</span>
|
||||
</a>
|
||||
<div class="relative flex-(& col)">
|
||||
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
|
||||
${$version}
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
$sponsorship = html`<div
|
||||
@ -118,16 +155,14 @@ function Banner() {
|
||||
variant="brand"
|
||||
class="grow justify-center"
|
||||
href="https://www.buymeacoffee.com/dragonwocky"
|
||||
>
|
||||
Buy me a coffee
|
||||
>Buy me a coffee
|
||||
<//>
|
||||
<${Button}
|
||||
icon="calendar-heart"
|
||||
variant="brand"
|
||||
class="grow justify-center"
|
||||
href="https://github.com/sponsors/dragonwocky"
|
||||
>
|
||||
Sponsor me
|
||||
>Sponsor me
|
||||
<//>
|
||||
</div>
|
||||
<!-- Disclaimer: these perks are only a draft, for anyone reading this.
|
||||
@ -140,7 +175,6 @@ function Banner() {
|
||||
the instructions in the <b>#welcome</b> channel.
|
||||
<//>
|
||||
</div>`;
|
||||
|
||||
initDatabase()
|
||||
.get("agreedToTerms")
|
||||
.then((agreedToTerms) => {
|
||||
|
@ -62,7 +62,7 @@ function List({ id, mods, description }) {
|
||||
};
|
||||
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
||||
});
|
||||
return html`<div class="flex flex-col gap-y-[14px]">
|
||||
return html`<div class="flex-(& col) gap-y-[14px]">
|
||||
<${Search} items=${$mods} itemType=${id} />
|
||||
<${Description} innerHTML=${description} />
|
||||
${$mods}
|
||||
|
@ -37,7 +37,7 @@ function Mod({
|
||||
class="rounded-[4px] mr-[12px] h-[74px] my-auto"
|
||||
/>`
|
||||
: ""}
|
||||
<div class="flex flex-col max-w-[50%]">
|
||||
<div class="flex-(& col) max-w-[50%]">
|
||||
<div class="flex items-center text-[14px] mb-[5px]">
|
||||
<h3 class="my-0">${name}</h3>
|
||||
${[`v${version}`, ...tags].map((tag) => {
|
||||
@ -45,8 +45,7 @@ function Mod({
|
||||
class="text-([12px] [color:var(--theme--fg-secondary)])
|
||||
ml-[8px] py-[2px] px-[6px] leading-tight tracking-wide
|
||||
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
||||
>
|
||||
${tag}
|
||||
>${tag}
|
||||
</span>`;
|
||||
})}
|
||||
</div>
|
||||
|
@ -4,7 +4,6 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { Heading } from "../components/Heading.mjs";
|
||||
import { Description } from "../components/Description.mjs";
|
||||
import { Checkbox } from "../components/Checkbox.mjs";
|
||||
import { Button } from "../components/Button.mjs";
|
||||
@ -68,7 +67,7 @@ function Onboarding() {
|
||||
>Build your own extension.
|
||||
<//>
|
||||
<${Tile}
|
||||
href="https://github.com/notion-enhancer/notion-enhancer/issues/new?template=BUG_REPORT.md"
|
||||
href="https://github.com/notion-enhancer/notion-enhancer/issues"
|
||||
icon="bug"
|
||||
title="Something not working?"
|
||||
>Report a bug.
|
||||
|
@ -38,7 +38,7 @@ function Option({ _get, _set, ...opt }) {
|
||||
class="notion-enhancer--menu-option flex items-center justify-between
|
||||
mb-[18px] ${opt.type === "toggle" ? "cursor-pointer" : ""}"
|
||||
>
|
||||
<div class="flex flex-col ${opt.type === "text" ? "w-full" : "mr-[10%]"}">
|
||||
<div class="flex-(& col) ${opt.type === "text" ? "w-full" : "mr-[10%]"}">
|
||||
<h5 class="text-[14px] mb-[2px] mt-0">${opt.label}</h5>
|
||||
${opt.type === "text"
|
||||
? html`<${Input}
|
||||
|
@ -133,7 +133,7 @@ function Profile({ id }) {
|
||||
<p class="text-[14px] py-[2px] px-[8px]">
|
||||
Are you sure you want to delete the profile ${$confirmName} permanently?
|
||||
</p>
|
||||
<div class="flex flex-col gap-[8px] py-[6px] px-[8px]">
|
||||
<div class="flex-(& col) gap-[8px] py-[6px] px-[8px]">
|
||||
<${Button}
|
||||
tabindex="0"
|
||||
icon="trash"
|
||||
|
@ -4,7 +4,7 @@
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { extendProps, setState, useState } from "../state.mjs";
|
||||
import { setState, useState } from "../state.mjs";
|
||||
import { Description } from "../components/Description.mjs";
|
||||
|
||||
function SidebarHeading({}, ...children) {
|
||||
@ -19,7 +19,7 @@ function SidebarHeading({}, ...children) {
|
||||
}
|
||||
|
||||
function SidebarButton({ id, icon, ...props }, ...children) {
|
||||
const { html } = globalThis.__enhancerApi,
|
||||
const { html, extendProps } = globalThis.__enhancerApi,
|
||||
$btn = html`<${props["href"] ? "a" : "button"}
|
||||
class="flex items-center select-none text-[14px]
|
||||
py-[5px] px-[15px] last:mb-[12px] w-full transition
|
||||
@ -59,7 +59,7 @@ function Sidebar({ items, categories }) {
|
||||
policy and terms & conditions on the welcome page.
|
||||
</span>`,
|
||||
$sidebar = html`<aside
|
||||
class="notion-enhancer--menu-sidebar flex flex-col row-span-1
|
||||
class="notion-enhancer--menu-sidebar flex-(& col) row-span-1
|
||||
h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
|
||||
>
|
||||
${items.map((item) => {
|
||||
|
@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { setState, useState } from "./state.mjs";
|
||||
import { checkForUpdate, isDevelopmentBuild } from "../update.mjs";
|
||||
import { Sidebar } from "./islands/Sidebar.mjs";
|
||||
import { Footer } from "./islands/Footer.mjs";
|
||||
import { Banner } from "./islands/Banner.mjs";
|
||||
@ -120,12 +121,15 @@ const render = async () => {
|
||||
categories=${categories}
|
||||
/>`,
|
||||
$main = html`
|
||||
<main class="flex flex-col overflow-hidden transition-[height]">
|
||||
<main class="flex-(& col) overflow-hidden transition-[height]">
|
||||
<!-- wrappers necessary for transitions and breakpoints -->
|
||||
<div class="grow overflow-auto">
|
||||
<div class="relative h-full w-full">
|
||||
<${View} id="welcome">
|
||||
<${Banner} />
|
||||
<${Banner}
|
||||
updateAvailable=${await checkForUpdate()}
|
||||
isDevelopmentBuild=${await isDevelopmentBuild()}
|
||||
/>
|
||||
<${Onboarding} />
|
||||
<//>
|
||||
<${View} id="core">
|
||||
@ -152,7 +156,9 @@ const render = async () => {
|
||||
$skeleton.replaceWith($sidebar, $main);
|
||||
};
|
||||
|
||||
window.addEventListener("focus", () => setState({ rerender: true }));
|
||||
window.addEventListener("focus", () => {
|
||||
setState({ focus: true, rerender: true });
|
||||
});
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data?.namespace !== "notion-enhancer") return;
|
||||
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
|
||||
|
@ -21,21 +21,4 @@ const setState = (state) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const extendProps = (props, extend) => {
|
||||
for (const key in extend) {
|
||||
const { [key]: userProvided } = props;
|
||||
if (typeof extend[key] === "function") {
|
||||
props[key] = (...args) => {
|
||||
extend[key](...args);
|
||||
userProvided?.(...args);
|
||||
};
|
||||
} else if (key === "class") {
|
||||
if (userProvided) props[key] += " ";
|
||||
if (!userProvided) props[key] = "";
|
||||
props[key] += extend[key];
|
||||
} else props[key] = extend[key] ?? userProvided;
|
||||
}
|
||||
return props;
|
||||
};
|
||||
|
||||
export { setState, useState, extendProps };
|
||||
export { setState, useState };
|
||||
|
@ -1,448 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as api from '../../api/index.mjs';
|
||||
import { notifications, $changelogModal } from './notifications.mjs';
|
||||
import { modComponents, options } from './components.mjs';
|
||||
import * as router from './router.mjs';
|
||||
import './styles.mjs';
|
||||
|
||||
(async () => {
|
||||
const { env, fs, storage, electron, registry, web, components } = api;
|
||||
|
||||
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
|
||||
for (let script of mod.js?.menu || []) {
|
||||
script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
|
||||
script.default(api, await registry.db(mod.id));
|
||||
}
|
||||
}
|
||||
const errors = await registry.errors();
|
||||
if (errors.length) {
|
||||
console.error('[notion-enhancer] registry errors:');
|
||||
console.table(errors);
|
||||
const $errNotification = await notifications.add({
|
||||
icon: 'alert-circle',
|
||||
message: 'Failed to load mods (check console).',
|
||||
color: 'red',
|
||||
});
|
||||
if (['win32', 'linux', 'darwin'].includes(env.name)) {
|
||||
$errNotification.addEventListener('click', () => electron.browser.openDevTools());
|
||||
}
|
||||
}
|
||||
|
||||
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
||||
profileName = await registry.profileName(),
|
||||
profileDB = await registry.profileDB();
|
||||
|
||||
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
|
||||
|
||||
globalThis.addEventListener('beforeunload', (_event) => {
|
||||
// trigger input save
|
||||
document.activeElement.blur();
|
||||
});
|
||||
|
||||
const $main = web.html`<main class="main"></main>`,
|
||||
$sidebar = web.html`<article class="sidebar"></article>`,
|
||||
$options = web.html`<div class="options-container">
|
||||
<p class="options-placeholder">Select a mod to view and configure its options.</p>
|
||||
</div>`,
|
||||
$profile = web.html`<button class="profile-trigger">
|
||||
Profile: ${web.escape(profileName)}
|
||||
</button>`;
|
||||
|
||||
// profile
|
||||
|
||||
let _$profileConfig;
|
||||
const openProfileMenu = async () => {
|
||||
if (!_$profileConfig) {
|
||||
const profileNames = [
|
||||
...new Set([
|
||||
...Object.keys(await storage.get(['profiles'], { default: {} })),
|
||||
profileName,
|
||||
]),
|
||||
],
|
||||
$options = profileNames.map(
|
||||
(profile) => web.raw`<option
|
||||
class="select-option"
|
||||
value="${web.escape(profile)}"
|
||||
${profile === profileName ? 'selected' : ''}
|
||||
>${web.escape(profile)}</option>`
|
||||
),
|
||||
$select = web.html`<select class="input">
|
||||
<option class="select-option" value="--">-- new --</option>
|
||||
${$options.join('')}
|
||||
</select>`,
|
||||
$edit = web.html`<input
|
||||
type="text"
|
||||
class="input"
|
||||
value="${web.escape(profileName)}"
|
||||
pattern="/^[A-Za-z0-9_-]+$/"
|
||||
>`,
|
||||
$export = web.html`<button class="profile-export">
|
||||
${await components.feather('download', { class: 'profile-icon-action' })}
|
||||
</button>`,
|
||||
$import = web.html`<label class="profile-import">
|
||||
<input type="file" class="hidden" accept="application/json">
|
||||
${await components.feather('upload', { class: 'profile-icon-action' })}
|
||||
</label>`,
|
||||
$save = web.html`<button class="profile-save">
|
||||
${await components.feather('save', { class: 'profile-icon-text' })} Save
|
||||
</button>`,
|
||||
$delete = web.html`<button class="profile-delete">
|
||||
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
|
||||
</button>`,
|
||||
$error = web.html`<p class="profile-error"></p>`;
|
||||
|
||||
$export.addEventListener('click', async (_event) => {
|
||||
const now = new Date(),
|
||||
$a = web.html`<a
|
||||
class="hidden"
|
||||
download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()}.json"
|
||||
href="data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2)
|
||||
)}"
|
||||
></a>`;
|
||||
web.render(document.body, $a);
|
||||
$a.click();
|
||||
$a.remove();
|
||||
});
|
||||
|
||||
$import.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader();
|
||||
reader.onload = async (progress) => {
|
||||
try {
|
||||
const profileUpload = JSON.parse(progress.currentTarget.result);
|
||||
if (!profileUpload) throw Error;
|
||||
await storage.set(['profiles', $select.value], profileUpload);
|
||||
env.reload();
|
||||
} catch {
|
||||
web.render(web.empty($error), 'Invalid JSON uploaded.');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
$select.addEventListener('change', (_event) => {
|
||||
if ($select.value === '--') {
|
||||
$edit.value = '';
|
||||
} else $edit.value = $select.value;
|
||||
});
|
||||
|
||||
$save.addEventListener('click', async (_event) => {
|
||||
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
`The profile "${web.escape($edit.value)}" already exists.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!$edit.value || !$edit.value.match(/^[A-Za-z0-9_-]+$/)) {
|
||||
web.render(
|
||||
web.empty($error),
|
||||
'Profile names may not be empty & may only contain letters, numbers, hyphens and underscores.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
await storage.set(['currentprofile'], $edit.value);
|
||||
if ($select.value === '--') {
|
||||
await storage.set(['profiles', $edit.value], {});
|
||||
} else if ($select.value !== $edit.value) {
|
||||
await storage.set(
|
||||
['profiles', $edit.value],
|
||||
await storage.get(['profiles', $select.value], {})
|
||||
);
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
}
|
||||
env.reload();
|
||||
});
|
||||
|
||||
$delete.addEventListener('click', async (_event) => {
|
||||
await storage.set(['profiles', $select.value], undefined);
|
||||
await storage.set(
|
||||
['currentprofile'],
|
||||
profileNames.find((profile) => profile !== $select.value) || 'default'
|
||||
);
|
||||
env.reload();
|
||||
});
|
||||
|
||||
_$profileConfig = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.html`<p class="options-placeholder">
|
||||
Profiles are used to switch entire configurations.<br>
|
||||
Be careful - deleting a profile deletes all configuration
|
||||
related to it.<br>
|
||||
</p>`,
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$select,
|
||||
web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<label class="input-label"></label>`,
|
||||
$edit,
|
||||
web.html`${await components.feather('type', { class: 'input-icon' })}`
|
||||
),
|
||||
web.render(
|
||||
web.html`<p class="profile-actions"></p>`,
|
||||
$export,
|
||||
$import,
|
||||
$save,
|
||||
$delete
|
||||
),
|
||||
$error
|
||||
);
|
||||
}
|
||||
web.render(web.empty($options), _$profileConfig);
|
||||
};
|
||||
$profile.addEventListener('click', () => openSidebarMenu('profile'));
|
||||
|
||||
// mods
|
||||
|
||||
const $modLists = {},
|
||||
generators = {
|
||||
options: async (mod) => {
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (const opt of mod.options) {
|
||||
if (!opt.environments.includes(env.name)) continue;
|
||||
web.render($fragment, await options[opt.type](mod, opt));
|
||||
}
|
||||
if (!mod.options.length) {
|
||||
web.render($fragment, web.html`<p class="options-placeholder">No options.</p>`);
|
||||
}
|
||||
return $fragment;
|
||||
},
|
||||
mod: async (mod) => {
|
||||
const $mod = web.html`<div class="mod" data-id="${web.escape(mod.id)}"></div>`,
|
||||
$toggle = modComponents.toggle('', await registry.enabled(mod.id));
|
||||
$toggle.addEventListener('change', async (event) => {
|
||||
if (event.target.checked && mod.tags.includes('theme')) {
|
||||
const mode = mod.tags.includes('light') ? 'light' : 'dark',
|
||||
id = mod.id,
|
||||
mods = await registry.list(
|
||||
async (mod) =>
|
||||
(await registry.enabled(mod.id)) &&
|
||||
mod.tags.includes('theme') &&
|
||||
mod.tags.includes(mode) &&
|
||||
mod.id !== id
|
||||
);
|
||||
for (const mod of mods) {
|
||||
profileDB.set(['_mods', mod.id], false);
|
||||
document.querySelector(
|
||||
`[data-id="${web.escape(mod.id)}"] .toggle-check`
|
||||
).checked = false;
|
||||
}
|
||||
}
|
||||
profileDB.set(['_mods', mod.id], event.target.checked);
|
||||
notifications.onChange();
|
||||
});
|
||||
$mod.addEventListener('click', () => openSidebarMenu(mod.id));
|
||||
return web.render(
|
||||
web.html`<article class="mod-container"></article>`,
|
||||
web.render(
|
||||
$mod,
|
||||
mod.preview
|
||||
? modComponents.preview(
|
||||
mod.preview.startsWith('http')
|
||||
? mod.preview
|
||||
: fs.localPath(`repo/${mod._dir}/${mod.preview}`)
|
||||
)
|
||||
: '',
|
||||
web.render(
|
||||
web.html`<div class="mod-body"></div>`,
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
modComponents.description(mod.description),
|
||||
modComponents.authors(mod.authors),
|
||||
mod.environments.includes(env.name) && !registry.core.includes(mod.id)
|
||||
? $toggle
|
||||
: ''
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
modList: async (category, message = '') => {
|
||||
if (!$modLists[category]) {
|
||||
const $search = web.html`<input type="search" class="search"
|
||||
placeholder="Search ('/' to focus)">`,
|
||||
$list = web.html`<div class="mods-list"></div>`,
|
||||
mods = await registry.list(
|
||||
(mod) => mod.environments.includes(env.name) && mod.tags.includes(category)
|
||||
);
|
||||
web.addHotkeyListener(['/'], () => $search.focus());
|
||||
$search.addEventListener('input', (_event) => {
|
||||
const query = $search.value.toLowerCase();
|
||||
for (const $mod of $list.children) {
|
||||
const matches = !query || $mod.innerText.toLowerCase().includes(query);
|
||||
$mod.classList[matches ? 'remove' : 'add']('hidden');
|
||||
}
|
||||
});
|
||||
for (const mod of mods) {
|
||||
mod.tags = mod.tags.filter((tag) => tag !== category);
|
||||
web.render($list, await generators.mod(mod));
|
||||
mod.tags.unshift(category);
|
||||
}
|
||||
$modLists[category] = web.render(
|
||||
web.html`<div></div>`,
|
||||
web.render(
|
||||
web.html`<label class="search-container"></label>`,
|
||||
$search,
|
||||
web.html`${await components.feather('search', { class: 'input-icon' })}`
|
||||
),
|
||||
message ? web.render(web.html`<p class="main-message"></p>`, message) : '',
|
||||
$list
|
||||
);
|
||||
}
|
||||
return $modLists[category];
|
||||
},
|
||||
};
|
||||
|
||||
async function openModMenu(id) {
|
||||
let $mod;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
$mod = $list.querySelector(`[data-id="${web.escape(id)}"]`);
|
||||
if ($mod) break;
|
||||
}
|
||||
const mod = await registry.get(id);
|
||||
if (!$mod || !mod || $mod.className === 'mod-selected') return;
|
||||
|
||||
$mod.className = 'mod-selected';
|
||||
const fragment = [
|
||||
web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
|
||||
modComponents.tags(mod.tags),
|
||||
await generators.options(mod),
|
||||
];
|
||||
web.render(web.empty($options), ...fragment);
|
||||
}
|
||||
|
||||
// views
|
||||
|
||||
const $notionNavItem = web.html`<h1 class="nav-notion">
|
||||
${(await fs.getText('media/colour.svg')).replace(
|
||||
/width="\d+" height="\d+"/,
|
||||
`class="nav-notion-icon"`
|
||||
)}
|
||||
<span>notion-enhancer</span>
|
||||
</h1>`;
|
||||
$notionNavItem.addEventListener('click', env.focusNotion);
|
||||
|
||||
const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`,
|
||||
$extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
|
||||
$themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`,
|
||||
$integrationsNavItem = web.html`<a href="?view=integrations" class="nav-item">integrations</a>`,
|
||||
$changelogNavItem = web.html`<button class="nav-item nav-changelog">
|
||||
${await components.feather('clock', { class: 'nav-changelog-icon' })}
|
||||
</button>`;
|
||||
components.addTooltip($changelogNavItem, '**Update changelog & welcome message**');
|
||||
$changelogNavItem.addEventListener('click', () => {
|
||||
$changelogModal.scrollTop = 0;
|
||||
$changelogModal.classList.add('modal-visible');
|
||||
});
|
||||
|
||||
web.render(
|
||||
document.body,
|
||||
web.render(
|
||||
web.html`<div class="body-container"></div>`,
|
||||
web.render(
|
||||
web.html`<div class="content-container"></div>`,
|
||||
web.render(
|
||||
web.html`<nav class="nav"></nav>`,
|
||||
$notionNavItem,
|
||||
$coreNavItem,
|
||||
$extensionsNavItem,
|
||||
$themesNavItem,
|
||||
$integrationsNavItem,
|
||||
web.html`<a href="https://notion-enhancer.github.io" target="_blank" class="nav-item">docs</a>`,
|
||||
web.html`<a href="https://discord.gg/sFWPXtA" target="_blank" class="nav-item">community</a>`,
|
||||
$changelogNavItem
|
||||
),
|
||||
$main
|
||||
),
|
||||
web.render($sidebar, $profile, $options)
|
||||
)
|
||||
);
|
||||
|
||||
function selectNavItem($item) {
|
||||
for (const $selected of document.querySelectorAll('.nav-item-selected')) {
|
||||
$selected.className = 'nav-item';
|
||||
}
|
||||
$item.className = 'nav-item-selected';
|
||||
}
|
||||
|
||||
await generators.modList(
|
||||
'core',
|
||||
`Core mods provide the basics required for
|
||||
all other extensions and themes to work. They
|
||||
can't be disabled, but they can be configured
|
||||
- just click on a mod to access its options.`
|
||||
);
|
||||
router.addView('core', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($coreNavItem);
|
||||
return web.render($main, await generators.modList('core'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'extension',
|
||||
`Extensions build on the functionality and layout of
|
||||
the Notion client, modifying and interacting with
|
||||
existing interfaces.`
|
||||
);
|
||||
router.addView('extensions', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($extensionsNavItem);
|
||||
return web.render($main, await generators.modList('extension'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'theme',
|
||||
`Themes change Notion's colour scheme.
|
||||
Dark themes will only work when Notion is in dark mode,
|
||||
and light themes will only work when Notion is in light mode.
|
||||
Only one theme of each mode can be enabled at a time.`
|
||||
);
|
||||
router.addView('themes', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($themesNavItem);
|
||||
return web.render($main, await generators.modList('theme'));
|
||||
});
|
||||
|
||||
await generators.modList(
|
||||
'integration',
|
||||
web.html`<span class="danger">Integrations are extensions that use an unofficial API
|
||||
to access and modify content. They are used just like
|
||||
normal extensions, but may be more dangerous to use.</span>`
|
||||
);
|
||||
router.addView('integrations', async () => {
|
||||
web.empty($main);
|
||||
selectNavItem($integrationsNavItem);
|
||||
return web.render($main, await generators.modList('integration'));
|
||||
});
|
||||
|
||||
router.setDefaultView('extensions');
|
||||
|
||||
router.addQueryListener('id', openSidebarMenu);
|
||||
function openSidebarMenu(id) {
|
||||
if (!id) return;
|
||||
id = web.escape(id);
|
||||
|
||||
const deselectedMods = `.mod-selected:not([data-id="${id}"])`;
|
||||
for (const $list of Object.values($modLists)) {
|
||||
for (const $selected of $list.querySelectorAll(deselectedMods)) {
|
||||
$selected.className = 'mod';
|
||||
}
|
||||
}
|
||||
router.updateQuery(`?id=${id}`);
|
||||
|
||||
if (id === 'profile') {
|
||||
openProfileMenu();
|
||||
} else openModMenu(id);
|
||||
}
|
||||
})();
|
@ -1,146 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: menu
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
import { env, fs, storage, web, components } from '../../api/index.mjs';
|
||||
import { tw } from './styles.mjs';
|
||||
|
||||
import '../../dep/markdown-it.min.js';
|
||||
const md = markdownit({ linkify: true });
|
||||
|
||||
const notificationsURL = 'https://notion-enhancer.github.io/notifications.json';
|
||||
export const notifications = {
|
||||
$container: web.html`<div class="notifications-container"></div>`,
|
||||
async add({ icon, message, id = undefined, color = undefined, link = undefined }) {
|
||||
const $notification = link
|
||||
? web.html`<a
|
||||
href="${web.escape(link)}"
|
||||
class="${tw`notification-${color || 'default'}`}"
|
||||
role="alert"
|
||||
target="_blank"
|
||||
></a>`
|
||||
: web.html`<p
|
||||
class="${tw`notification-${color || 'default'}`}"
|
||||
role="alert"
|
||||
tabindex="0"
|
||||
></p>`,
|
||||
resolve = async () => {
|
||||
if (id !== undefined) {
|
||||
notifications.cache.push(id);
|
||||
await storage.set(['notifications'], notifications.cache);
|
||||
}
|
||||
$notification.remove();
|
||||
};
|
||||
$notification.addEventListener('click', resolve);
|
||||
$notification.addEventListener('keyup', (event) => {
|
||||
if (['Enter', ' '].includes(event.key)) resolve();
|
||||
});
|
||||
web.render(
|
||||
notifications.$container,
|
||||
web.render(
|
||||
$notification,
|
||||
web.html`<span class="notification-text markdown-inline">
|
||||
${md.renderInline(message)}
|
||||
</span>`,
|
||||
web.html`${await components.feather(icon, { class: 'notification-icon' })}`
|
||||
)
|
||||
);
|
||||
return $notification;
|
||||
},
|
||||
_onChange: false,
|
||||
async onChange() {
|
||||
if (this._onChange) return;
|
||||
this._onChange = true;
|
||||
const $notification = await this.add({
|
||||
icon: 'refresh-cw',
|
||||
message: 'Reload to apply changes.',
|
||||
});
|
||||
$notification.addEventListener('click', env.reload);
|
||||
},
|
||||
};
|
||||
|
||||
(async () => {
|
||||
notifications.cache = await storage.get(['notifications'], []);
|
||||
notifications.provider = await fs.getJSON(notificationsURL);
|
||||
|
||||
web.render(document.body, notifications.$container);
|
||||
for (const notification of notifications.provider) {
|
||||
const cached = notifications.cache.includes(notification.id),
|
||||
versionMatches = notification.version === env.version,
|
||||
envMatches = !notification.environments || notification.environments.includes(env.name);
|
||||
if (!cached && versionMatches && envMatches) notifications.add(notification);
|
||||
}
|
||||
})();
|
||||
|
||||
export const $changelogModal = web.render(
|
||||
web.html`<div class="modal" role="dialog" aria-modal="true">
|
||||
<div class="modal-overlay" aria-hidden="true"></div>
|
||||
</div>`
|
||||
);
|
||||
|
||||
(async () => {
|
||||
const $changelogModalButton = web.html`<button type="button" class="modal-button">
|
||||
Accept & Continue
|
||||
</button>`;
|
||||
$changelogModalButton.addEventListener('click', async () => {
|
||||
$changelogModal.classList.remove('modal-visible');
|
||||
await storage.set(['last_read_changelog'], env.version);
|
||||
});
|
||||
|
||||
web.render(
|
||||
$changelogModal,
|
||||
web.render(
|
||||
web.html`<div class="modal-box"></div>`,
|
||||
web.html`<div class="modal-body">
|
||||
<div class="modal-title">
|
||||
${(await fs.getText('media/colour.svg')).replace(
|
||||
/width="\d+" height="\d+"/,
|
||||
`class="modal-title-icon"`
|
||||
)}
|
||||
<div>
|
||||
<h1 class="modal-title-heading">
|
||||
notion-enhancer v${env.version}
|
||||
</h1>
|
||||
<p class="modal-title-description">
|
||||
an enhancer/customiser for the all-in-one productivity workspace notion.so
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<p>
|
||||
Welcome to the notion-enhancer! For help getting started, check out the
|
||||
<a href="https://notion-enhancer.github.io/getting-started/basic-usage/" class="link" target="_blank">
|
||||
basic usage</a> guide. If you've upgraded from a previous version of the notion-enhancer, you can see
|
||||
what's new <a href="https://notion-enhancer.github.io/about/changelog/" class="link">here</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you spot a bug or have a new feature idea, have a read through the
|
||||
<a href="https://notion-enhancer.github.io/about/contributing/" class="link">Contributing</a>
|
||||
guide to learn how & where to talk to us about it. For extra support, come join our
|
||||
<a href="https://discord.com/invite/sFWPXtA" class="link" target="_blank">Discord community</a>.
|
||||
</p>
|
||||
<p>
|
||||
Maintaining and updating the notion-enhancer does take a lot of time and work,
|
||||
so if you'd like to support future development
|
||||
<a href="https://github.com/sponsors/dragonwocky" class="important-link" target="_blank">
|
||||
please consider making a donation</a>.
|
||||
</p>
|
||||
<p>
|
||||
By clicking "Accept & Continue" below you agree to the notion-enhancer's
|
||||
<a href="https://notion-enhancer.github.io/about/privacy-policy/" class="link">Privacy Policy</a> and
|
||||
<a href="https://notion-enhancer.github.io/about/terms-and-conditions/" class="link">Terms & Conditions</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>`,
|
||||
web.render(web.html`<div class="modal-actions"></div>`, $changelogModalButton)
|
||||
)
|
||||
);
|
||||
|
||||
const lastReadChangelog = await storage.get(['last_read_changelog']);
|
||||
web.render(document.body, $changelogModal);
|
||||
if (lastReadChangelog !== env.version) {
|
||||
$changelogModal.classList.add('modal-visible');
|
||||
}
|
||||
})();
|
44
src/core/update.mjs
Normal file
44
src/core/update.mjs
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* notion-enhancer
|
||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||
* (https://notion-enhancer.github.io/) under the MIT license
|
||||
*/
|
||||
|
||||
let _release;
|
||||
const repo = "notion-enhancer/notion-enhancer",
|
||||
endpoint = `https://api.github.com/repos/${repo}/releases/latest`,
|
||||
getRelease = async () => {
|
||||
const { readJson } = globalThis.__enhancerApi;
|
||||
_release ??= (await readJson(endpoint))?.tag_name.replace(/^v/, "");
|
||||
return _release;
|
||||
};
|
||||
|
||||
const parseVersion = (semver) => {
|
||||
while (semver.split("-")[0].split(".").length < 3) semver = `0.${semver}`;
|
||||
let [major, minor, patch, build] = semver.split("."),
|
||||
prerelease = patch.split("-")[1]?.split(".")[0];
|
||||
patch = patch.split("-")[0];
|
||||
return [major, minor, patch, prerelease, build]
|
||||
.map((v) => v ?? "")
|
||||
.map((v) => (/^\d+$/.test(v) ? parseInt(v) : v));
|
||||
},
|
||||
greaterThan = (a, b) => {
|
||||
// is a greater than b
|
||||
a = parseVersion(a);
|
||||
b = parseVersion(b);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] > b[i]) return true;
|
||||
else if (a[i] < b[i]) return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForUpdate = async () => {
|
||||
const { version } = globalThis.__enhancerApi;
|
||||
return greaterThan(await getRelease(), version) ? _release : false;
|
||||
},
|
||||
isDevelopmentBuild = async () => {
|
||||
const { version } = globalThis.__enhancerApi;
|
||||
return !(await checkForUpdate()) && version !== _release;
|
||||
};
|
||||
|
||||
export { checkForUpdate, isDevelopmentBuild };
|
Loading…
Reference in New Issue
Block a user