save options in menu

This commit is contained in:
dragonwocky 2021-04-25 23:29:01 +10:00
parent 8d0ce2e777
commit 15b34ef638
6 changed files with 168 additions and 142 deletions

View File

@ -6,33 +6,56 @@
'use strict'; 'use strict';
export const ERROR = Symbol(); export const ERROR = Symbol(),
env = {},
storage = {},
fs = {},
web = {},
fmt = {},
regexers = {},
registry = {};
export const env = {};
env.name = 'extension'; env.name = 'extension';
env.version = chrome.runtime.getManifest().version; env.version = chrome.runtime.getManifest().version;
env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' }); env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' }); env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
/** - */ storage.get = (namespace, key = undefined, fallback = undefined) =>
export const storage = {};
storage.set = (id, key, value) =>
new Promise((res, rej) => chrome.storage.sync.set({ [`[${id}]${key}`]: value }, res));
storage.get = (id, key, fallback = undefined) =>
new Promise((res, rej) => new Promise((res, rej) =>
chrome.storage.sync.get([`[${id}]${key}`], (values) => chrome.storage.sync.get([namespace], async (values) => {
res(values[`[${id}]${key}`] ?? fallback) values =
) values[namespace] && Object.getOwnPropertyNames(values[namespace]).length
? values[namespace]
: await registry.defaults(namespace);
res((key ? values[key] : values) ?? fallback);
})
); );
storage.set = (namespace, key, value) =>
new Promise(async (res, rej) => {
const values = await storage.get(namespace, undefined, {});
chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res);
});
storage.reset = (namespace) =>
new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res));
/** - */ fs.getJSON = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.json()
);
fs.getText = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.text()
);
fs.isFile = async (path) => {
try {
await fetch(chrome.runtime.getURL(path));
return true;
} catch {
return false;
}
};
export const web = {}; web.whenReady = (selectors = []) => {
web.whenReady = (selectors = [], callback = () => {}) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
function onLoad() { function onLoad() {
let isReadyInt; let isReadyInt;
@ -40,7 +63,6 @@ web.whenReady = (selectors = [], callback = () => {}) => {
function isReadyTest() { function isReadyTest() {
if (selectors.every((selector) => document.querySelector(selector))) { if (selectors.every((selector) => document.querySelector(selector))) {
clearInterval(isReadyInt); clearInterval(isReadyInt);
callback();
res(true); res(true);
} }
} }
@ -53,11 +75,12 @@ web.whenReady = (selectors = [], callback = () => {}) => {
} else onLoad(); } else onLoad();
}); });
}; };
web.loadStyleset = (path) => {
/** document.head.appendChild(
* @param {string} html web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(path)}">`)
* @returns HTMLElement );
*/ return true;
};
web.createElement = (html) => { web.createElement = (html) => {
const template = document.createElement('template'); const template = document.createElement('template');
template.innerHTML = html.includes('<pre') template.innerHTML = html.includes('<pre')
@ -70,25 +93,14 @@ web.createElement = (html) => {
}; };
web.escapeHtml = (str) => web.escapeHtml = (str) =>
str str
.replace(/&/g, '&amp;') // (?![^\s]+;) .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/'/g, '&#39;') .replace(/'/g, '&#39;')
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
// why a tagged template? because it syntax highlights // why a tagged template? because it syntax highlights
// https://marketplace.visualstudio.com/items?itemName=bierner.lit-html // https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
web.html = (html, ...templates) => html.map((str) => str + (templates.shift() || '')).join(''); web.html = (html, ...templates) => html.map((str) => str + (templates.shift() ?? '')).join('');
/**
* @param {string} sheet
*/
web.loadStyleset = (sheet) => {
document.head.appendChild(
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(sheet)}">`)
);
return true;
};
/** /**
* @param {array} keys * @param {array} keys
@ -119,10 +131,6 @@ web.hotkeyListener = (keys, callback) => {
web._hotkeys.push({ keys, callback }); web._hotkeys.push({ keys, callback });
}; };
/** - */
export const fmt = {};
import './dep/prism.js'; import './dep/prism.js';
fmt.Prism = Prism; fmt.Prism = Prism;
fmt.Prism.manual = true; fmt.Prism.manual = true;
@ -188,69 +196,36 @@ fmt.slugger = (heading, slugs = new Set()) => {
return slug; return slug;
}; };
/** - */ regexers.uuid = (str) => {
const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
export const fs = {}; if (match && match.length) return true;
error(`invalid uuid ${str}`);
/** return false;
* @param {string} path
*/
fs.getJSON = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.json()
);
fs.getText = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.text()
);
fs.isFile = async (path) => {
try {
await fetch(chrome.runtime.getURL(path));
return true;
} catch {
return false;
}
}; };
regexers.semver = (str) => {
/** - */ const match = str.match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i
export const regexers = { );
uuid(str) { if (match && match.length) return true;
const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); error(`invalid semver ${str}`);
if (match && match.length) return true; return false;
error(`invalid uuid ${str}`); };
return false; regexers.email = (str) => {
}, const match = str.match(
semver(str) { /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i
const match = str.match( );
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i if (match && match.length) return true;
); error(`invalid email ${str}`);
if (match && match.length) return true; return false;
error(`invalid semver ${str}`); };
return false; regexers.url = (str) => {
}, const match = str.match(
email(str) { /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i
const match = str.match( );
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i if (match && match.length) return true;
); error(`invalid url ${str}`);
if (match && match.length) return true; return false;
error(`invalid email ${str}`);
return false;
},
url(str) {
const match = str.match(
/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i
);
if (match && match.length) return true;
error(`invalid url ${str}`);
return false;
},
}; };
/** - */
export const registry = {};
registry.validate = async (mod, err, check) => { registry.validate = async (mod, err, check) => {
let conditions = [ let conditions = [
@ -444,8 +419,33 @@ registry.validate = async (mod, err, check) => {
} while (conditions.some((condition) => Array.isArray(condition))); } while (conditions.some((condition) => Array.isArray(condition)));
return conditions; return conditions;
}; };
registry.defaults = async (id) => {
const mod = (await registry.get()).find((mod) => mod.id === id);
if (!mod || !mod.options) return {};
const defaults = {};
for (const opt of mod.options) {
switch (opt.type) {
case 'toggle':
defaults[opt.key] = opt.value;
break;
case 'select':
defaults[opt.key] = opt.values[0];
break;
case 'text':
defaults[opt.key] = opt.value;
break;
case 'number':
defaults[opt.key] = opt.value;
break;
case 'file':
defaults[opt.key] = undefined;
break;
}
}
return defaults;
};
registry.get = async (callback = () => {}) => { registry.get = async () => {
registry._list = []; registry._list = [];
if (!registry._errors) registry._errors = []; if (!registry._errors) registry._errors = [];
for (const dir of await fs.getJSON('repo/registry.json')) { for (const dir of await fs.getJSON('repo/registry.json')) {
@ -466,12 +466,9 @@ registry.get = async (callback = () => {}) => {
err('invalid mod.json'); err('invalid mod.json');
} }
} }
callback(registry._list);
return registry._list; return registry._list;
}; };
registry.errors = async () => {
registry.errors = async (callback = () => {}) => {
if (!registry._errors) await registry.get(); if (!registry._errors) await registry.get();
callback(registry._errors);
return registry._errors; return registry._errors;
}; };

