/* * notion-enhancer core: menu * (c) 2021 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ 'use strict'; import { env, fs, storage, registry, web, components } from '../../api/_.mjs'; import { notifications } from './notifications.mjs'; import { blocks, options } from './blocks.mjs'; import './styles.mjs'; const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'), profileName = await registry.profileName(), profileDB = await registry.profileDB(); for (const mod of await registry.list((mod) => registry.enabled(mod.id))) { for (const sheet of mod.css?.menu || []) { web.loadStylesheet(`repo/${mod._dir}/${sheet}`); } } web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion); const loadTheme = async () => { document.documentElement.className = (await storage.get(['theme'], 'light')) === 'dark' ? 'dark' : ''; }; document.addEventListener('visibilitychange', loadTheme); loadTheme(); window.addEventListener('beforeunload', (event) => { // trigger input save document.activeElement.blur(); }); const $main = web.html`
`, $sidebar = web.html``, $options = web.html`

Select a mod to view and configure its options.

`, $profile = web.html``; let _$profileConfig; $profile.addEventListener('click', async (event) => { for (const $selected of document.querySelectorAll('.mod-selected')) { $selected.className = 'mod'; } if (!_$profileConfig) { const profileNames = [ ...new Set([ ...Object.keys(await storage.get(['profiles'], { default: {} })), profileName, ]), ], $options = profileNames.map( (profile) => web.raw`` ), $select = web.html``, $edit = web.html``, $export = web.html``, $import = web.html``, $save = web.html``, $delete = web.html``, $error = web.html`

`; $export.addEventListener('click', async (event) => { const now = new Date(), $a = web.html``; 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', async (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) { web.render(web.empty($error), 'Profile names cannot be empty.'); return false; } if (!$edit.value.match(/^[A-Za-z0-9_-]+$/)) { web.render( web.empty($error), 'Profile names can only contain letters, numbers, dashes 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`
`, web.html`

Profiles are used to switch entire configurations. Here they can be selected, renamed or deleted. Profile names can only contain letters, numbers, dashes and underscores.
Be careful - deleting a profile deletes all configuration related to it.

`, web.render( web.html``, $select, web.html`${await components.feather('chevron-down', { class: 'input-icon' })}` ), web.render( web.html``, $edit, web.html`${await components.feather('type', { class: 'input-icon' })}` ), web.render(web.html`

`, $export, $import, $save, $delete), $error ); } web.render(web.empty($options), _$profileConfig); }); const _$modListCache = {}, 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`

No options.

`); } return $fragment; }, mod: async (mod) => { const $mod = web.html`
`, $toggle = blocks.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( (mod) => mod.environments.includes(env.name) && 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', async (event) => { if ($mod.className === 'mod-selected') return; for (const $selected of document.querySelectorAll('.mod-selected')) { $selected.className = 'mod'; } $mod.className = 'mod-selected'; const fragment = [ web.render(blocks.title(mod.name), blocks.version(mod.version)), blocks.tags(mod.tags), await generators.options(mod), ]; web.render(web.empty($options), ...fragment); }); return web.render( web.html`
`, web.render( $mod, mod.preview ? blocks.preview( mod.preview.startsWith('http') ? mod.preview : fs.localPath(`repo/${mod._dir}/${mod.preview}`) ) : '', web.render( web.html`
`, web.render(blocks.title(mod.name), blocks.version(mod.version)), blocks.tags(mod.tags), blocks.description(mod.description), blocks.authors(mod.authors), mod.environments.includes(env.name) && !registry.core.includes(mod.id) ? $toggle : '' ) ) ); }, modList: async (category, message = '') => { if (!_$modListCache[category]) { const $search = web.html``, $list = web.html`
`, 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); } _$modListCache[category] = web.render( web.html`
`, web.render( web.html``, $search, web.html`${await components.feather('search', { class: 'input-icon' })}` ), message ? web.html`

${web.escape(message)}

` : '', $list ); } return _$modListCache[category]; }, }; const $notionNavItem = web.html`

${(await fs.getText('media/colour.svg')).replace( /width="\d+" height="\d+"/, `class="nav-notion-icon"` )} notion-enhancer

`; $notionNavItem.addEventListener('click', env.focusNotion); const $coreNavItem = web.html`core`, $extensionsNavItem = web.html`extensions`, $themesNavItem = web.html`themes`; web.render( document.body, web.render( web.html`
`, web.render( web.html`
`, web.render( web.html``, $notionNavItem, $coreNavItem, $extensionsNavItem, $themesNavItem, web.html`docs`, web.html`community` ), $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'; } import * as router from './router.mjs'; router.addView('core', async () => { web.empty($main); selectNavItem($coreNavItem); return web.render($main, await generators.modList('core')); }); router.addView('extensions', async () => { web.empty($main); selectNavItem($extensionsNavItem); return web.render($main, await generators.modList('extension')); }); router.addView('themes', async () => { web.empty($main); selectNavItem($themesNavItem); return web.render( $main, await generators.modList( 'theme', `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.loadView('extensions', $main);