mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-06 21:49:03 +00:00
added profile switcher + split up menu js
This commit is contained in:
parent
ea53f672ee
commit
775d8412d0
@ -9,10 +9,11 @@ a complete rework of the enhancer including a port to the browser as a chrome ex
|
|||||||
- new: notifications sourced from an online endpoint for sending global user alerts.
|
- new: notifications sourced from an online endpoint for sending global user alerts.
|
||||||
- new: simplify user installs by depending on the chrome web store and [notion-repackaged](https://github.com/notion-enhancer/notion-repackaged).
|
- new: simplify user installs by depending on the chrome web store and [notion-repackaged](https://github.com/notion-enhancer/notion-repackaged).
|
||||||
- new: separate menu profiles for mod configurations.
|
- new: separate menu profiles for mod configurations.
|
||||||
|
- new: a hotkey option type that allows typing in/pressing a hotkey to enter it, instead of typing.
|
||||||
- improved: split the core mod into separate mods for specific features.
|
- improved: split the core mod into separate mods for specific features.
|
||||||
- improved: theming variables that are more specific, less laggy, and less complicated.
|
- improved: theming variables that are more specific, less laggy, and less complicated.
|
||||||
- improved: merged bracketed-links into tweaks.
|
- improved: merged bracketed-links into tweaks.
|
||||||
- improved: a redesigned menu with separate categories for mods and a sidebar for configuring options.
|
- improved: a redesigned menu with nicer ui, separate categories for mods and a sidebar for configuration.
|
||||||
- removed: integrated scrollbar tweak (notion now includes by default).
|
- removed: integrated scrollbar tweak (notion now includes by default).
|
||||||
- removed: js insert. css insert moved to tweaks mod.
|
- removed: js insert. css insert moved to tweaks mod.
|
||||||
- removed: majority of layout and font size variables - better to leave former to notion and use `ctrl +` for latter.
|
- removed: majority of layout and font size variables - better to leave former to notion and use `ctrl +` for latter.
|
||||||
|
@ -28,11 +28,11 @@ export const core = [
|
|||||||
/** all available configuration types */
|
/** all available configuration types */
|
||||||
export const optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey'];
|
export const optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey'];
|
||||||
|
|
||||||
|
/** the name of the active configuration profile */
|
||||||
|
export const profileName = await storage.get(['currentprofile'], 'default');
|
||||||
|
|
||||||
/** the root database for the current profile */
|
/** the root database for the current profile */
|
||||||
export const profile = storage.db([
|
export const profileDB = storage.db(['profiles', profileName]);
|
||||||
'profiles',
|
|
||||||
await storage.get(['currentprofile'], 'default'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* internally used to validate mod.json files and provide helpful errors
|
* internally used to validate mod.json files and provide helpful errors
|
||||||
@ -282,7 +282,7 @@ export const enabled = async (id) => {
|
|||||||
const mod = await get(id);
|
const mod = await get(id);
|
||||||
if (!mod.environments.includes(env.name)) return false;
|
if (!mod.environments.includes(env.name)) return false;
|
||||||
if (core.includes(id)) return true;
|
if (core.includes(id)) return true;
|
||||||
return await profile.get(['_mods', id], false);
|
return await profileDB.get(['_mods', id], false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -322,8 +322,8 @@ export const db = async (id) => {
|
|||||||
// profiles -> profile -> mod -> option
|
// profiles -> profile -> mod -> option
|
||||||
fallback = (await optionDefault(id, path[1])) ?? fallback;
|
fallback = (await optionDefault(id, path[1])) ?? fallback;
|
||||||
}
|
}
|
||||||
return profile.get(path, fallback);
|
return profileDB.get(path, fallback);
|
||||||
},
|
},
|
||||||
profile.set
|
profileDB.set
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -22,17 +22,16 @@ const _queue = [],
|
|||||||
*/
|
*/
|
||||||
export const get = (path, fallback = undefined) => {
|
export const get = (path, fallback = undefined) => {
|
||||||
if (!path.length) return fallback;
|
if (!path.length) return fallback;
|
||||||
const namespace = path.shift();
|
|
||||||
return new Promise((res, rej) =>
|
return new Promise((res, rej) =>
|
||||||
chrome.storage.local.get(async (values) => {
|
chrome.storage.local.get(async (values) => {
|
||||||
let value = values[namespace];
|
let value = values;
|
||||||
do {
|
while (path.length) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
value = fallback;
|
value = fallback;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value = value[path.shift()];
|
value = value[path.shift()];
|
||||||
} while (path.length);
|
}
|
||||||
res(value ?? fallback);
|
res(value ?? fallback);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -53,10 +52,9 @@ export const set = (path, value) => {
|
|||||||
_queue.shift();
|
_queue.shift();
|
||||||
}
|
}
|
||||||
const pathClone = [...path],
|
const pathClone = [...path],
|
||||||
namespace = path.shift();
|
namespace = path[0];
|
||||||
chrome.storage.local.get(async (values) => {
|
chrome.storage.local.get(async (values) => {
|
||||||
const update = values[namespace] ?? {};
|
let pointer = values,
|
||||||
let pointer = update,
|
|
||||||
old;
|
old;
|
||||||
while (path.length) {
|
while (path.length) {
|
||||||
const key = path.shift();
|
const key = path.shift();
|
||||||
@ -68,7 +66,7 @@ export const set = (path, value) => {
|
|||||||
pointer[key] = pointer[key] ?? {};
|
pointer[key] = pointer[key] ?? {};
|
||||||
pointer = pointer[key];
|
pointer = pointer[key];
|
||||||
}
|
}
|
||||||
chrome.storage.local.set({ [namespace]: update }, () => {
|
chrome.storage.local.set({ [namespace]: values[namespace] }, () => {
|
||||||
_onChangeListeners.forEach((listener) =>
|
_onChangeListeners.forEach((listener) =>
|
||||||
listener({ type: 'set', path: pathClone, new: value, old })
|
listener({ type: 'set', path: pathClone, new: value, old })
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* 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 { fmt, registry, web } from '../../api/_.mjs';
|
||||||
|
|
||||||
|
import { notifications } from './notifications.mjs';
|
||||||
|
|
||||||
|
export const components = {
|
||||||
|
preview: (url) => web.html`<img
|
||||||
|
class="mod-preview"
|
||||||
|
src="${web.escape(url)}"
|
||||||
|
alt=""
|
||||||
|
>`,
|
||||||
|
title: (title) => web.html`<h4 class="mod-title"><span>${web.escape(title)}</span></h4>`,
|
||||||
|
version: (version) => web.html`<span class="mod-version">v${web.escape(version)}</span>`,
|
||||||
|
tags: (tags) => {
|
||||||
|
if (!tags.length) return '';
|
||||||
|
return web.render(
|
||||||
|
web.html`<p class="mod-tags"></p>`,
|
||||||
|
tags.map((tag) => `#${web.escape(tag)}`).join(' ')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
description: (description) => web.html`<p class="mod-description markdown-inline">
|
||||||
|
${fmt.md.renderInline(description)}
|
||||||
|
</p>`,
|
||||||
|
authors: (authors) => {
|
||||||
|
const author = (author) => web.html`<a
|
||||||
|
class="mod-author"
|
||||||
|
href="${web.escape(author.homepage)}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img class="mod-author-avatar"
|
||||||
|
src="${web.escape(author.avatar)}" alt="${web.escape(author.name)}'s avatar"
|
||||||
|
> <span>${web.escape(author.name)}</span>
|
||||||
|
</a>`;
|
||||||
|
return web.render(web.html`<p class="mod-authors-container"></p>`, ...authors.map(author));
|
||||||
|
},
|
||||||
|
toggle: (label, checked) => {
|
||||||
|
const $label = web.html`<label tabindex="0" class="toggle-label">
|
||||||
|
<span>${web.escape(label)}</span>
|
||||||
|
</label>`,
|
||||||
|
$input = web.html`<input tabindex="-1" type="checkbox" class="toggle-check"
|
||||||
|
${checked ? 'checked' : ''}>`,
|
||||||
|
$feature = web.html`<span class="toggle-box toggle-feature"></span>`;
|
||||||
|
$label.addEventListener('keyup', (event) => {
|
||||||
|
if (['Enter', ' '].includes(event.key)) $input.checked = !$input.checked;
|
||||||
|
});
|
||||||
|
return web.render($label, $input, $feature);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
toggle: async (mod, opt) => {
|
||||||
|
const checked = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||||
|
$toggle = components.toggle(opt.label, checked),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = $toggle.children[0],
|
||||||
|
$input = $toggle.children[1];
|
||||||
|
if (opt.tooltip) {
|
||||||
|
$label.prepend($tooltip);
|
||||||
|
web.tooltip($tooltip, opt.tooltip);
|
||||||
|
}
|
||||||
|
$input.addEventListener('change', async (event) => {
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $input.checked);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
return $toggle;
|
||||||
|
},
|
||||||
|
select: async (mod, opt) => {
|
||||||
|
const value = await registry.profileDB.get([mod.id, opt.key], opt.values[0]),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$options = opt.values.map(
|
||||||
|
(option) => web.raw`<option
|
||||||
|
class="select-option"
|
||||||
|
value="${web.escape(option)}"
|
||||||
|
${option === value ? 'selected' : ''}
|
||||||
|
>${web.escape(option)}</option>`
|
||||||
|
),
|
||||||
|
$select = web.html`<select class="input">
|
||||||
|
${$options.join('')}
|
||||||
|
</select>`,
|
||||||
|
$icon = web.html`${web.icon('chevron-down', { class: 'input-icon' })}`;
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$select.addEventListener('change', async (event) => {
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $select.value);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
return web.render($label, $select, $icon);
|
||||||
|
},
|
||||||
|
text: async (mod, opt) => {
|
||||||
|
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
||||||
|
$icon = web.html`${web.icon('type', { class: 'input-icon' })}`;
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$input.addEventListener('change', async (event) => {
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
return web.render($label, $input, $icon);
|
||||||
|
},
|
||||||
|
number: async (mod, opt) => {
|
||||||
|
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$input = web.html`<input type="number" class="input" value="${value}">`,
|
||||||
|
$icon = web.html`${web.icon('hash', { class: 'input-icon' })}`;
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$input.addEventListener('change', async (event) => {
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
return web.render($label, $input, $icon);
|
||||||
|
},
|
||||||
|
color: async (mod, opt) => {
|
||||||
|
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$input = web.html`<input type="text" class="input">`,
|
||||||
|
$icon = web.html`${web.icon('droplet', { class: 'input-icon' })}`,
|
||||||
|
paint = () => {
|
||||||
|
$input.style.background = $picker.toBackground();
|
||||||
|
$input.style.color = $picker.isLight() ? '#000' : '#fff';
|
||||||
|
$input.style.padding = '';
|
||||||
|
},
|
||||||
|
$picker = new web.jscolor($input, {
|
||||||
|
value,
|
||||||
|
format: 'rgba',
|
||||||
|
previewSize: 0,
|
||||||
|
borderRadius: 3,
|
||||||
|
borderColor: 'var(--theme--ui_divider)',
|
||||||
|
controlBorderColor: 'var(--theme--ui_divider)',
|
||||||
|
backgroundColor: 'var(--theme--bg)',
|
||||||
|
onInput: paint,
|
||||||
|
onChange: paint,
|
||||||
|
});
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$input.addEventListener('change', async (event) => {
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
paint();
|
||||||
|
return web.render($label, $input, $icon);
|
||||||
|
},
|
||||||
|
file: async (mod, opt) => {
|
||||||
|
const { filename } = (await registry.profileDB.get([mod.id, opt.key], {})) || {},
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$pseudo = web.html`<span class="input"><span class="input-placeholder">Upload file...</span></span>`,
|
||||||
|
$input = web.html`<input type="file" class="hidden" accept=${web.escape(
|
||||||
|
opt.extensions.join(',')
|
||||||
|
)}>`,
|
||||||
|
$icon = web.html`${web.icon('file', { class: 'input-icon' })}`,
|
||||||
|
$filename = web.html`<span>${web.escape(filename || 'none')}</span>`,
|
||||||
|
$latest = web.render(web.html`<button class="file-latest">Latest: </button>`, $filename);
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$input.addEventListener('change', (event) => {
|
||||||
|
const file = event.target.files[0],
|
||||||
|
reader = new FileReader();
|
||||||
|
reader.onload = async (progress) => {
|
||||||
|
$filename.innerText = file.name;
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], {
|
||||||
|
filename: file.name,
|
||||||
|
content: progress.currentTarget.result,
|
||||||
|
});
|
||||||
|
notifications.onChange();
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
$latest.addEventListener('click', (event) => {
|
||||||
|
$filename.innerText = 'none';
|
||||||
|
registry.profileDB.set([mod.id, opt.key], {});
|
||||||
|
});
|
||||||
|
return web.render(
|
||||||
|
web.html`<div></div>`,
|
||||||
|
web.render($label, $input, $pseudo, $icon),
|
||||||
|
$latest
|
||||||
|
);
|
||||||
|
},
|
||||||
|
hotkey: async (mod, opt) => {
|
||||||
|
const value = await registry.profileDB.get([mod.id, opt.key], opt.value),
|
||||||
|
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
||||||
|
$label = web.render(
|
||||||
|
web.html`<label class="input-label"></label>`,
|
||||||
|
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
||||||
|
),
|
||||||
|
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
||||||
|
$icon = web.html`${web.icon('command', { class: 'input-icon' })}`;
|
||||||
|
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
||||||
|
$input.addEventListener('keydown', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const pressed = [],
|
||||||
|
modifiers = {
|
||||||
|
metaKey: 'Meta',
|
||||||
|
ctrlKey: 'Control',
|
||||||
|
altKey: 'Alt',
|
||||||
|
shiftKey: 'Shift',
|
||||||
|
};
|
||||||
|
for (const modifier in modifiers) {
|
||||||
|
if (event[modifier]) pressed.push(modifiers[modifier]);
|
||||||
|
}
|
||||||
|
const empty = ['Backspace', 'Delete'].includes(event.key) && !pressed.length;
|
||||||
|
if (!empty && !pressed.includes(event.key)) {
|
||||||
|
let key = event.key;
|
||||||
|
if (key === ' ') key = 'Space';
|
||||||
|
if (key.length === 1) key = event.key.toUpperCase();
|
||||||
|
pressed.push(key);
|
||||||
|
}
|
||||||
|
$input.value = pressed.join('+');
|
||||||
|
await registry.profileDB.set([mod.id, opt.key], $input.value);
|
||||||
|
notifications.onChange();
|
||||||
|
});
|
||||||
|
return web.render($label, $input, $icon);
|
||||||
|
},
|
||||||
|
};
|
@ -1,433 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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';
|
|
||||||
|
|
||||||
const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e';
|
|
||||||
import { env, storage, web, fmt, fs, registry, regexers } from '../../api/_.mjs';
|
|
||||||
|
|
||||||
import * as router from './router.js';
|
|
||||||
|
|
||||||
const components = {};
|
|
||||||
components.card = async (mod) => {
|
|
||||||
const $card = web.createElement(web.html`
|
|
||||||
<article class="library--card" data-mod='${mod.id}'>
|
|
||||||
${
|
|
||||||
mod.preview
|
|
||||||
? web.html`<img
|
|
||||||
alt=""
|
|
||||||
class="library--preview"
|
|
||||||
src="${web.escapeHtml(mod.preview)}"
|
|
||||||
/>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
for="enable--${web.escapeHtml(mod.id)}"
|
|
||||||
class="library--title library--toggle_label"
|
|
||||||
>
|
|
||||||
<input type="checkbox" id="enable--${web.escapeHtml(mod.id)}"
|
|
||||||
${(await registry.isEnabled(mod.id)) ? 'checked' : ''}/>
|
|
||||||
<h2>
|
|
||||||
<span>
|
|
||||||
${web.escapeHtml(mod.name)}
|
|
||||||
<span class="library--version">v${web.escapeHtml(mod.version)}</span>
|
|
||||||
</span>
|
|
||||||
${
|
|
||||||
registry.CORE.includes(mod.id) ? '' : web.html`<span class="library--toggle"></span>`
|
|
||||||
}
|
|
||||||
</h2>
|
|
||||||
</label>
|
|
||||||
<ul class="library--tags">
|
|
||||||
${mod.tags.map((tag) => web.html`<li>#${web.escapeHtml(tag)}</li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
<p class="library--description markdown">${fmt.md.renderInline(mod.description)}</p>
|
|
||||||
<ul class="library--authors">
|
|
||||||
${mod.authors
|
|
||||||
.map(
|
|
||||||
(author) =>
|
|
||||||
web.html`
|
|
||||||
<li>
|
|
||||||
<a href="${web.escapeHtml(author.url)}">
|
|
||||||
<img alt="" src="${web.escapeHtml(author.icon)}" />
|
|
||||||
<span>${web.escapeHtml(author.name)}</span>
|
|
||||||
</a>
|
|
||||||
</li>`
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</ul>
|
|
||||||
<p class="library--expand">
|
|
||||||
<a href="?view=mod&id=${web.escapeHtml(mod.id)}">
|
|
||||||
<span><i data-icon="fa/solid/long-arrow-alt-right"></i></span>
|
|
||||||
<span>settings & documentation</span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</article>`);
|
|
||||||
$card.querySelector('.library--title input').addEventListener('change', async (event) => {
|
|
||||||
storage.set('_mods', mod.id, event.target.checked);
|
|
||||||
});
|
|
||||||
return $card;
|
|
||||||
};
|
|
||||||
components.options = {
|
|
||||||
async toggle(id, { key, label, tooltip }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label
|
|
||||||
for="toggle--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
class="library--toggle_label"
|
|
||||||
>
|
|
||||||
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
${state ? 'checked' : ''}/>
|
|
||||||
<p>
|
|
||||||
<span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span>
|
|
||||||
<span class="library--toggle"></span>
|
|
||||||
</p>
|
|
||||||
</label>`);
|
|
||||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked));
|
|
||||||
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
async select(id, { key, label, tooltip, values }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label
|
|
||||||
for="select--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
class="library--select_label"
|
|
||||||
>
|
|
||||||
<p><span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
|
|
||||||
<p class="library--select">
|
|
||||||
<span><i data-icon="fa/solid/caret-down"></i></span>
|
|
||||||
<select id="select--${web.escapeHtml(`${id}.${key}`)}">
|
|
||||||
${values.map(
|
|
||||||
(value) =>
|
|
||||||
web.html`<option value="${web.escapeHtml(value)}"
|
|
||||||
${value === state ? 'selected' : ''}>
|
|
||||||
${web.escapeHtml(value)}</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
</label>`);
|
|
||||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
|
||||||
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
async text(id, { key, label, tooltip }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label
|
|
||||||
for="text--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
class="library--text_label"
|
|
||||||
>
|
|
||||||
<p><span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
|
|
||||||
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
rows="1">${web.escapeHtml(state)}</textarea>
|
|
||||||
</label>`);
|
|
||||||
opt.querySelector('textarea').addEventListener('input', (event) => {
|
|
||||||
event.target.style.removeProperty('--txt--scroll-height');
|
|
||||||
event.target.style.setProperty(
|
|
||||||
'--txt--scroll-height',
|
|
||||||
event.target.scrollHeight + 1 + 'px'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
|
||||||
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
async number(id, { key, label, tooltip }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label
|
|
||||||
for="number--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
class="library--number_label"
|
|
||||||
>
|
|
||||||
<p><span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
|
|
||||||
<input id="number--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
type="number" value="${web.escapeHtml(state.toString())}"/>
|
|
||||||
</label>`);
|
|
||||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
|
||||||
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
async color(id, { key, label, tooltip }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label for="color--${web.escapeHtml(`${id}.${key}`)}" class="library--color_label">
|
|
||||||
<p class="library--color_title">
|
|
||||||
<span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
<i data-icon="fa/solid/question-circle"></i>
|
|
||||||
</span>
|
|
||||||
<p class="library--color">
|
|
||||||
<span><i data-icon="fa/solid/eye-dropper"></i></span>
|
|
||||||
<input type="text" id="color--${web.escapeHtml(`${id}.${key}`)}"/>
|
|
||||||
</p>
|
|
||||||
</label>`);
|
|
||||||
const $fill = opt.querySelector('input'),
|
|
||||||
paintInput = () => {
|
|
||||||
$fill.style.background = picker.toBackground();
|
|
||||||
$fill.style.color = picker.isLight() ? '#000' : '#fff';
|
|
||||||
},
|
|
||||||
picker = new fmt.JSColor($fill, {
|
|
||||||
value: state,
|
|
||||||
previewSize: 0,
|
|
||||||
borderRadius: 3,
|
|
||||||
borderColor: 'var(--theme--divider)',
|
|
||||||
controlBorderColor: 'var(--theme--divider)',
|
|
||||||
backgroundColor: 'var(--theme--page)',
|
|
||||||
onInput() {
|
|
||||||
paintInput();
|
|
||||||
},
|
|
||||||
onChange() {
|
|
||||||
paintInput();
|
|
||||||
storage.set(id, key, this.toRGBAString());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
paintInput();
|
|
||||||
opt.addEventListener('click', (event) => {
|
|
||||||
picker.show();
|
|
||||||
});
|
|
||||||
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
async file(id, { key, label, tooltip, extensions }) {
|
|
||||||
const state = await storage.get(id, key),
|
|
||||||
opt = web.createElement(web.html`
|
|
||||||
<label
|
|
||||||
for="file--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
class="library--file_label"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="file--${web.escapeHtml(`${id}.${key}`)}"
|
|
||||||
${web.escapeHtml(
|
|
||||||
extensions && extensions.length
|
|
||||||
? ` accept=${web.escapeHtml(extensions.join(','))}`
|
|
||||||
: ''
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<p class="library--file_title"><span data-tooltip>${web.escapeHtml(label)}
|
|
||||||
<i data-icon="fa/solid/question-circle"></i></span>
|
|
||||||
<span class="library--file_remove"><i data-icon="fa/solid/minus"></i></span></p>
|
|
||||||
<p class="library--file">
|
|
||||||
<span><i data-icon="fa/solid/file"></i></span>
|
|
||||||
<span class="library--file_path">${web.escapeHtml(state || 'choose file...')}</span>
|
|
||||||
</p>
|
|
||||||
</label>`);
|
|
||||||
opt.addEventListener('change', (event) => {
|
|
||||||
const file = event.target.files[0],
|
|
||||||
reader = new FileReader();
|
|
||||||
opt.querySelector('.library--file_path').innerText = file.name;
|
|
||||||
reader.onload = (progress) => {
|
|
||||||
storage.set(id, key, file.name);
|
|
||||||
storage.set(id, `_file.${key}`, progress.currentTarget.result);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
opt.querySelector('.library--file_remove').addEventListener(
|
|
||||||
'click',
|
|
||||||
(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
opt.querySelector('input').value = '';
|
|
||||||
opt.querySelector('.library--file_path').innerText = 'choose file...';
|
|
||||||
storage.set(id, key, undefined);
|
|
||||||
storage.set(id, `_file.${key}`, undefined);
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
opt.addEventListener('click', (event) => {
|
|
||||||
document.documentElement.scrollTop = 0;
|
|
||||||
});
|
|
||||||
web.addTooltip(
|
|
||||||
opt.querySelector('[data-tooltip]'),
|
|
||||||
`${tooltip ? `${tooltip}\n\n` : ''}**warning:** ${
|
|
||||||
'browser extensions do not have true filesystem access, ' +
|
|
||||||
'so file content is only saved on selection. re-select files to apply edits.'
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return opt;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionButtons = {
|
|
||||||
_reloadTriggered: false,
|
|
||||||
async reload($fragment = document) {
|
|
||||||
let $reload = $fragment.querySelector('[data-reload]');
|
|
||||||
if (!$reload && this._reloadTriggered) {
|
|
||||||
$reload = web.createElement(web.html`
|
|
||||||
<button class="action--alert" data-reload>
|
|
||||||
<span><i data-icon="fa/solid/redo"></i></span>
|
|
||||||
<span>reload tabs to apply changes</span>
|
|
||||||
</button>`);
|
|
||||||
$reload.addEventListener('click', env.reload);
|
|
||||||
$fragment.querySelector('.action--buttons').append($reload);
|
|
||||||
await new Promise((res, rej) => requestAnimationFrame(res));
|
|
||||||
$reload.dataset.triggered = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async clearFilters($fragment = document) {
|
|
||||||
let $clearFilters = $fragment.querySelector('[data-clear-filters]');
|
|
||||||
const search = router.getSearch();
|
|
||||||
if (search.get('tag') || search.has('enabled') || search.has('disabled')) {
|
|
||||||
if (!$clearFilters) {
|
|
||||||
$clearFilters = web.createElement(web.html`
|
|
||||||
<a class="action--alert" href="?view=library" data-clear-filters>
|
|
||||||
<span><i data-icon="fa/solid/times"></i></span>
|
|
||||||
<span>clear filters</span>
|
|
||||||
</a>`);
|
|
||||||
$fragment.querySelector('.action--buttons').append($clearFilters);
|
|
||||||
await new Promise((res, rej) => requestAnimationFrame(res));
|
|
||||||
$clearFilters.dataset.triggered = true;
|
|
||||||
}
|
|
||||||
} else if ($clearFilters) $clearFilters.remove();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
storage.addChangeListener(async (event) => {
|
|
||||||
actionButtons._reloadTriggered = true;
|
|
||||||
actionButtons.reload();
|
|
||||||
router.load();
|
|
||||||
|
|
||||||
if (event.namespace === '_mods' && event.new === true) {
|
|
||||||
const enabledTheme = (await registry.get()).find((mod) => mod.id === event.key);
|
|
||||||
if (
|
|
||||||
enabledTheme.tags.includes('theme') &&
|
|
||||||
(await storage.get(_id, 'themes.autoresolve', true))
|
|
||||||
) {
|
|
||||||
for (const theme of await registry.get(
|
|
||||||
(mod) =>
|
|
||||||
mod.tags.includes('theme') &&
|
|
||||||
mod.id !== enabledTheme.id &&
|
|
||||||
((mod.tags.includes('dark') && enabledTheme.tags.includes('dark')) ||
|
|
||||||
(mod.tags.includes('light') && enabledTheme.tags.includes('light')))
|
|
||||||
)) {
|
|
||||||
if (document.body.dataset.view === 'library') {
|
|
||||||
const $toggle = document.getElementById(`enable--${theme.id}`);
|
|
||||||
if ($toggle.checked) $toggle.click();
|
|
||||||
} else storage.set('_mods', theme.id, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.addView(
|
|
||||||
'library',
|
|
||||||
async () => {
|
|
||||||
const $fragment = web.createFragment(web.html`
|
|
||||||
<p class="action--buttons">
|
|
||||||
<a href="?view=library&tag=theme">
|
|
||||||
<span><i data-icon="fa/solid/palette"></i></span>
|
|
||||||
<span>themes</span>
|
|
||||||
</a>
|
|
||||||
<a href="?view=library&tag=extension">
|
|
||||||
<span><i data-icon="fa/solid/plus"></i></span>
|
|
||||||
<span>extensions</span>
|
|
||||||
</a>
|
|
||||||
<a href="?view=library&enabled">
|
|
||||||
<span><i data-icon="fa/solid/toggle-on"></i></span>
|
|
||||||
<span>enabled</span>
|
|
||||||
</a>
|
|
||||||
<a href="?view=library&disabled">
|
|
||||||
<span><i data-icon="fa/solid/toggle-off"></i></span>
|
|
||||||
<span>disabled</span>
|
|
||||||
</a>
|
|
||||||
</p>`);
|
|
||||||
for (const mod of await registry.get(
|
|
||||||
(mod) => !mod.environments || mod.environments.includes(env.name)
|
|
||||||
)) {
|
|
||||||
$fragment.append(await components.card(mod));
|
|
||||||
}
|
|
||||||
actionButtons.reload($fragment);
|
|
||||||
actionButtons.clearFilters($fragment);
|
|
||||||
return $fragment;
|
|
||||||
},
|
|
||||||
async (search = router.getSearch()) => {
|
|
||||||
for (const [filter, active] of [
|
|
||||||
['tag=theme', search.get('tag') === 'theme'],
|
|
||||||
['tag=extension', search.get('tag') === 'extension'],
|
|
||||||
['enabled', search.has('enabled')],
|
|
||||||
['disabled', search.has('disabled')],
|
|
||||||
]) {
|
|
||||||
document
|
|
||||||
.querySelector(`.action--buttons > [href="?view=library&${filter}"]`)
|
|
||||||
.classList[active ? 'add' : 'remove']('action--active');
|
|
||||||
}
|
|
||||||
const visible = new Set();
|
|
||||||
for (const mod of await registry.get()) {
|
|
||||||
const isEnabled = await registry.isEnabled(mod.id),
|
|
||||||
filterConditions =
|
|
||||||
(search.has('tag') ? mod.tags.includes(search.get('tag')) : true) &&
|
|
||||||
(search.has('enabled') && search.has('disabled')
|
|
||||||
? true
|
|
||||||
: search.has('enabled')
|
|
||||||
? isEnabled
|
|
||||||
: search.has('disabled')
|
|
||||||
? !isEnabled
|
|
||||||
: true);
|
|
||||||
if (filterConditions) visible.add(mod.id);
|
|
||||||
}
|
|
||||||
for (const card of document.querySelectorAll('main > .library--card'))
|
|
||||||
card.style.display = 'none';
|
|
||||||
for (const card of document.querySelectorAll('main > .library--card'))
|
|
||||||
if (visible.has(card.dataset.mod)) card.style.display = '';
|
|
||||||
actionButtons.clearFilters();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.addView(
|
|
||||||
'mod',
|
|
||||||
async () => {
|
|
||||||
const mod = (await registry.get()).find((mod) => mod.id === router.getSearch().get('id'));
|
|
||||||
if (!mod) return false;
|
|
||||||
const $fragment = web.createFragment(web.html`
|
|
||||||
<p class="action--buttons">
|
|
||||||
<a href="?view=library">
|
|
||||||
<span><i data-icon="fa/solid/long-arrow-alt-left"></i></span>
|
|
||||||
<span>back to library</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/notion-enhancer/extension/tree/main/repo/${encodeURIComponent(
|
|
||||||
mod._dir
|
|
||||||
)}">
|
|
||||||
<span><i data-icon="fa/solid/code"></i></span>
|
|
||||||
<span>view source code</span>
|
|
||||||
</a>
|
|
||||||
</p>`);
|
|
||||||
const $card = await components.card(mod);
|
|
||||||
$card.querySelector('.library--expand').remove();
|
|
||||||
if (mod.options && mod.options.length) {
|
|
||||||
const options = web.createElement(web.html`<div class="library--options"></div>`);
|
|
||||||
mod.options
|
|
||||||
.filter((opt) => !opt.environments || opt.environments.includes(env.name))
|
|
||||||
.forEach(async (opt) =>
|
|
||||||
options.append(await components.options[opt.type](mod.id, opt))
|
|
||||||
);
|
|
||||||
$card.append(options);
|
|
||||||
}
|
|
||||||
$fragment.append(
|
|
||||||
$card,
|
|
||||||
web.createElement(web.html`
|
|
||||||
<article class="documentation--body markdown">
|
|
||||||
${
|
|
||||||
(await fs.isFile(`repo/${mod._dir}/README.md`))
|
|
||||||
? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`))
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
</article>`)
|
|
||||||
);
|
|
||||||
fmt.Prism.highlightAllUnder($fragment);
|
|
||||||
actionButtons.reload($fragment);
|
|
||||||
return $fragment;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
if (document.querySelector('[data-mod]').dataset.mod !== router.getSearch().get('id'))
|
|
||||||
router.load(true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
router.setDefaultView('library');
|
|
||||||
router.load();
|
|
@ -6,13 +6,12 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// initialisation and external interactions
|
import { env, fs, storage, registry, web } from '../../api/_.mjs';
|
||||||
|
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e');
|
||||||
|
|
||||||
import * as api from '../../api/_.mjs';
|
import './styles.mjs';
|
||||||
const { env, fmt, fs, registry, web } = api,
|
import { notifications } from './notifications.mjs';
|
||||||
db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e');
|
import { components, options } from './components.mjs';
|
||||||
|
|
||||||
import { tw } from './styles.mjs';
|
|
||||||
|
|
||||||
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
|
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
|
||||||
|
|
||||||
@ -34,406 +33,209 @@ window.addEventListener('beforeunload', (event) => {
|
|||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
});
|
});
|
||||||
|
|
||||||
const notifications = {
|
|
||||||
$container: web.html`<div class="notifications-container"></div>`,
|
|
||||||
cache: await db.get(['notifications'], []),
|
|
||||||
provider: [
|
|
||||||
env.welcomeNotification,
|
|
||||||
...(await fs.getJSON('https://notion-enhancer.github.io/notifications.json')),
|
|
||||||
],
|
|
||||||
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 db.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">
|
|
||||||
${fmt.md.renderInline(message)}
|
|
||||||
</span>`,
|
|
||||||
web.html`${web.icon(icon, { class: 'notification-icon' })}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return $notification;
|
|
||||||
},
|
|
||||||
_onChange: false,
|
|
||||||
onChange() {
|
|
||||||
if (this._onChange) return;
|
|
||||||
this._onChange = true;
|
|
||||||
const $notification = this.add({
|
|
||||||
icon: 'refresh-cw',
|
|
||||||
message: 'Reload to apply changes.',
|
|
||||||
});
|
|
||||||
$notification.addEventListener('click', env.reload);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
web.render(document.body, notifications.$container);
|
|
||||||
for (const notification of notifications.provider) {
|
|
||||||
if (
|
|
||||||
!notifications.cache.includes(notification.id) &&
|
|
||||||
notification.version === env.version &&
|
|
||||||
(!notification.environments || notification.environments.includes(env.name))
|
|
||||||
) {
|
|
||||||
notifications.add(notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = await registry.errors();
|
|
||||||
if (errors.length) {
|
|
||||||
console.log('[notion-enhancer] registry errors:');
|
|
||||||
console.table(errors);
|
|
||||||
notifications.add({
|
|
||||||
icon: 'alert-circle',
|
|
||||||
message: 'Failed to load mods (check console).',
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// mod config
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
preview: (url) => web.html`<img
|
|
||||||
class="mod-preview"
|
|
||||||
src="${web.escape(url)}"
|
|
||||||
alt=""
|
|
||||||
>`,
|
|
||||||
title: (title) => web.html`<h4 class="mod-title"><span>${web.escape(title)}</span></h4>`,
|
|
||||||
version: (version) => web.html`<span class="mod-version">v${web.escape(version)}</span>`,
|
|
||||||
tags: (tags) => {
|
|
||||||
if (!tags.length) return '';
|
|
||||||
return web.render(
|
|
||||||
web.html`<p class="mod-tags"></p>`,
|
|
||||||
tags.map((tag) => `#${web.escape(tag)}`).join(' ')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
description: (description) => web.html`<p class="mod-description markdown-inline">
|
|
||||||
${fmt.md.renderInline(description)}
|
|
||||||
</p>`,
|
|
||||||
authors: (authors) => {
|
|
||||||
const author = (author) => web.html`<a
|
|
||||||
class="mod-author"
|
|
||||||
href="${web.escape(author.homepage)}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img class="mod-author-avatar"
|
|
||||||
src="${web.escape(author.avatar)}" alt="${web.escape(author.name)}'s avatar"
|
|
||||||
> <span>${web.escape(author.name)}</span>
|
|
||||||
</a>`;
|
|
||||||
return web.render(web.html`<p class="mod-authors-container"></p>`, ...authors.map(author));
|
|
||||||
},
|
|
||||||
toggle: (label, checked) => {
|
|
||||||
const $label = web.html`<label tabindex="0" class="toggle-label">
|
|
||||||
<span>${web.escape(label)}</span>
|
|
||||||
</label>`,
|
|
||||||
$input = web.html`<input tabindex="-1" type="checkbox" class="toggle-check"
|
|
||||||
${checked ? 'checked' : ''}>`,
|
|
||||||
$feature = web.html`<span class="toggle-box toggle-feature"></span>`;
|
|
||||||
$label.addEventListener('keyup', (event) => {
|
|
||||||
if (['Enter', ' '].includes(event.key)) $input.checked = !$input.checked;
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $feature);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const $main = web.html`<main class="main"></main>`,
|
const $main = web.html`<main class="main"></main>`,
|
||||||
$sidebar = web.html`<article class="sidebar"></article>`,
|
$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">
|
$options = web.html`<div class="options-container">
|
||||||
<p class="options-empty">Select a mod to view and configure its options.</p>
|
<p class="options-placeholder">Select a mod to view and configure its options.</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const options = {
|
let _$profileConfig;
|
||||||
toggle: async (mod, opt) => {
|
$profile.addEventListener('click', async (event) => {
|
||||||
const checked = await registry.profile.get([mod.id, opt.key], opt.value),
|
for (const $selected of document.querySelectorAll('.mod-selected')) {
|
||||||
$toggle = components.toggle(opt.label, checked),
|
$selected.className = 'mod';
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
}
|
||||||
$label = $toggle.children[0],
|
if (!_$profileConfig) {
|
||||||
$input = $toggle.children[1];
|
const profileNames = [
|
||||||
if (opt.tooltip) {
|
...new Set([
|
||||||
$label.prepend($tooltip);
|
...Object.keys(await storage.get(['profiles'], { default: {} })),
|
||||||
web.tooltip($tooltip, opt.tooltip);
|
registry.profileName,
|
||||||
}
|
]),
|
||||||
$input.addEventListener('change', async (event) => {
|
],
|
||||||
await registry.profile.set([mod.id, opt.key], $input.checked);
|
$options = profileNames.map(
|
||||||
notifications.onChange();
|
(profile) => web.raw`<option
|
||||||
});
|
class="select-option"
|
||||||
return $toggle;
|
value="${web.escape(profile)}"
|
||||||
},
|
${profile === registry.profileName ? 'selected' : ''}
|
||||||
select: async (mod, opt) => {
|
>${web.escape(profile)}</option>`
|
||||||
const value = await registry.profile.get([mod.id, opt.key], opt.values[0]),
|
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
),
|
||||||
$select = web.html`<select class="input">
|
$select = web.html`<select class="input">
|
||||||
${opt.values
|
<option class="select-option" value="--">-- new --</option>
|
||||||
.map(
|
${$options.join('')}
|
||||||
(option) =>
|
|
||||||
web.raw`<option
|
|
||||||
class="select-option"
|
|
||||||
value="${web.escape(option)}"
|
|
||||||
${option === value ? 'selected' : ''}
|
|
||||||
>${web.escape(option)}</option>`
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</select>`,
|
</select>`,
|
||||||
$icon = web.html`${web.icon('chevron-down', { class: 'input-icon' })}`;
|
$edit = web.html`<input
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
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) => {
|
$select.addEventListener('change', async (event) => {
|
||||||
await registry.profile.set([mod.id, opt.key], $select.value);
|
if ($select.value === '--') {
|
||||||
notifications.onChange();
|
$edit.value = '';
|
||||||
|
} else $edit.value = $select.value;
|
||||||
});
|
});
|
||||||
return web.render($label, $select, $icon);
|
$save.addEventListener('click', async (event) => {
|
||||||
},
|
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
||||||
text: async (mod, opt) => {
|
web.render(
|
||||||
const value = await registry.profile.get([mod.id, opt.key], opt.value),
|
web.empty($error),
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
`The profile "${web.escape($edit.value)}" already exists.`
|
||||||
$label = web.render(
|
);
|
||||||
web.html`<label class="input-label"></label>`,
|
return false;
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
|
||||||
$icon = web.html`${web.icon('type', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
|
||||||
$input.addEventListener('change', async (event) => {
|
|
||||||
await registry.profile.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
number: async (mod, opt) => {
|
|
||||||
const value = await registry.profile.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="number" class="input" value="${value}">`,
|
|
||||||
$icon = web.html`${web.icon('hash', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
|
||||||
$input.addEventListener('change', async (event) => {
|
|
||||||
await registry.profile.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
color: async (mod, opt) => {
|
|
||||||
const value = await registry.profile.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input">`,
|
|
||||||
$icon = web.html`${web.icon('droplet', { class: 'input-icon' })}`,
|
|
||||||
paint = () => {
|
|
||||||
$input.style.background = $picker.toBackground();
|
|
||||||
$input.style.color = $picker.isLight() ? '#000' : '#fff';
|
|
||||||
$input.style.padding = '';
|
|
||||||
},
|
|
||||||
$picker = new web.jscolor($input, {
|
|
||||||
value,
|
|
||||||
format: 'rgba',
|
|
||||||
previewSize: 0,
|
|
||||||
borderRadius: 3,
|
|
||||||
borderColor: 'var(--theme--ui_divider)',
|
|
||||||
controlBorderColor: 'var(--theme--ui_divider)',
|
|
||||||
backgroundColor: 'var(--theme--bg)',
|
|
||||||
onInput: paint,
|
|
||||||
onChange: paint,
|
|
||||||
});
|
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
|
||||||
$input.addEventListener('change', async (event) => {
|
|
||||||
await registry.profile.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
paint();
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
file: async (mod, opt) => {
|
|
||||||
const { filename } = (await registry.profile.get([mod.id, opt.key], {})) || {},
|
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
|
||||||
$pseudo = web.html`<span class="input"><span class="input-placeholder">Upload file...</span></span>`,
|
|
||||||
$input = web.html`<input type="file" class="hidden" accept=${web.escape(
|
|
||||||
opt.extensions.join(',')
|
|
||||||
)}>`,
|
|
||||||
$icon = web.html`${web.icon('file', { class: 'input-icon' })}`,
|
|
||||||
$filename = web.html`<span>${web.escape(filename || 'none')}</span>`,
|
|
||||||
$latest = web.render(web.html`<button class="file-latest">Latest: </button>`, $filename);
|
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
|
||||||
$input.addEventListener('change', (event) => {
|
|
||||||
const file = event.target.files[0],
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = async (progress) => {
|
|
||||||
$filename.innerText = file.name;
|
|
||||||
await registry.profile.set([mod.id, opt.key], {
|
|
||||||
filename: file.name,
|
|
||||||
content: progress.currentTarget.result,
|
|
||||||
});
|
|
||||||
notifications.onChange();
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
$latest.addEventListener('click', (event) => {
|
|
||||||
$filename.innerText = 'none';
|
|
||||||
registry.profile.set([mod.id, opt.key], {});
|
|
||||||
});
|
|
||||||
return web.render(
|
|
||||||
web.html`<div></div>`,
|
|
||||||
web.render($label, $input, $pseudo, $icon),
|
|
||||||
$latest
|
|
||||||
);
|
|
||||||
},
|
|
||||||
hotkey: async (mod, opt) => {
|
|
||||||
const value = await registry.profile.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
|
||||||
$icon = web.html`${web.icon('command', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip) web.tooltip($tooltip, opt.tooltip);
|
|
||||||
$input.addEventListener('keydown', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const pressed = [],
|
|
||||||
modifiers = {
|
|
||||||
metaKey: 'Meta',
|
|
||||||
ctrlKey: 'Control',
|
|
||||||
altKey: 'Alt',
|
|
||||||
shiftKey: 'Shift',
|
|
||||||
};
|
|
||||||
for (const modifier in modifiers) {
|
|
||||||
if (event[modifier]) pressed.push(modifiers[modifier]);
|
|
||||||
}
|
}
|
||||||
const empty = ['Backspace', 'Delete'].includes(event.key) && !pressed.length;
|
if (!$edit.value.match(/^[A-Za-z0-9_-]+$/)) {
|
||||||
if (!empty && !pressed.includes(event.key)) {
|
web.render(
|
||||||
let key = event.key;
|
web.empty($error),
|
||||||
if (key === ' ') key = 'Space';
|
'Profile names can only contain letters, numbers, dashes and underscores.'
|
||||||
if (key.length === 1) key = event.key.toUpperCase();
|
);
|
||||||
pressed.push(key);
|
return false;
|
||||||
}
|
}
|
||||||
$input.value = pressed.join('+');
|
await storage.set(['currentprofile'], $edit.value);
|
||||||
await registry.profile.set([mod.id, opt.key], $input.value);
|
if ($select.value === '--') {
|
||||||
notifications.onChange();
|
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();
|
||||||
});
|
});
|
||||||
return web.render($label, $input, $icon);
|
$delete.addEventListener('click', async (event) => {
|
||||||
},
|
await storage.set(['profiles', $select.value], undefined);
|
||||||
};
|
await storage.set(
|
||||||
|
['currentprofile'],
|
||||||
components.options = async (mod) => {
|
profileNames.find((profile) => profile !== $select.value) || 'default'
|
||||||
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-empty">No options.</p>`);
|
|
||||||
}
|
|
||||||
return $fragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
components.mod = async (mod) => {
|
|
||||||
const $mod = web.html`<div class="mod"></div>`,
|
|
||||||
$toggle = components.toggle('', await registry.enabled(mod.id));
|
|
||||||
$toggle.addEventListener('change', (event) => {
|
|
||||||
registry.profile.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';
|
|
||||||
web.render(
|
|
||||||
web.empty($options),
|
|
||||||
web.render(components.title(mod.name), components.version(mod.version)),
|
|
||||||
components.tags(mod.tags),
|
|
||||||
await components.options(mod)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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 : ''
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
components._$modListCache = {};
|
|
||||||
components.modList = async (category) => {
|
|
||||||
if (!components._$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());
|
location.reload();
|
||||||
$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);
|
_$profileConfig = web.render(
|
||||||
web.render($list, await components.mod(mod));
|
|
||||||
mod.tags.unshift(category);
|
|
||||||
}
|
|
||||||
components._$modListCache[category] = web.render(
|
|
||||||
web.html`<div></div>`,
|
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.render(
|
||||||
web.html`<label class="search-container"></label>`,
|
web.html`<label class="input-label"></label>`,
|
||||||
$search,
|
$select,
|
||||||
web.html`${web.icon('search', { class: 'input-icon' })}`
|
web.html`${web.icon('chevron-down', { class: 'input-icon' })}`
|
||||||
),
|
),
|
||||||
$list
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return components._$modListCache[category];
|
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">
|
const $notionNavItem = web.html`<h1 class="nav-notion">
|
||||||
${(await fs.getText('icon/colour.svg')).replace(
|
${(await fs.getText('icon/colour.svg')).replace(
|
||||||
@ -465,7 +267,7 @@ web.render(
|
|||||||
),
|
),
|
||||||
$main
|
$main
|
||||||
),
|
),
|
||||||
web.render($sidebar, $options)
|
web.render($sidebar, $profile, $options)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -481,19 +283,19 @@ import * as router from './router.mjs';
|
|||||||
router.addView('core', async () => {
|
router.addView('core', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($coreNavItem);
|
selectNavItem($coreNavItem);
|
||||||
return web.render($main, await components.modList('core'));
|
return web.render($main, await generators.modList('core'));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.addView('extensions', async () => {
|
router.addView('extensions', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($extensionsNavItem);
|
selectNavItem($extensionsNavItem);
|
||||||
return web.render($main, await components.modList('extension'));
|
return web.render($main, await generators.modList('extension'));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.addView('themes', async () => {
|
router.addView('themes', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($themesNavItem);
|
selectNavItem($themesNavItem);
|
||||||
return web.render($main, await components.modList('theme'));
|
return web.render($main, await generators.modList('theme'));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.loadView('extensions', $main);
|
router.loadView('extensions', $main);
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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, fmt, registry, web } from '../../api/_.mjs';
|
||||||
|
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e');
|
||||||
|
|
||||||
|
import { tw } from './styles.mjs';
|
||||||
|
|
||||||
|
export const notifications = {
|
||||||
|
$container: web.html`<div class="notifications-container"></div>`,
|
||||||
|
cache: await db.get(['notifications'], []),
|
||||||
|
provider: [
|
||||||
|
env.welcomeNotification,
|
||||||
|
...(await fs.getJSON('https://notion-enhancer.github.io/notifications.json')),
|
||||||
|
],
|
||||||
|
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 db.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">
|
||||||
|
${fmt.md.renderInline(message)}
|
||||||
|
</span>`,
|
||||||
|
web.html`${web.icon(icon, { class: 'notification-icon' })}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return $notification;
|
||||||
|
},
|
||||||
|
_onChange: false,
|
||||||
|
onChange() {
|
||||||
|
if (this._onChange) return;
|
||||||
|
this._onChange = true;
|
||||||
|
const $notification = this.add({
|
||||||
|
icon: 'refresh-cw',
|
||||||
|
message: 'Reload to apply changes.',
|
||||||
|
});
|
||||||
|
$notification.addEventListener('click', env.reload);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
web.render(document.body, notifications.$container);
|
||||||
|
for (const notification of notifications.provider) {
|
||||||
|
if (
|
||||||
|
!notifications.cache.includes(notification.id) &&
|
||||||
|
notification.version === env.version &&
|
||||||
|
(!notification.environments || notification.environments.includes(env.name))
|
||||||
|
) {
|
||||||
|
notifications.add(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = await registry.errors();
|
||||||
|
if (errors.length) {
|
||||||
|
console.log('[notion-enhancer] registry errors:');
|
||||||
|
console.table(errors);
|
||||||
|
notifications.add({
|
||||||
|
icon: 'alert-circle',
|
||||||
|
message: 'Failed to load mods (check console).',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
}
|
@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
import { web } from '../../api/_.mjs';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
let _defaultView = '',
|
let _defaultView = '';
|
||||||
$viewRoot;
|
|
||||||
const _views = new Map();
|
const _views = new Map();
|
||||||
|
|
||||||
export function addView(name, loadFunc) {
|
export function addView(name, loadFunc) {
|
||||||
|
@ -40,14 +40,14 @@ const customClasses = {
|
|||||||
'nav-notion': apply`flex items-center font-semibold text-xl cursor-pointer select-none mr-4
|
'nav-notion': apply`flex items-center font-semibold text-xl cursor-pointer select-none mr-4
|
||||||
ml-4 sm:mb-4 md:w-full lg:(w-auto ml-0 mb-0)`,
|
ml-4 sm:mb-4 md:w-full lg:(w-auto ml-0 mb-0)`,
|
||||||
'nav-notion-icon': apply`h-12 w-12 mr-5 sm:(h-6 w-6 mr-3)`,
|
'nav-notion-icon': apply`h-12 w-12 mr-5 sm:(h-6 w-6 mr-3)`,
|
||||||
'nav-item': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium hover:bg-interactive-hover focus:bg-interactive-active`,
|
'nav-item': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium hover:bg-interactive-hover focus:bg-interactive-focus`,
|
||||||
'nav-item-selected': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium ring-1 ring-divider bg-notion-secondary`,
|
'nav-item-selected': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium ring-1 ring-divider bg-notion-secondary`,
|
||||||
'main': apply`transition px-4 py-3 overflow-y-auto max-h-full-48 sm:max-h-full-32 lg:max-h-full-16`,
|
'main': apply`transition px-4 py-3 overflow-y-auto max-h-full-48 sm:max-h-full-32 lg:max-h-full-16`,
|
||||||
'mods-list': apply`flex flex-wrap`,
|
'mods-list': apply`flex flex-wrap`,
|
||||||
'mod-container': apply`w-full md:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5 px-2.5 py-2.5 box-border`,
|
'mod-container': apply`w-full md:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5 px-2.5 py-2.5 box-border`,
|
||||||
'mod': apply`relative h-full w-full flex flex-col overflow-hidden rounded-lg shadow-lg
|
'mod': apply`relative h-full w-full flex flex-col overflow-hidden rounded-lg shadow-lg
|
||||||
bg-notion-secondary border border-divider cursor-pointer`,
|
bg-notion-secondary border border-divider cursor-pointer`,
|
||||||
'mod-selected': apply`mod ring ring-accent-blue-active`,
|
'mod-selected': apply`mod ring ring-accent-blue-focus`,
|
||||||
'mod-body': apply`px-4 py-3 flex flex-col flex-auto children:cursor-pointer`,
|
'mod-body': apply`px-4 py-3 flex flex-col flex-auto children:cursor-pointer`,
|
||||||
'mod-preview': apply`object-cover w-full h-32`,
|
'mod-preview': apply`object-cover w-full h-32`,
|
||||||
'mod-title': apply`mb-2 text-xl font-semibold tracking-tight flex items-center`,
|
'mod-title': apply`mb-2 text-xl font-semibold tracking-tight flex items-center`,
|
||||||
@ -57,16 +57,24 @@ const customClasses = {
|
|||||||
'mod-authors-container': apply`text-sm font-medium`,
|
'mod-authors-container': apply`text-sm font-medium`,
|
||||||
'mod-author': apply`flex items-center mb-2`,
|
'mod-author': apply`flex items-center mb-2`,
|
||||||
'mod-author-avatar': apply`inline object-cover w-5 h-5 rounded-full mr-2`,
|
'mod-author-avatar': apply`inline object-cover w-5 h-5 rounded-full mr-2`,
|
||||||
|
'sidebar': apply`h-full w-96 px-4 pt-3 pb-32 flex flex-col bg-notion-secondary border-l border-divider`,
|
||||||
|
'profile-button': apply`block px-4 py-3 mb-2 rounded-md text-sm text-left font-semibold shadow-inner
|
||||||
|
bg-accent-red-hover border border-accent-red text-accent-red focus:(outline-none ring ring-inset ring-accent-red)`,
|
||||||
|
'profile-save': apply`text-sm px-3 py-2 font-medium mt-2 bg-accent-blue text-accent-blue-text rounded-md
|
||||||
|
hover:bg-accent-blue-hover focus:(bg-accent-blue-focus outline-none)`,
|
||||||
|
'profile-delete': apply`text-sm px-3 py-2 font-medium ml-3 mt-3 bg-red-tag text-red-tag-text rounded-md
|
||||||
|
border border-red-text hover:bg-red-text focus:(outline-none bg-red-text)`,
|
||||||
|
'profile-error': apply`text-xs mt-2 text-red-text`,
|
||||||
|
'button-icon': apply`w-4 h-4 -mt-1 inline-block mr-1`,
|
||||||
|
'options-container': apply`px-4 py-3 shadow-inner rounded-lg bg-notion border border-divider space-y-3`,
|
||||||
|
'options-placeholder': apply`text-sm text-foreground-secondary`,
|
||||||
'toggle-box': apply`w-9 h-5 p-0.5 flex items-center bg-toggle-off rounded-full duration-300 ease-in-out cursor-pointer`,
|
'toggle-box': apply`w-9 h-5 p-0.5 flex items-center bg-toggle-off rounded-full duration-300 ease-in-out cursor-pointer`,
|
||||||
'toggle-label': apply`relative text-sm flex w-full mt-auto`,
|
'toggle-label': apply`relative text-sm flex w-full mt-auto`,
|
||||||
'toggle-check': apply`appearance-none ml-auto checked:sibling:(bg-toggle-on after::translate-x-4)`,
|
'toggle-check': apply`appearance-none ml-auto checked:sibling:(bg-toggle-on after::translate-x-4)`,
|
||||||
'toggle-feature': apply`after::(${pseudoContent} w-4 h-4 bg-toggle-feature rounded-full duration-300) cursor-pointer`,
|
'toggle-feature': apply`after::(${pseudoContent} w-4 h-4 bg-toggle-feature rounded-full duration-300) cursor-pointer`,
|
||||||
'sidebar': apply`h-full w-96 px-4 pt-3 pb-32 flex flex-col bg-notion-secondary border-l border-divider`,
|
|
||||||
'options-container': apply`px-4 py-3 shadow-inner rounded-lg bg-notion border border-divider space-y-3`,
|
|
||||||
'options-empty': apply`text-sm text-foreground-secondary`,
|
|
||||||
'input-label': apply`block text-sm mt-2 relative`,
|
'input-label': apply`block text-sm mt-2 relative`,
|
||||||
'input': apply`transition block w-full mt-2 pl-3 pr-14 py-2 text-sm rounded-md flex bg-input text-foreground
|
'input': apply`transition block w-full mt-2 pl-3 pr-14 py-2 text-sm rounded-md flex bg-input text-foreground
|
||||||
appearance-none placeholder-foreground-secondary ring-1 ring-divider focus:(outline-none ring ring-accent-blue-active)`,
|
appearance-none placeholder-foreground-secondary ring-1 ring-divider focus:(outline-none ring ring-accent-blue-focus)`,
|
||||||
'input-tooltip': apply`h-4 w-4 -mt-1 inline-block mr-2`,
|
'input-tooltip': apply`h-4 w-4 -mt-1 inline-block mr-2`,
|
||||||
'input-icon': apply`absolute w-11 h-9 right-0 bottom-0 py-2 px-3 bg-notion-secondary rounded-r-md text-icon`,
|
'input-icon': apply`absolute w-11 h-9 right-0 bottom-0 py-2 px-3 bg-notion-secondary rounded-r-md text-icon`,
|
||||||
'input-placeholder': apply`text-foreground-secondary`,
|
'input-placeholder': apply`text-foreground-secondary`,
|
||||||
@ -96,8 +104,8 @@ setup({
|
|||||||
'icon-secondary': 'var(--theme--icon_secondary)',
|
'icon-secondary': 'var(--theme--icon_secondary)',
|
||||||
'foreground': 'var(--theme--text)',
|
'foreground': 'var(--theme--text)',
|
||||||
'foreground-secondary': 'var(--theme--text_secondary)',
|
'foreground-secondary': 'var(--theme--text_secondary)',
|
||||||
'interactive-active': 'var(--theme--ui_interactive-active)',
|
|
||||||
'interactive-hover': 'var(--theme--ui_interactive-hover)',
|
'interactive-hover': 'var(--theme--ui_interactive-hover)',
|
||||||
|
'interactive-focus': 'var(--theme--ui_interactive-focus)',
|
||||||
'tag': 'var(--theme--tag_default)',
|
'tag': 'var(--theme--tag_default)',
|
||||||
'tag-text': 'var(--theme--tag_default-text)',
|
'tag-text': 'var(--theme--tag_default-text)',
|
||||||
'toggle': {
|
'toggle': {
|
||||||
@ -108,7 +116,7 @@ setup({
|
|||||||
'accent': {
|
'accent': {
|
||||||
'blue': 'var(--theme--accent_blue)',
|
'blue': 'var(--theme--accent_blue)',
|
||||||
'blue-hover': 'var(--theme--accent_blue-hover)',
|
'blue-hover': 'var(--theme--accent_blue-hover)',
|
||||||
'blue-active': 'var(--theme--accent_blue-active)',
|
'blue-focus': 'var(--theme--accent_blue-focus)',
|
||||||
'blue-text': 'var(--theme--accent_blue-text)',
|
'blue-text': 'var(--theme--accent_blue-text)',
|
||||||
'red': 'var(--theme--accent_red)',
|
'red': 'var(--theme--accent_red)',
|
||||||
'red-hover': 'var(--theme--accent_red-hover)',
|
'red-hover': 'var(--theme--accent_red-hover)',
|
||||||
|
@ -349,7 +349,7 @@ body,
|
|||||||
.notion-default-overlay-container
|
.notion-default-overlay-container
|
||||||
[style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];']
|
[style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];']
|
||||||
.notion-focusable[style*='background: rgb(223, 223, 222);'] {
|
.notion-focusable[style*='background: rgb(223, 223, 222);'] {
|
||||||
background: var(--theme--ui_interactive-active) !important;
|
background: var(--theme--ui_interactive-focus) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notion-focusable-within,
|
.notion-focusable-within,
|
||||||
@ -641,8 +641,8 @@ body,
|
|||||||
}
|
}
|
||||||
.notion-focusable-within:focus-within,
|
.notion-focusable-within:focus-within,
|
||||||
.notion-focusable:focus-visible {
|
.notion-focusable:focus-visible {
|
||||||
box-shadow: var(--theme--accent_blue-active, rgba(26, 170, 220, 0.7)) 0px 0px 0px 1px inset,
|
box-shadow: var(--theme--accent_blue-focus, rgba(26, 170, 220, 0.7)) 0px 0px 0px 1px inset,
|
||||||
var(--theme--accent_blue-active, rgba(26, 170, 220, 0.4)) 0px 0px 0px 2px !important;
|
var(--theme--accent_blue-focus, rgba(26, 170, 220, 0.4)) 0px 0px 0px 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulsing-button-border {
|
@keyframes pulsing-button-border {
|
||||||
@ -661,7 +661,7 @@ body,
|
|||||||
background: var(--theme--accent_blue-hover) !important;
|
background: var(--theme--accent_blue-hover) !important;
|
||||||
}
|
}
|
||||||
[style*='background: rgb(0, 141, 190);'] {
|
[style*='background: rgb(0, 141, 190);'] {
|
||||||
background: var(--theme--accent_blue-active) !important;
|
background: var(--theme--accent_blue-focus) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[style*='background-color: rgb(235, 87, 87); height: 28px; width: 28px;'] {
|
[style*='background-color: rgb(235, 87, 87); height: 28px; width: 28px;'] {
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
--theme--accent_blue: rgb(46, 170, 220);
|
--theme--accent_blue: rgb(46, 170, 220);
|
||||||
--theme--accent_blue-selection: rgb(46, 170, 220, 0.25);
|
--theme--accent_blue-selection: rgb(46, 170, 220, 0.25);
|
||||||
--theme--accent_blue-hover: rgb(6, 156, 205);
|
--theme--accent_blue-hover: rgb(6, 156, 205);
|
||||||
--theme--accent_blue-active: rgb(0, 141, 190);
|
--theme--accent_blue-focus: rgb(0, 141, 190);
|
||||||
--theme--accent_blue-text: #fff;
|
--theme--accent_blue-text: #fff;
|
||||||
--theme--accent_red: #eb5757;
|
--theme--accent_red: #eb5757;
|
||||||
--theme--accent_red-hover: rgba(235, 87, 87, 0.1);
|
--theme--accent_red-hover: rgba(235, 87, 87, 0.1);
|
||||||
@ -47,7 +47,7 @@
|
|||||||
--theme--ui_shadow: rgba(15, 15, 15, 0.15);
|
--theme--ui_shadow: rgba(15, 15, 15, 0.15);
|
||||||
--theme--ui_divider: rgb(237, 237, 236);
|
--theme--ui_divider: rgb(237, 237, 236);
|
||||||
--theme--ui_interactive-hover: rgba(55, 53, 47, 0.08);
|
--theme--ui_interactive-hover: rgba(55, 53, 47, 0.08);
|
||||||
--theme--ui_interactive-active: rgba(55, 53, 47, 0.16);
|
--theme--ui_interactive-focus: rgba(55, 53, 47, 0.16);
|
||||||
--theme--ui_toggle-on: var(--theme--accent_blue);
|
--theme--ui_toggle-on: var(--theme--accent_blue);
|
||||||
--theme--ui_toggle-off: rgba(135, 131, 120, 0.3);
|
--theme--ui_toggle-off: rgba(135, 131, 120, 0.3);
|
||||||
--theme--ui_toggle-feature: #fff;
|
--theme--ui_toggle-feature: #fff;
|
||||||
@ -211,7 +211,7 @@
|
|||||||
--theme--ui_shadow: rgba(15, 15, 15, 0.15);
|
--theme--ui_shadow: rgba(15, 15, 15, 0.15);
|
||||||
--theme--ui_divider: rgb(255, 255, 255, 0.07);
|
--theme--ui_divider: rgb(255, 255, 255, 0.07);
|
||||||
--theme--ui_interactive-hover: rgb(71, 76, 80);
|
--theme--ui_interactive-hover: rgb(71, 76, 80);
|
||||||
--theme--ui_interactive-active: rgb(63, 68, 71);
|
--theme--ui_interactive-focus: rgb(63, 68, 71);
|
||||||
--theme--ui_toggle-on: var(--theme--accent_blue);
|
--theme--ui_toggle-on: var(--theme--accent_blue);
|
||||||
--theme--ui_toggle-off: rgba(202, 204, 206, 0.3);
|
--theme--ui_toggle-off: rgba(202, 204, 206, 0.3);
|
||||||
--theme--ui_toggle-feature: #fff;
|
--theme--ui_toggle-feature: #fff;
|
||||||
|
Loading…
Reference in New Issue
Block a user