View File

@ -7,7 +7,7 @@
'use strict'; 'use strict';
import(chrome.runtime.getURL('helpers.js')).then(({ web, registry }) => { import(chrome.runtime.getURL('helpers.js')).then(({ web, registry }) => {
web.whenReady([], async () => { web.whenReady().then(async () => {
for (let mod of await registry.get()) { for (let mod of await registry.get()) {
for (let sheet of mod.css?.client || []) { for (let sheet of mod.css?.client || []) {
web.loadStyleset(`repo/${mod._dir}/${sheet}`); web.loadStyleset(`repo/${mod._dir}/${sheet}`);

View File

@ -51,8 +51,6 @@
display: flex; display: flex;
} }
.enhancer--notifications > div > :last-child > div { .enhancer--notifications > div > :last-child > div {
margin-left: auto;
margin-bottom: 2px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -11,7 +11,7 @@ import { env, storage, web, fs } from '../../helpers.js';
const sidebarSelector = const sidebarSelector =
'#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)'; '#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)';
web.whenReady([sidebarSelector], async () => { web.whenReady([sidebarSelector]).then(async () => {
const $enhancerSidebarElement = web.createElement( const $enhancerSidebarElement = web.createElement(
web.html`<div class="enhancer--sidebarMenuTrigger" role="button" tabindex="0"> web.html`<div class="enhancer--sidebarMenuTrigger" role="button" tabindex="0">
<div> <div>

View File

@ -272,7 +272,7 @@ label > span:not([class]) {
height: 0.8rem; height: 0.8rem;
width: 0.8rem; width: 0.8rem;
left: 0.325rem; left: 0.325rem;
top: 0.225rem; top: 0.18rem;
position: absolute; position: absolute;
border-radius: 100%; border-radius: 100%;
background: var(--theme--toggle_dot); background: var(--theme--toggle_dot);

View File

@ -87,16 +87,22 @@ components.card = {
}, },
}; };
components.options = { components.options = {
toggle: (id, { key, label, value }) => async toggle(id, { key, label }) {
web.createElement(web.html`<label const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="toggle--${web.escapeHtml(`${id}.${key}`)}" for="toggle--${web.escapeHtml(`${id}.${key}`)}"
class="library--toggle_label" class="library--toggle_label"
> >
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}" /> <input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
${state ? 'checked' : ''}/>
<p><span>${label}</span><span class="library--toggle"></span></p <p><span>${label}</span><span class="library--toggle"></span></p
></label>`), ></label>`);
select: async (id, { key, label, values }) => opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked));
web.createElement(web.html`<label return opt;
},
async select(id, { key, label, values }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="select--${web.escapeHtml(`${id}.${key}`)}" for="select--${web.escapeHtml(`${id}.${key}`)}"
class="library--select_label" class="library--select_label"
> >
@ -106,37 +112,47 @@ components.options = {
<select id="select--${web.escapeHtml(`${id}.${key}`)}"> <select id="select--${web.escapeHtml(`${id}.${key}`)}">
${values.map( ${values.map(
(value) => (value) =>
web.html`<option value="${web.escapeHtml(value)}">${web.escapeHtml( web.html`<option value="${web.escapeHtml(value)}"
value ${value === state ? 'selected' : ''}>
)}</option>` ${web.escapeHtml(value)}</option>`
)} )}
</select> </select>
</p> </p>
</label>`), </label>`);
text(id, { key, label, value }) { opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
const opt = web.createElement(web.html`<label return opt;
},
async text(id, { key, label }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="text--${web.escapeHtml(`${id}.${key}`)}" for="text--${web.escapeHtml(`${id}.${key}`)}"
class="library--text_label" class="library--text_label"
> >
<p>${label}</p> <p>${label}</p>
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}" rows="1"></textarea> <textarea id="text--${web.escapeHtml(`${id}.${key}`)}" rows="1">${state}</textarea>
</label>`); </label>`);
opt.querySelector('textarea').addEventListener('input', (ev) => { opt.querySelector('textarea').addEventListener('input', (event) => {
ev.target.style.removeProperty('--txt--scroll-height'); event.target.style.removeProperty('--txt--scroll-height');
ev.target.style.setProperty('--txt--scroll-height', ev.target.scrollHeight + 'px'); event.target.style.setProperty('--txt--scroll-height', event.target.scrollHeight + 'px');
}); });
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
return opt; return opt;
}, },
number: (id, { key, label, value }) => async number(id, { key, label }) {
web.createElement(web.html`<label const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="number--${web.escapeHtml(`${id}.${key}`)}" for="number--${web.escapeHtml(`${id}.${key}`)}"
class="library--number_label" class="library--number_label"
> >
<p>${web.escapeHtml(label)}</p> <p>${web.escapeHtml(label)}</p>
<input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" /> <input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" value="${state}"/>
</label>`), </label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
return opt;
},
async file(id, { key, label, extensions }) { async file(id, { key, label, extensions }) {
const opt = web.createElement(web.html`<label const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="file--${web.escapeHtml(`${id}.${key}`)}" for="file--${web.escapeHtml(`${id}.${key}`)}"
class="library--file_label" class="library--file_label"
> >
@ -145,18 +161,33 @@ components.options = {
id="file--${web.escapeHtml(`${id}.${key}`)}" id="file--${web.escapeHtml(`${id}.${key}`)}"
${web.escapeHtml( ${web.escapeHtml(
extensions && extensions.length extensions && extensions.length
? ` accept="${web.escapeHtml(extensions.join(','))}"` ? ` accept=${web.escapeHtml(extensions.join(','))}`
: '' : ''
)} )}
/> />
<p>${web.escapeHtml(label)}</p> <p>${web.escapeHtml(label)}</p>
<p class="library--file"> <p class="library--file">
<span><i data-icon="fa/file"></i></span> <span><i data-icon="fa/file"></i></span>
<span class="library--file_path">choose file...</span> <span class="library--file_path">${state || 'choose file...'}</span>
</p>
<p class="library--warning">
warning: browser extensions do not have true filesystem access,
so the content of the file is saved on selection. after editing it,
the file will need to be re-selected.
</p> </p>
</label>`); </label>`);
opt.querySelector('input').addEventListener('change', (ev) => { opt.addEventListener('change', (event) => {
opt.querySelector('.library--file_path').innerText = ev.target.files[0].name; const file = event.target.files[0],
reader = new FileReader();
opt.querySelector('.library--file_path').innerText = file.name;
storage.set(id, key, file.name);
reader.onload = (progress) => {
storage.set(id, `_file.${key}`, progress.currentTarget.result);
};
reader.readAsText(file);
});
opt.addEventListener('click', (event) => {
document.documentElement.scrollTop = 0;
}); });
return opt; return opt;
}, },
@ -278,7 +309,7 @@ const views = {
}, 50); }, 50);
document document
.querySelectorAll('img') .querySelectorAll('img')
.forEach((img) => (img.onerror = (ev) => ev.target.remove())); .forEach((img) => (img.onerror = (event) => event.target.remove()));
document document
.querySelectorAll('a[href^="?"]') .querySelectorAll('a[href^="?"]')
.forEach((a) => a.addEventListener('click', this._router)); .forEach((a) => a.addEventListener('click', this._router));
@ -310,12 +341,12 @@ const views = {
views._router = views._router.bind(views); views._router = views._router.bind(views);
views._navigator = views._navigator.bind(views); views._navigator = views._navigator.bind(views);
views._load(); views._load();
window.addEventListener('popstate', (ev) => { window.addEventListener('popstate', (event) => {
if (ev.state) views._load(); if (event.state) views._load();
}); });
const notifications = { const notifications = {
_generate({ heading, message = '', type = 'information' }, callback = () => {}) { _generate({ heading, message = '', type = 'information' }, onDismiss = () => {}) {
let svg = '', let svg = '',
className = 'notification'; className = 'notification';
switch (type) { switch (type) {
@ -347,7 +378,7 @@ const notifications = {
$notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize) $notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize)
}rem`; }rem`;
setTimeout(() => $notif.remove(), 400); setTimeout(() => $notif.remove(), 400);
callback(); onDismiss();
}); });
setTimeout(() => { setTimeout(() => {
$notif.style.opacity = 1; $notif.style.opacity = 1;
@ -355,7 +386,7 @@ const notifications = {
return $notif; return $notif;
}, },
async load() { async load() {
let notifications = { const notifications = {
list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'), list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'),
dismissed: await storage.get(_id, 'notifications', []), dismissed: await storage.get(_id, 'notifications', []),
}; };