stylus/install-usercss/install-usercss.js
2020-10-09 13:47:57 +03:00

411 lines
13 KiB
JavaScript

/* global CodeMirror semverCompare closeCurrentTab messageBox download
$ $$ $create $createLink t prefs API */
'use strict';
(() => {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
const initialUrl = params.get('updateUrl');
let installed = null;
let installedDup = null;
const liveReload = initLiveReload();
liveReload.ready.then(initSourceCode, error => messageBox.alert(error, 'pre'));
const theme = prefs.get('editor.theme');
const cm = CodeMirror($('.main'), {
readOnly: true,
colorpicker: true,
theme,
});
if (theme !== 'default') {
document.head.appendChild($create('link', {
rel: 'stylesheet',
href: `vendor/codemirror/theme/${theme}.css`
}));
}
window.addEventListener('resize', adjustCodeHeight);
// "History back" in Firefox (for now) restores the old DOM including the messagebox,
// which stays after installing since we don't want to wait for the fadeout animation before resolving.
document.addEventListener('visibilitychange', () => {
if (messageBox.element) messageBox.element.remove();
if (installed) liveReload.onToggled();
});
setTimeout(() => {
if (!installed) {
$('#header').appendChild($create('.lds-spinner',
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}
}, 200);
function updateMeta(style, dup = installedDup) {
installedDup = dup;
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
cm.setPreprocessor(data.preprocessor);
const installButtonLabel = t(
installed ? 'installButtonInstalled' :
!dup ? 'installButton' :
versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall'
);
document.title = `${installButtonLabel} ${data.name}`;
$('.install').textContent = installButtonLabel;
$('.install').classList.add(
installed ? 'installed' :
!dup ? 'install' :
versionTest > 0 ? 'update' :
'reinstall');
$('.set-update-url').title = dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '';
$('.meta-name').textContent = data.name;
$('.meta-version').textContent = data.version;
$('.meta-description').textContent = data.description;
if (data.author) {
$('.meta-author').parentNode.style.display = '';
$('.meta-author').textContent = '';
$('.meta-author').appendChild(makeAuthor(data.author));
} else {
$('.meta-author').parentNode.style.display = 'none';
}
$('.meta-license').parentNode.style.display = data.license ? '' : 'none';
$('.meta-license').textContent = data.license;
$('.applies-to').textContent = '';
getAppliesTo(style).forEach(pattern =>
$('.applies-to').appendChild($create('li', pattern)));
$('.external-link').textContent = '';
const externalLink = makeExternalLink();
if (externalLink) {
$('.external-link').appendChild(externalLink);
}
$('#header').classList.add('meta-init');
$('#header').classList.remove('meta-init-error');
setTimeout(() => $.remove('.lds-spinner'), 1000);
showError('');
requestAnimationFrame(adjustCodeHeight);
function makeAuthor(text) {
const match = text.match(/^(.+?)(?:\s+<(.+?)>)?(?:\s+\((.+?)\))?$/);
if (!match) {
return document.createTextNode(text);
}
const [, name, email, url] = match;
const frag = document.createDocumentFragment();
if (email) {
frag.appendChild($createLink(`mailto:${email}`, name));
} else {
frag.appendChild($create('span', name));
}
if (url) {
frag.appendChild($createLink(url,
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
$create('SVG:path', {
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z'
}))
));
}
return frag;
}
function makeExternalLink() {
const urls = [
data.homepageURL && [data.homepageURL, t('externalHomepage')],
data.supportURL && [data.supportURL, t('externalSupport')],
];
return (data.homepageURL || data.supportURL) && (
$create('div', [
$create('h3', t('externalLink')),
$create('ul', urls.map(args => args &&
$create('li',
$createLink(...args)
)
))
]));
}
}
function showError(err) {
$('.warnings').textContent = '';
if (err) {
$('.warnings').appendChild(buildWarning(err));
}
$('.warnings').classList.toggle('visible', Boolean(err));
$('.container').classList.toggle('has-warnings', Boolean(err));
adjustCodeHeight();
}
function install(style) {
installed = style;
$$.remove('.warning');
$('button.install').disabled = true;
$('button.install').classList.add('installed');
$('#live-reload-install-hint').classList.toggle('hidden', !liveReload.enabled);
$('h2.installed').classList.add('active');
$('.set-update-url input[type=checkbox]').disabled = true;
$('.set-update-url').title = style.updateUrl ?
t('installUpdateFrom', style.updateUrl) : '';
updateMeta(style);
if (!liveReload.enabled && !prefs.get('openEditInWindow')) {
location.href = '/edit.html?id=' + style.id;
} else {
API.openEditor({id: style.id});
if (!liveReload.enabled) {
if (tabId < 0 && history.length > 1) {
history.back();
} else {
closeCurrentTab();
}
}
}
}
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
cm.refresh();
API.buildUsercss({sourceCode, checkDup: true})
.then(init)
.catch(err => {
$('#header').classList.add('meta-init-error');
console.error(err);
showError(err);
});
}
function buildWarning(err) {
const contents = Array.isArray(err) ?
[$create('pre', err.join('\n'))] :
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
if (Number.isInteger(err.index) && typeof contents[0] === 'string') {
const pos = cm.posFromIndex(err.index);
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
contents.push($create('pre', drawLinePointer(pos)));
setTimeout(() => {
cm.scrollIntoView({line: pos.line + 1, ch: pos.ch}, window.innerHeight / 4);
cm.setCursor(pos.line, pos.ch + 1);
cm.focus();
});
}
return $create('.warning', [
t('parseUsercssError'),
'\n',
...contents,
]);
}
function drawLinePointer(pos) {
const SIZE = 60;
const line = cm.getLine(pos.line);
const numTabs = pos.ch + 1 - line.slice(0, pos.ch + 1).replace(/\t/g, '').length;
const pointer = ' '.repeat(pos.ch) + '^';
const start = Math.max(Math.min(pos.ch - SIZE / 2, line.length - SIZE), 0);
const end = Math.min(Math.max(pos.ch + SIZE / 2, SIZE), line.length);
const leftPad = start !== 0 ? '...' : '';
const rightPad = end !== line.length ? '...' : '';
return (
leftPad +
line.slice(start, end).replace(/\t/g, ' '.repeat(cm.options.tabSize)) +
rightPad +
'\n' +
' '.repeat(leftPad.length + numTabs * cm.options.tabSize) +
pointer.slice(start, end)
);
}
function init({style, dup}) {
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
updateMeta(style, dup);
// update UI
if (versionTest < 0) {
$('.actions').parentNode.insertBefore(
$create('.warning', t('versionInvalidOlder')),
$('.actions')
);
}
$('button.install').onclick = () => {
(!dup ?
Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [
data.name,
dupData.version,
data.version,
]))
).then(ok => ok &&
API.installUsercss(style)
.then(install)
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
);
};
// set updateUrl
const checker = $('.set-update-url input[type=checkbox]');
const updateUrl = new URL(style.updateUrl || initialUrl);
if (dup && dup.updateUrl === updateUrl.href) {
checker.checked = true;
// there is no way to "unset" updateUrl, you can only overwrite it.
checker.disabled = true;
} else if (updateUrl.protocol !== 'file:') {
checker.checked = true;
style.updateUrl = updateUrl.href;
}
checker.onchange = () => {
style.updateUrl = checker.checked ? updateUrl.href : null;
};
checker.onchange();
$('.set-update-url p').textContent = updateUrl.href.length < 300 ? updateUrl.href :
updateUrl.href.slice(0, 300) + '...';
if (initialUrl.startsWith('file:')) {
$('.live-reload input').onchange = liveReload.onToggled;
} else {
$('.live-reload').remove();
}
}
function getAppliesTo(style) {
function *_gen() {
for (const section of style.sections) {
for (const type of ['urls', 'urlPrefixes', 'domains', 'regexps']) {
if (section[type]) {
yield *section[type];
}
}
}
}
const result = [..._gen()];
if (!result.length) {
result.push(chrome.i18n.getMessage('appliesToEverything'));
}
return result;
}
function adjustCodeHeight() {
// Chrome-only bug (apparently): it doesn't limit the scroller element height
const scroller = cm.display.scroller;
const prevWindowHeight = adjustCodeHeight.prevWindowHeight;
if (scroller.scrollHeight === scroller.clientHeight ||
prevWindowHeight && window.innerHeight !== prevWindowHeight) {
adjustCodeHeight.prevWindowHeight = window.innerHeight;
cm.setSize(null, $('.main').offsetHeight - $('.warnings').offsetHeight);
}
}
function initLiveReload() {
const DELAY = 500;
let isEnabled = false;
let timer = 0;
/** @type function(?options):Promise<string|null> */
let getData = null;
/** @type Promise */
let sequence = null;
if (tabId < 0) {
getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else {
getData = PortDownloader();
sequence = getData({timer: false});
}
return {
get enabled() {
return isEnabled;
},
ready: sequence,
onToggled(e) {
if (e) isEnabled = e.target.checked;
if (installed || installedDup) {
(isEnabled ? start : stop)();
$('.install').disabled = isEnabled;
Object.assign($('#live-reload-install-hint'), {
hidden: !isEnabled,
textContent: t(`liveReloadInstallHint${tabId >= 0 ? 'FF' : ''}`),
});
}
},
};
function check() {
getData()
.then(update, logError)
.then(() => {
timer = 0;
start();
});
}
function logError(error) {
console.warn(t('liveReloadError', error));
}
function start() {
timer = timer || setTimeout(check, DELAY);
}
function stop() {
clearTimeout(timer);
timer = 0;
}
function update(code) {
if (code == null) return;
sequence = sequence.catch(console.error).then(() => {
const {id} = installed || installedDup;
const scrollInfo = cm.getScrollInfo();
const cursor = cm.getCursor();
cm.setValue(code);
cm.setCursor(cursor);
cm.scrollTo(scrollInfo.left, scrollInfo.top);
return API.installUsercss({id, sourceCode: code})
.then(updateMeta)
.catch(showError);
});
}
function DirectDownloader() {
let oldCode = null;
const passChangedCode = code => {
const isSame = code === oldCode;
oldCode = code;
return isSame ? null : code;
};
return () => download(initialUrl).then(passChangedCode);
}
function PortDownloader() {
const resolvers = new Map();
const port = chrome.tabs.connect(tabId, {name: 'downloadSelf'});
port.onMessage.addListener(({id, code, error}) => {
const r = resolvers.get(id);
resolvers.delete(id);
if (error) {
r.reject(error);
} else {
r.resolve(code);
}
});
port.onDisconnect.addListener(() => {
browser.tabs.get(tabId)
.then(tab => tab.url === initialUrl && location.reload())
.catch(closeCurrentTab);
});
return ({timer = true} = {}) => new Promise((resolve, reject) => {
const id = performance.now();
resolvers.set(id, {resolve, reject});
port.postMessage({id, timer});
});
}
}
})();