mirror of
				https://github.com/notion-enhancer/notion-enhancer.git
				synced 2025-10-30 21:58:08 +11: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" | ||||
|     notifications=${(await checkForUpdate()) ? 1 : 0} | ||||
|     icon="notion-enhancer${menuButtonIconStyle === "Monochrome" | ||||
|       ? "?mask" | ||||
|       : " text-[16px]"}" | ||||
|       ></i> | ||||
|     </div> | ||||
|     <div>notion-enhancer</div> | ||||
|   </div>`; | ||||
|     >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]" | ||||
|       > | ||||
|         <div class="relative flex-(& col)"> | ||||
|           <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> | ||||
|           ${$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