mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 07:49:02 +00:00
save options in menu
This commit is contained in:
parent
8d0ce2e777
commit
15b34ef638
extension
@ -6,33 +6,56 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
export const ERROR = Symbol();
|
||||
export const ERROR = Symbol(),
|
||||
env = {},
|
||||
storage = {},
|
||||
fs = {},
|
||||
web = {},
|
||||
fmt = {},
|
||||
regexers = {},
|
||||
registry = {};
|
||||
|
||||
export const env = {};
|
||||
env.name = 'extension';
|
||||
env.version = chrome.runtime.getManifest().version;
|
||||
|
||||
env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
|
||||
env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
|
||||
|
||||
/** - */
|
||||
|
||||
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) =>
|
||||
storage.get = (namespace, key = undefined, fallback = undefined) =>
|
||||
new Promise((res, rej) =>
|
||||
chrome.storage.sync.get([`[${id}]${key}`], (values) =>
|
||||
res(values[`[${id}]${key}`] ?? fallback)
|
||||
)
|
||||
chrome.storage.sync.get([namespace], async (values) => {
|
||||
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 = [], callback = () => {}) => {
|
||||
web.whenReady = (selectors = []) => {
|
||||
return new Promise((res, rej) => {
|
||||
function onLoad() {
|
||||
let isReadyInt;
|
||||
@ -40,7 +63,6 @@ web.whenReady = (selectors = [], callback = () => {}) => {
|
||||
function isReadyTest() {
|
||||
if (selectors.every((selector) => document.querySelector(selector))) {
|
||||
clearInterval(isReadyInt);
|
||||
callback();
|
||||
res(true);
|
||||
}
|
||||
}
|
||||
@ -53,11 +75,12 @@ web.whenReady = (selectors = [], callback = () => {}) => {
|
||||
} else onLoad();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns HTMLElement
|
||||
*/
|
||||
web.loadStyleset = (path) => {
|
||||
document.head.appendChild(
|
||||
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(path)}">`)
|
||||
);
|
||||
return true;
|
||||
};
|
||||
web.createElement = (html) => {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = html.includes('<pre')
|
||||
@ -70,25 +93,14 @@ web.createElement = (html) => {
|
||||
};
|
||||
web.escapeHtml = (str) =>
|
||||
str
|
||||
.replace(/&/g, '&') // (?![^\s]+;)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
// why a tagged template? because it syntax highlights
|
||||
// https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
|
||||
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;
|
||||
};
|
||||
web.html = (html, ...templates) => html.map((str) => str + (templates.shift() ?? '')).join('');
|
||||
|
||||
/**
|
||||
* @param {array} keys
|
||||
@ -119,10 +131,6 @@ web.hotkeyListener = (keys, callback) => {
|
||||
web._hotkeys.push({ keys, callback });
|
||||
};
|
||||
|
||||
/** - */
|
||||
|
||||
export const fmt = {};
|
||||
|
||||
import './dep/prism.js';
|
||||
fmt.Prism = Prism;
|
||||
fmt.Prism.manual = true;
|
||||
@ -188,69 +196,36 @@ fmt.slugger = (heading, slugs = new Set()) => {
|
||||
return slug;
|
||||
};
|
||||
|
||||
/** - */
|
||||
|
||||
export const fs = {};
|
||||
|
||||
/**
|
||||
* @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.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);
|
||||
if (match && match.length) return true;
|
||||
error(`invalid uuid ${str}`);
|
||||
return false;
|
||||
};
|
||||
|
||||
/** - */
|
||||
|
||||
export const 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);
|
||||
if (match && match.length) return true;
|
||||
error(`invalid uuid ${str}`);
|
||||
return false;
|
||||
},
|
||||
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
|
||||
);
|
||||
if (match && match.length) return true;
|
||||
error(`invalid semver ${str}`);
|
||||
return false;
|
||||
},
|
||||
email(str) {
|
||||
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 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;
|
||||
},
|
||||
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
|
||||
);
|
||||
if (match && match.length) return true;
|
||||
error(`invalid semver ${str}`);
|
||||
return false;
|
||||
};
|
||||
regexers.email = (str) => {
|
||||
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 email ${str}`);
|
||||
return false;
|
||||
};
|
||||
regexers.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) => {
|
||||
let conditions = [
|
||||
@ -444,8 +419,33 @@ registry.validate = async (mod, err, check) => {
|
||||
} while (conditions.some((condition) => Array.isArray(condition)));
|
||||
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 = [];
|
||||
if (!registry._errors) registry._errors = [];
|
||||
for (const dir of await fs.getJSON('repo/registry.json')) {
|
||||
@ -466,12 +466,9 @@ registry.get = async (callback = () => {}) => {
|
||||
err('invalid mod.json');
|
||||
}
|
||||
}
|
||||
callback(registry._list);
|
||||
return registry._list;
|
||||
};
|
||||
|
||||
registry.errors = async (callback = () => {}) => {
|
||||
registry.errors = async () => {
|
||||
if (!registry._errors) await registry.get();
|
||||
callback(registry._errors);
|
||||
return registry._errors;
|
||||
};
|
||||
|
@ -7,7 +7,7 @@
|
||||
'use strict';
|
||||
|
||||
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 sheet of mod.css?.client || []) {
|
||||
web.loadStyleset(`repo/${mod._dir}/${sheet}`);
|
||||
|
@ -51,8 +51,6 @@
|
||||
display: flex;
|
||||
}
|
||||
.enhancer--notifications > div > :last-child > div {
|
||||
margin-left: auto;
|
||||
margin-bottom: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -11,7 +11,7 @@ import { env, storage, web, fs } from '../../helpers.js';
|
||||
|
||||
const sidebarSelector =
|
||||
'#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(
|
||||
web.html`<div class="enhancer--sidebarMenuTrigger" role="button" tabindex="0">
|
||||
<div>
|
||||
|
@ -272,7 +272,7 @@ label > span:not([class]) {
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
left: 0.325rem;
|
||||
top: 0.225rem;
|
||||
top: 0.18rem;
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
background: var(--theme--toggle_dot);
|
||||
|
@ -87,16 +87,22 @@ components.card = {
|
||||
},
|
||||
};
|
||||
components.options = {
|
||||
toggle: (id, { key, label, value }) =>
|
||||
web.createElement(web.html`<label
|
||||
async toggle(id, { key, label }) {
|
||||
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}`)}" />
|
||||
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
|
||||
${state ? 'checked' : ''}/>
|
||||
<p><span>${label}</span><span class="library--toggle"></span></p
|
||||
></label>`),
|
||||
select: async (id, { key, label, values }) =>
|
||||
web.createElement(web.html`<label
|
||||
></label>`);
|
||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked));
|
||||
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}`)}"
|
||||
class="library--select_label"
|
||||
>
|
||||
@ -106,37 +112,47 @@ components.options = {
|
||||
<select id="select--${web.escapeHtml(`${id}.${key}`)}">
|
||||
${values.map(
|
||||
(value) =>
|
||||
web.html`<option value="${web.escapeHtml(value)}">${web.escapeHtml(
|
||||
value
|
||||
)}</option>`
|
||||
web.html`<option value="${web.escapeHtml(value)}"
|
||||
${value === state ? 'selected' : ''}>
|
||||
${web.escapeHtml(value)}</option>`
|
||||
)}
|
||||
</select>
|
||||
</p>
|
||||
</label>`),
|
||||
text(id, { key, label, value }) {
|
||||
const opt = web.createElement(web.html`<label
|
||||
</label>`);
|
||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
||||
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}`)}"
|
||||
class="library--text_label"
|
||||
>
|
||||
<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>`);
|
||||
opt.querySelector('textarea').addEventListener('input', (ev) => {
|
||||
ev.target.style.removeProperty('--txt--scroll-height');
|
||||
ev.target.style.setProperty('--txt--scroll-height', ev.target.scrollHeight + 'px');
|
||||
opt.querySelector('textarea').addEventListener('input', (event) => {
|
||||
event.target.style.removeProperty('--txt--scroll-height');
|
||||
event.target.style.setProperty('--txt--scroll-height', event.target.scrollHeight + 'px');
|
||||
});
|
||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
||||
return opt;
|
||||
},
|
||||
number: (id, { key, label, value }) =>
|
||||
web.createElement(web.html`<label
|
||||
async number(id, { key, label }) {
|
||||
const state = await storage.get(id, key),
|
||||
opt = web.createElement(web.html`<label
|
||||
for="number--${web.escapeHtml(`${id}.${key}`)}"
|
||||
class="library--number_label"
|
||||
>
|
||||
<p>${web.escapeHtml(label)}</p>
|
||||
<input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" />
|
||||
</label>`),
|
||||
<input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" value="${state}"/>
|
||||
</label>`);
|
||||
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
|
||||
return opt;
|
||||
},
|
||||
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}`)}"
|
||||
class="library--file_label"
|
||||
>
|
||||
@ -145,18 +161,33 @@ components.options = {
|
||||
id="file--${web.escapeHtml(`${id}.${key}`)}"
|
||||
${web.escapeHtml(
|
||||
extensions && extensions.length
|
||||
? ` accept="${web.escapeHtml(extensions.join(','))}"`
|
||||
? ` accept=${web.escapeHtml(extensions.join(','))}`
|
||||
: ''
|
||||
)}
|
||||
/>
|
||||
<p>${web.escapeHtml(label)}</p>
|
||||
<p class="library--file">
|
||||
<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>
|
||||
</label>`);
|
||||
opt.querySelector('input').addEventListener('change', (ev) => {
|
||||
opt.querySelector('.library--file_path').innerText = ev.target.files[0].name;
|
||||
opt.addEventListener('change', (event) => {
|
||||
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;
|
||||
},
|
||||
@ -278,7 +309,7 @@ const views = {
|
||||
}, 50);
|
||||
document
|
||||
.querySelectorAll('img')
|
||||
.forEach((img) => (img.onerror = (ev) => ev.target.remove()));
|
||||
.forEach((img) => (img.onerror = (event) => event.target.remove()));
|
||||
document
|
||||
.querySelectorAll('a[href^="?"]')
|
||||
.forEach((a) => a.addEventListener('click', this._router));
|
||||
@ -310,12 +341,12 @@ const views = {
|
||||
views._router = views._router.bind(views);
|
||||
views._navigator = views._navigator.bind(views);
|
||||
views._load();
|
||||
window.addEventListener('popstate', (ev) => {
|
||||
if (ev.state) views._load();
|
||||
window.addEventListener('popstate', (event) => {
|
||||
if (event.state) views._load();
|
||||
});
|
||||
|
||||
const notifications = {
|
||||
_generate({ heading, message = '', type = 'information' }, callback = () => {}) {
|
||||
_generate({ heading, message = '', type = 'information' }, onDismiss = () => {}) {
|
||||
let svg = '',
|
||||
className = 'notification';
|
||||
switch (type) {
|
||||
@ -347,7 +378,7 @@ const notifications = {
|
||||
$notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
}rem`;
|
||||
setTimeout(() => $notif.remove(), 400);
|
||||
callback();
|
||||
onDismiss();
|
||||
});
|
||||
setTimeout(() => {
|
||||
$notif.style.opacity = 1;
|
||||
@ -355,7 +386,7 @@ const notifications = {
|
||||
return $notif;
|
||||
},
|
||||
async load() {
|
||||
let notifications = {
|
||||
const notifications = {
|
||||
list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'),
|
||||
dismissed: await storage.get(_id, 'notifications', []),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user