/* * notion-enhancer core: menu * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ 'use strict'; import { env, fs, storage, registry, web } from '../../api/_.mjs'; const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'); import './styles.mjs'; import { notifications } from './notifications.mjs'; import { components, options } from './components.mjs'; web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion); 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}`); } } const loadTheme = async () => { document.documentElement.className = (await db.get(['theme'], 'light')) === 'dark' ? 'dark' : ''; }; document.addEventListener('visibilitychange', loadTheme); loadTheme(); window.addEventListener('beforeunload', (event) => { // trigger input save document.activeElement.blur(); }); const $main = web.html`<main class="main"></main>`, $sidebar = web.html`<article class="sidebar"></article>`, $profile = web.html`<button class="profile-button"> Profile: ${web.escape(registry.profileName)} </button>`, $options = web.html`<div class="options-container"> <p class="options-placeholder">Select a mod to view and configure its options.</p> </div>`; 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: {} })), registry.profileName, ]), ], $options = profileNames.map( (profile) => web.raw`<option class="select-option" value="${web.escape(profile)}" ${profile === registry.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(registry.profileName)}" pattern="/^[A-Za-z0-9_-]+$/" >`, $save = web.html`<button class="profile-save"> ${web.icon('save', { class: 'button-icon' })} Save </button>`, $delete = web.html`<button class="profile-delete"> ${web.icon('trash-2', { class: 'button-icon' })} Delete </button>`, $error = web.html`<p class="profile-error"></p>`; $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.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); } location.reload(); }); $delete.addEventListener('click', async (event) => { await storage.set(['profiles', $select.value], undefined); await storage.set( ['currentprofile'], profileNames.find((profile) => profile !== $select.value) || 'default' ); location.reload(); }); _$profileConfig = web.render( web.html`<div></div>`, web.html`<p class="options-placeholder"> 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. <br> Be careful - deleting a profile deletes all configuration related to it. </p>`, web.render( web.html`<label class="input-label"></label>`, $select, web.html`${web.icon('chevron-down', { class: 'input-icon' })}` ), web.render( web.html`<label class="input-label"></label>`, $edit, web.html`${web.icon('type', { class: 'input-icon' })}` ), web.render(web.html`<p></p>`, $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) { 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"></div>`, $toggle = components.toggle('', await registry.enabled(mod.id)); $toggle.addEventListener('change', (event) => { registry.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(components.title(mod.name), components.version(mod.version)), components.tags(mod.tags), await generators.options(mod), ]; web.render(web.empty($options), ...fragment); }); return web.render( web.html`<article class="mod-container"></article>`, web.render( $mod, mod.preview ? components.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(components.title(mod.name), components.version(mod.version)), components.tags(mod.tags), components.description(mod.description), components.authors(mod.authors), mod.environments.includes(env.name) && !registry.core.includes(mod.id) ? $toggle : '' ) ) ); }, modList: async (category) => { if (!_$modListCache[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); } _$modListCache[category] = web.render( web.html`<div></div>`, web.render( web.html`<label class="search-container"></label>`, $search, web.html`${web.icon('search', { class: 'input-icon' })}` ), $list ); } return _$modListCache[category]; }, }; const $notionNavItem = web.html`<h1 class="nav-notion"> ${(await fs.getText('icon/colour.svg')).replace( /width="\d+" height="\d+"/, `class="nav-notion-icon"` )} <a href="https://notion-enhancer.github.io/" target="_blank">notion-enhancer</a> </h1>`; $notionNavItem.children[0].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>`, $communityNavItem = web.html`<a href="https://discord.gg/sFWPXtA" class="nav-item">community</a>`; 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, $communityNavItem ), $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')); }); router.loadView('extensions', $main);