mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 15:59: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({
|
twind.install({
|
||||||
|
darkMode: "class",
|
||||||
rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
|
rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
|
||||||
variants: [
|
variants: [
|
||||||
// https://github.com/tw-in-js/twind/blob/main/packages/preset-ext/src/variants.ts
|
// 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);
|
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 ??= {};
|
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
|
* (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)`;
|
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) => {
|
export default async (api, db) => {
|
||||||
const {
|
const {
|
||||||
html,
|
html,
|
||||||
@ -109,24 +147,14 @@ export default async (api, db) => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
document.body.append($menuModal);
|
document.body.append($menuModal);
|
||||||
|
|
||||||
const $menuButton = html`<div
|
const $menuButton = html`<${SidebarButton}
|
||||||
onclick=${openMenu}
|
onclick=${openMenu}
|
||||||
tabindex="0"
|
notifications=${(await checkForUpdate()) ? 1 : 0}
|
||||||
role="button"
|
icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
|
||||||
class="notion-enhancer--menu-button
|
? "?mask"
|
||||||
flex select-none cursor-pointer rounded-[3px]
|
: " text-[16px]"}"
|
||||||
text-[14px] my-px mx-[4px] py-[2px] px-[10px]
|
>notion-enhancer
|
||||||
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>`;
|
|
||||||
addMutationListener(notionSidebar, () => {
|
addMutationListener(notionSidebar, () => {
|
||||||
if (document.contains($menuButton)) return;
|
if (document.contains($menuButton)) return;
|
||||||
document.querySelector(notionSidebar)?.append($menuButton);
|
document.querySelector(notionSidebar)?.append($menuButton);
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extendProps } from "../state.mjs";
|
|
||||||
|
|
||||||
function Button({ icon, variant, tagName, ...props }, ...children) {
|
function Button({ icon, variant, tagName, ...props }, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html, extendProps } = globalThis.__enhancerApi;
|
||||||
extendProps(props, {
|
extendProps(props, {
|
||||||
class: `notion-enhancer--menu-button shrink-0
|
class: `notion-enhancer--menu-button shrink-0
|
||||||
flex gap-[8px] items-center px-[12px] rounded-[4px]
|
flex gap-[8px] items-center px-[12px] rounded-[4px]
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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 }) {
|
function Checkbox({ _get, _set, ...props }) {
|
||||||
const { html } = globalThis.__enhancerApi,
|
const { html, extendProps } = globalThis.__enhancerApi,
|
||||||
$input = html`<input
|
$input = html`<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="hidden checked:sibling:(px-px
|
class="hidden checked:sibling:(px-px
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extendProps } from "../state.mjs";
|
|
||||||
|
|
||||||
function Description(props, ...children) {
|
function Description(props, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html, extendProps } = globalThis.__enhancerApi;
|
||||||
extendProps(props, {
|
extendProps(props, {
|
||||||
class: `notion-enhancer--menu-description typography
|
class: `notion-enhancer--menu-description typography
|
||||||
leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
|
leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extendProps } from "../state.mjs";
|
|
||||||
|
|
||||||
function Heading(props, ...children) {
|
function Heading(props, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html, extendProps } = globalThis.__enhancerApi;
|
||||||
extendProps(props, {
|
extendProps(props, {
|
||||||
class: `notion-enhancer--menu-heading text-[16px]
|
class: `notion-enhancer--menu-heading text-[16px]
|
||||||
font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
|
font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extendProps, useState } from "../state.mjs";
|
import { useState } from "../state.mjs";
|
||||||
|
|
||||||
const updateHotkey = (event) => {
|
const updateHotkey = (event) => {
|
||||||
const keys = [];
|
const keys = [];
|
||||||
@ -75,7 +75,7 @@ function Input({
|
|||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
let $filename, $clear;
|
let $filename, $clear;
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html, extendProps } = globalThis.__enhancerApi;
|
||||||
Coloris({ format: "rgb" });
|
Coloris({ format: "rgb" });
|
||||||
|
|
||||||
type ??= "text";
|
type ??= "text";
|
||||||
|
@ -4,15 +4,15 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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) {
|
function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi,
|
const { html, extendProps } = globalThis.__enhancerApi,
|
||||||
$popup = html`<div
|
$popup = html`<div
|
||||||
class="notion-enhancer--menu-popup
|
class="notion-enhancer--menu-popup
|
||||||
group absolute top-0 left-0 w-full h-full
|
group absolute top-0 left-0 w-full h-full
|
||||||
flex flex-col justify-center items-end
|
flex-(& col) justify-center items-end z-20
|
||||||
pointer-events-none z-20"
|
pointer-events-none font-normal text-left"
|
||||||
>
|
>
|
||||||
<div class="relative right-[100%]">
|
<div class="relative right-[100%]">
|
||||||
<div
|
<div
|
||||||
@ -34,10 +34,10 @@ function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
|
|||||||
onopen?.();
|
onopen?.();
|
||||||
};
|
};
|
||||||
$popup.hide = () => {
|
$popup.hide = () => {
|
||||||
|
onbeforeclose?.();
|
||||||
$popup.removeAttribute("open");
|
$popup.removeAttribute("open");
|
||||||
$popup.style.pointerEvents = "auto";
|
$popup.style.pointerEvents = "auto";
|
||||||
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||||
onbeforeclose?.();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$popup.style.pointerEvents = "";
|
$popup.style.pointerEvents = "";
|
||||||
setState({ popupOpen: false });
|
setState({ popupOpen: false });
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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";
|
import { Popup } from "./Popup.mjs";
|
||||||
|
|
||||||
function Option({ value, _get, _set }) {
|
function Option({ value, _get, _set }) {
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extendProps } from "../state.mjs";
|
|
||||||
|
|
||||||
function Tile({ icon, title, tagName, ...props }, ...children) {
|
function Tile({ icon, title, tagName, ...props }, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html, extendProps } = globalThis.__enhancerApi;
|
||||||
extendProps(props, {
|
extendProps(props, {
|
||||||
class: `flex items-center gap-[12px] px-[16px] py-[12px]
|
class: `flex items-center gap-[12px] px-[16px] py-[12px]
|
||||||
bg-[color:var(--theme--bg-secondary)] hover:bg-[color:var(--theme--bg-hover)]
|
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
|
* (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 }) {
|
function Toggle({ _get, _set, ...props }) {
|
||||||
const { html } = globalThis.__enhancerApi,
|
const { html, extendProps } = globalThis.__enhancerApi,
|
||||||
$input = html`<input
|
$input = html`<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="hidden checked:sibling:children:(
|
class="hidden checked:sibling:children:(
|
||||||
|
@ -4,10 +4,14 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Popup } from "../components/Popup.mjs";
|
||||||
import { Button } from "../components/Button.mjs";
|
import { Button } from "../components/Button.mjs";
|
||||||
import { Description } from "../components/Description.mjs";
|
import { Description } from "../components/Description.mjs";
|
||||||
import { useState } from "../state.mjs";
|
import { useState } from "../state.mjs";
|
||||||
|
|
||||||
|
const updateGuide =
|
||||||
|
"https://notion-enhancer.github.io/getting-started/updating/";
|
||||||
|
|
||||||
const rectToStyle = (rect) =>
|
const rectToStyle = (rect) =>
|
||||||
["width", "height", "top", "bottom", "left", "right"]
|
["width", "height", "top", "bottom", "left", "right"]
|
||||||
.filter((prop) => rect[prop])
|
.filter((prop) => rect[prop])
|
||||||
@ -63,9 +67,50 @@ function Circle(rect) {
|
|||||||
></div>`;
|
></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Banner() {
|
function Banner({ updateAvailable, isDevelopmentBuild }) {
|
||||||
const { html, version, initDatabase } = globalThis.__enhancerApi,
|
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]
|
class="relative flex overflow-hidden h-[192px] rounded-t-[4px]
|
||||||
border-(& purple-400) bg-purple-500 from-white/20 to-transparent
|
border-(& purple-400) bg-purple-500 from-white/20 to-transparent
|
||||||
text-white bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
|
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="36px" height="36px" top="136px" left="190px" />
|
||||||
<${Star} width="48px" height="48px" top="32px" left="336px" />
|
<${Star} width="48px" height="48px" top="32px" left="336px" />
|
||||||
<${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
|
<${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
|
||||||
|
|
||||||
<h1
|
<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"
|
font-bold leading-tight tracking-tight my-auto"
|
||||||
>
|
>
|
||||||
<a href="https://notion-enhancer.github.io/">
|
<a href="https://notion-enhancer.github.io/">
|
||||||
@ -87,21 +131,14 @@ function Banner() {
|
|||||||
<span class="text-[28px]">the notion-enhancer</span>
|
<span class="text-[28px]">the notion-enhancer</span>
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col absolute bottom-0 right-0
|
class="absolute bottom-0 right-0 py-[24px]
|
||||||
pr-[32px] md:pr-[48px] lg:pr-[64px] pb-[24px]"
|
px-[32px] md:px-[48px] lg:px-[64px]"
|
||||||
>
|
>
|
||||||
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
|
<div class="relative flex-(& col)">
|
||||||
<a
|
<i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
|
||||||
href="https://github.com/notion-enhancer/notion-enhancer/releases/tag/v${version}"
|
${$version}
|
||||||
>
|
</div>
|
||||||
<span
|
|
||||||
class="text-[12px] py-[2px] px-[6px]
|
|
||||||
font-medium leading-tight tracking-wide"
|
|
||||||
>v${version}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
$sponsorship = html`<div
|
$sponsorship = html`<div
|
||||||
@ -118,16 +155,14 @@ function Banner() {
|
|||||||
variant="brand"
|
variant="brand"
|
||||||
class="grow justify-center"
|
class="grow justify-center"
|
||||||
href="https://www.buymeacoffee.com/dragonwocky"
|
href="https://www.buymeacoffee.com/dragonwocky"
|
||||||
>
|
>Buy me a coffee
|
||||||
Buy me a coffee
|
|
||||||
<//>
|
<//>
|
||||||
<${Button}
|
<${Button}
|
||||||
icon="calendar-heart"
|
icon="calendar-heart"
|
||||||
variant="brand"
|
variant="brand"
|
||||||
class="grow justify-center"
|
class="grow justify-center"
|
||||||
href="https://github.com/sponsors/dragonwocky"
|
href="https://github.com/sponsors/dragonwocky"
|
||||||
>
|
>Sponsor me
|
||||||
Sponsor me
|
|
||||||
<//>
|
<//>
|
||||||
</div>
|
</div>
|
||||||
<!-- Disclaimer: these perks are only a draft, for anyone reading this.
|
<!-- 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.
|
the instructions in the <b>#welcome</b> channel.
|
||||||
<//>
|
<//>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
initDatabase()
|
initDatabase()
|
||||||
.get("agreedToTerms")
|
.get("agreedToTerms")
|
||||||
.then((agreedToTerms) => {
|
.then((agreedToTerms) => {
|
||||||
|
@ -62,7 +62,7 @@ function List({ id, mods, description }) {
|
|||||||
};
|
};
|
||||||
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
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} />
|
<${Search} items=${$mods} itemType=${id} />
|
||||||
<${Description} innerHTML=${description} />
|
<${Description} innerHTML=${description} />
|
||||||
${$mods}
|
${$mods}
|
||||||
|
@ -37,7 +37,7 @@ function Mod({
|
|||||||
class="rounded-[4px] mr-[12px] h-[74px] my-auto"
|
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]">
|
<div class="flex items-center text-[14px] mb-[5px]">
|
||||||
<h3 class="my-0">${name}</h3>
|
<h3 class="my-0">${name}</h3>
|
||||||
${[`v${version}`, ...tags].map((tag) => {
|
${[`v${version}`, ...tags].map((tag) => {
|
||||||
@ -45,8 +45,7 @@ function Mod({
|
|||||||
class="text-([12px] [color:var(--theme--fg-secondary)])
|
class="text-([12px] [color:var(--theme--fg-secondary)])
|
||||||
ml-[8px] py-[2px] px-[6px] leading-tight tracking-wide
|
ml-[8px] py-[2px] px-[6px] leading-tight tracking-wide
|
||||||
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
||||||
>
|
>${tag}
|
||||||
${tag}
|
|
||||||
</span>`;
|
</span>`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Heading } from "../components/Heading.mjs";
|
|
||||||
import { Description } from "../components/Description.mjs";
|
import { Description } from "../components/Description.mjs";
|
||||||
import { Checkbox } from "../components/Checkbox.mjs";
|
import { Checkbox } from "../components/Checkbox.mjs";
|
||||||
import { Button } from "../components/Button.mjs";
|
import { Button } from "../components/Button.mjs";
|
||||||
@ -68,7 +67,7 @@ function Onboarding() {
|
|||||||
>Build your own extension.
|
>Build your own extension.
|
||||||
<//>
|
<//>
|
||||||
<${Tile}
|
<${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"
|
icon="bug"
|
||||||
title="Something not working?"
|
title="Something not working?"
|
||||||
>Report a bug.
|
>Report a bug.
|
||||||
|
@ -38,7 +38,7 @@ function Option({ _get, _set, ...opt }) {
|
|||||||
class="notion-enhancer--menu-option flex items-center justify-between
|
class="notion-enhancer--menu-option flex items-center justify-between
|
||||||
mb-[18px] ${opt.type === "toggle" ? "cursor-pointer" : ""}"
|
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>
|
<h5 class="text-[14px] mb-[2px] mt-0">${opt.label}</h5>
|
||||||
${opt.type === "text"
|
${opt.type === "text"
|
||||||
? html`<${Input}
|
? html`<${Input}
|
||||||
|
@ -133,7 +133,7 @@ function Profile({ id }) {
|
|||||||
<p class="text-[14px] py-[2px] px-[8px]">
|
<p class="text-[14px] py-[2px] px-[8px]">
|
||||||
Are you sure you want to delete the profile ${$confirmName} permanently?
|
Are you sure you want to delete the profile ${$confirmName} permanently?
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-[8px] py-[6px] px-[8px]">
|
<div class="flex-(& col) gap-[8px] py-[6px] px-[8px]">
|
||||||
<${Button}
|
<${Button}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
icon="trash"
|
icon="trash"
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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";
|
import { Description } from "../components/Description.mjs";
|
||||||
|
|
||||||
function SidebarHeading({}, ...children) {
|
function SidebarHeading({}, ...children) {
|
||||||
@ -19,7 +19,7 @@ function SidebarHeading({}, ...children) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SidebarButton({ id, icon, ...props }, ...children) {
|
function SidebarButton({ id, icon, ...props }, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi,
|
const { html, extendProps } = globalThis.__enhancerApi,
|
||||||
$btn = html`<${props["href"] ? "a" : "button"}
|
$btn = html`<${props["href"] ? "a" : "button"}
|
||||||
class="flex items-center select-none text-[14px]
|
class="flex items-center select-none text-[14px]
|
||||||
py-[5px] px-[15px] last:mb-[12px] w-full transition
|
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.
|
policy and terms & conditions on the welcome page.
|
||||||
</span>`,
|
</span>`,
|
||||||
$sidebar = html`<aside
|
$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)]"
|
h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
|
||||||
>
|
>
|
||||||
${items.map((item) => {
|
${items.map((item) => {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { setState, useState } from "./state.mjs";
|
import { setState, useState } from "./state.mjs";
|
||||||
|
import { checkForUpdate, isDevelopmentBuild } from "../update.mjs";
|
||||||
import { Sidebar } from "./islands/Sidebar.mjs";
|
import { Sidebar } from "./islands/Sidebar.mjs";
|
||||||
import { Footer } from "./islands/Footer.mjs";
|
import { Footer } from "./islands/Footer.mjs";
|
||||||
import { Banner } from "./islands/Banner.mjs";
|
import { Banner } from "./islands/Banner.mjs";
|
||||||
@ -120,12 +121,15 @@ const render = async () => {
|
|||||||
categories=${categories}
|
categories=${categories}
|
||||||
/>`,
|
/>`,
|
||||||
$main = html`
|
$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 -->
|
<!-- wrappers necessary for transitions and breakpoints -->
|
||||||
<div class="grow overflow-auto">
|
<div class="grow overflow-auto">
|
||||||
<div class="relative h-full w-full">
|
<div class="relative h-full w-full">
|
||||||
<${View} id="welcome">
|
<${View} id="welcome">
|
||||||
<${Banner} />
|
<${Banner}
|
||||||
|
updateAvailable=${await checkForUpdate()}
|
||||||
|
isDevelopmentBuild=${await isDevelopmentBuild()}
|
||||||
|
/>
|
||||||
<${Onboarding} />
|
<${Onboarding} />
|
||||||
<//>
|
<//>
|
||||||
<${View} id="core">
|
<${View} id="core">
|
||||||
@ -152,7 +156,9 @@ const render = async () => {
|
|||||||
$skeleton.replaceWith($sidebar, $main);
|
$skeleton.replaceWith($sidebar, $main);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("focus", () => setState({ rerender: true }));
|
window.addEventListener("focus", () => {
|
||||||
|
setState({ focus: true, rerender: true });
|
||||||
|
});
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.data?.namespace !== "notion-enhancer") return;
|
if (event.data?.namespace !== "notion-enhancer") return;
|
||||||
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
|
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
|
||||||
|
@ -21,21 +21,4 @@ const setState = (state) => {
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extendProps = (props, extend) => {
|
export { setState, useState };
|
||||||
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 };
|
|
||||||
|
@ -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