WIP: install page

This commit is contained in:
eight 2017-09-24 11:39:04 +08:00
parent a4e3fba968
commit 18fd15317e
6 changed files with 308 additions and 249 deletions

View File

@ -326,6 +326,14 @@ function onRuntimeMessage(request, sender, sendResponse) {
case 'injectContent': case 'injectContent':
injectContent(sender.tab.id, request).then(sendResponse); injectContent(sender.tab.id, request).then(sendResponse);
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case 'openUsercssInstallPage':
usercssHelper.openInstallPage(sender.tab.id, request).then(sendResponse);
return KEEP_CHANNEL_OPEN;
case 'initUsercssInstallPage':
usercssHelper.initInstallPage(sender.tab.id, request).then(sendResponse);
return KEEP_CHANNEL_OPEN;
} }
} }

View File

@ -96,5 +96,30 @@ var usercssHelper = (function () {
); );
} }
return {build, save, findDup}; function openInstallPage(tabId, request) {
// FIXME: openURL doesn't reuse old page?
const url = '/install-usercss/install-usercss.html' +
'?updateUrl=' + encodeURIComponent(request.updateUrl) +
'&tabId=' + tabId;
const pending = openURL({url})
.then(tab => {
// FIXME: need a reliable way to check if a new tab is created
if (tab.url) {
chrome.runtime.onMessage.addListener(function _({method}, sender, sendResponse) {
if (method !== 'usercssInstallPageReady') {
return;
}
if (sender.tab.id !== tab.id) {
return;
}
chrome.runtime.onMessage.removeListener(_);
wrapReject(Promise.resolve(request)).then(sendResponse);
return true;
});
}
});
return wrapReject(pending);
}
return {build, save, findDup, openInstallPage};
})(); })();

View File

@ -1,31 +1,5 @@
/* global semverCompare makeLink */
'use strict'; 'use strict';
let pendingResource;
function install(style) {
const request = Object.assign(style, {
method: 'saveUsercss',
reason: 'update'
});
return runtimeSend(request)
.then(result => {
$$('.warning')
.forEach(el => el.remove());
$('button.install').textContent = 'Installed';
$('button.install').disabled = true;
$('button.install').classList.add('installed');
$('.set-update-url').disabled = true;
$('.set-update-url-label').title = result.updateUrl ?
t('installUpdateFrom', result.updateUrl) : '';
window.dispatchEvent(new CustomEvent('installed', {detail: result}));
})
.catch(err => {
alert(chrome.i18n.getMessage('styleInstallFailed', String(err)));
});
}
function runtimeSend(request) { function runtimeSend(request) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
@ -35,204 +9,6 @@ function runtimeSend(request) {
}); });
} }
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 initInstallPage({style, dup}, sourceLoader) {
return pendingResource.then(() => {
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
document.body.textContent = '';
document.body.appendChild(buildPage());
if (versionTest < 0) {
$('.actions').parentNode.insertBefore(
$element({className: 'warning', textContent: t('versionInvalidOlder')}),
$('.actions')
);
}
$('.code').textContent = style.sourceCode;
$('button.install').onclick = () => {
if (dup) {
if (confirm(chrome.i18n.getMessage('styleInstallOverwrite', [
data.name, dupData.version, data.version
]))) {
install(style);
}
} else if (confirm(chrome.i18n.getMessage('styleInstall', [data.name]))) {
install(style);
}
};
if (dup && dup.updateUrl === location.href) {
$('.set-update-url').checked = true;
// there is no way to "unset" updateUrl, you can only overwrite it.
$('.set-update-url').disabled = true;
} else if (!dup && location.protocol !== 'file:') {
$('.set-update-url').checked = true;
style.updateUrl = location.href;
}
$('.set-update-url').onchange = e => {
if (e.target.checked) {
style.updateUrl = location.href;
} else {
delete style.updateUrl;
}
};
if (location.protocol === 'file:') {
initLiveReload(sourceLoader);
}
function buildPage() {
return $element({className: 'container', appendChild: [
$element({className: 'header', appendChild: [
$element({className: 'actions', appendChild: [
$element({tag: 'button', className: 'install', textContent: installButtonLabel()}),
$element({
tag: 'label',
title: dup && dup.updateUrl && t('installUpdateFrom', dup.updateUrl) || '',
className: 'set-update-url-label',
appendChild: [
$element({
tag: 'input',
type: 'checkbox',
className: 'set-update-url'
}),
$element({tag: 'span', textContent: t('installUpdateFromLabel')})
]
})
]}),
$element({tag: 'h1', appendChild: [
data.name,
$element({tag: 'small', className: 'meta-version', textContent: data.version})
]}),
$element({tag: 'p', textContent: data.description}),
data.author && $element({tag: 'h3', textContent: t('author')}),
data.author,
data.license && $element({tag: 'h3', textContent: t('license')}),
data.license,
$element({tag: 'h3', textContent: t('appliesLabel')}),
$element({tag: 'ul', appendChild: getAppliesTo(style).map(
pattern => $element({tag: 'li', textContent: pattern})
)}),
externalLink(),
]}),
$element({className: 'main', appendChild: [
$element({className: 'code'})
]})
]});
}
function externalLink() {
const urls = [];
if (data.homepageURL) {
urls.push([data.homepageURL, t('externalHomepage')]);
}
if (data.supportURL) {
urls.push([data.supportURL, t('externalSupport')]);
}
if (urls.length) {
return $element({appendChild: [
$element({tag: 'h3', textContent: t('externalLink')}),
$element({tag: 'ul', appendChild: urls.map(args =>
$element({tag: 'li', appendChild: makeLink(...args)})
)})
]});
}
}
function installButtonLabel() {
return t(!dup ? 'installButton' :
versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall');
}
});
}
function initLiveReload(sourceLoader) {
let installed;
const watcher = sourceLoader.watch(source => {
$('.code').textContent = source;
return runtimeSend({
method: 'saveUsercss',
id: installed.id,
sourceCode: source
}).then(() => {
$$('.main .warning').forEach(e => e.remove());
}).catch(err => {
const oldWarning = $('.main .warning');
const warning = buildWarning(err);
if (oldWarning) {
oldWarning.replaceWith(warning);
} else {
$('.main').insertBefore(warning, $('.main').childNodes[0]);
}
});
});
window.addEventListener('installed', ({detail: style}) => {
installed = style;
if ($('.live-reload-checkbox').checked) {
watcher.start();
}
});
chrome.runtime.onMessage.addListener(request => {
if (request.method === 'styleDeleted') {
if (installed && installed.id === request.id) {
installed = null;
watcher.stop();
$('.live-reload-checkbox').checked = false;
location.reload();
}
}
});
$('.actions').appendChild($element({tag: 'label', className: 'live-reload', appendChild: [
$element({tag: 'input', type: 'checkbox', className: 'live-reload-checkbox'}),
$element({tag: 'span', textContent: t('liveReloadLabel')})
]}));
$('.live-reload-checkbox').onchange = e => {
if (!installed) {
return;
}
if (e.target.checked) {
watcher.start();
} else {
watcher.stop();
}
};
}
function buildWarning(err) {
return $element({className: 'warning', appendChild: [
t('parseUsercssError'),
$element({tag: 'pre', textContent: String(err)})
]});
}
function initErrorPage(err, source) {
return pendingResource.then(() => {
document.body.textContent = '';
[
buildWarning(err),
$element({className: 'code'})
].forEach(e => document.body.appendChild(e));
$('.code').textContent = source;
});
}
function createSourceLoader() { function createSourceLoader() {
let source; let source;
@ -292,28 +68,28 @@ function createSourceLoader() {
} }
function initUsercssInstall() { function initUsercssInstall() {
pendingResource = runtimeSend({ const pendingSource = createSourceLoader().load();
method: 'injectContent', chrome.runtime.onConnect.addListener(port => {
files: [ // FIXME: is this the correct way to reject a connection?
'/js/dom.js', // https://developer.chrome.com/extensions/messaging#connect
'/js/localization.js', console.assert(port.name === 'usercss-install');
'/js/usercss.js',
'/vendor/node-semver/semver.js',
'/content/install-user-css.css'
]
});
const sourceLoader = createSourceLoader(); port.onMessage.addListener(msg => {
sourceLoader.load() switch (msg.method) {
.then(() => case 'getSourceCode':
runtimeSend({ pendingSource.then(sourceCode =>
method: 'buildUsercss', port.postMessage({method: msg.method + 'Response', sourceCode})
sourceCode: sourceLoader.source(), ).catch(err =>
checkDup: true port.postMessage({method: msg.method + 'Response', error: err.message || String(err)})
}) );
) break;
.then(result => initInstallPage(result, sourceLoader)) }
.catch(err => initErrorPage(err, sourceLoader.source())); });
});
return runtimeSend({
method: 'openUsercssInstallPage',
updateUrl: location.href
}).catch(alert);
} }
function isUsercss() { function isUsercss() {

View File

@ -1,6 +1,7 @@
body { body {
margin: 0; margin: 0;
font: 12px arial, sans-serif; font: 12px arial, sans-serif;
background: white;
} }
* { * {
@ -21,7 +22,7 @@ body {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.header :first-child { .header > :first-child {
margin-top: 0; margin-top: 0;
} }
@ -71,8 +72,6 @@ h1 small {
.code { .code {
padding: 2em; padding: 2em;
font-family: monospace;
white-space: pre-wrap;
} }
.main { .main {

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Loading...</title>
<link rel="stylesheet" href="install-usercss.css">
<script src="/js/dom.js"></script>
<script src="/js/localization.js"></script>
<script src="/vendor/node-semver/semver.js"></script>
<script src="/vendor/codemirror/lib/codemirror.js"></script>
<link rel="stylesheet" href="/vendor/codemirror/lib/codemirror.css">
<script src="/vendor/codemirror/mode/css/css.js"></script>
</head>
<body>
<div class="container">
<div class="header">
<div class="actions">
<button class="install" i18n-text="installButton"></button>
<label class="set-update-url">
<input type="checkbox">
<span i18n-text="installUpdateFromLabel"></span>
</label>
</div>
<h1>
<span class="meta-name"></span>
<small class="meta-version"></small>
</h1>
<p class="meta-description"></p>
<div>
<h3 i18n-text="author"></h3>
<span class="meta-author"></span>
</div>
<div>
<h3 i18n-text="license"></h3>
<span class="meta-license"></span>
</div>
<h3 i18n-text="appliesLabel"></h3>
<ul class="applies-to">
</ul>
<div class="external-link"></div>
</div>
<div class="main">
<div class="code">
<textarea></textarea>
</div>
</div>
</div>
<script src="install-usercss.js"></script>
</body>
</html>

View File

@ -0,0 +1,198 @@
/* global CodeMirror semverCompare makeLink */
'use strict';
(function () {
const params = getParams();
const port = chrome.tabs.connect(
Number(params.tabId),
{name: 'usercss-install', frameId: 0}
);
port.postMessage({method: 'getSourceCode'});
port.onMessage.addListener(msg => {
switch (msg.method) {
case 'getSourceCodeResponse':
initSourceCode(msg.sourceCode);
break;
}
});
const cm = CodeMirror.fromTextArea($('.code textarea'), {readOnly: true});
function runtimeSend(request) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
request,
({status, result}) => (status === 'error' ? reject : resolve)(result)
);
});
}
function install(style) {
const request = Object.assign(style, {
method: 'saveUsercss',
reason: 'update'
});
return runtimeSend(request)
.then(result => {
$$('.warning')
.forEach(el => el.remove());
$('button.install').textContent = 'Installed';
$('button.install').disabled = true;
$('button.install').classList.add('installed');
$('.set-update-url').disabled = true;
$('.set-update-url-label').title = result.updateUrl ?
t('installUpdateFrom', result.updateUrl) : '';
window.dispatchEvent(new CustomEvent('installed', {detail: result}));
})
.catch(err => {
alert(chrome.i18n.getMessage('styleInstallFailed', String(err)));
});
}
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
runtimeSend({
method: 'buildUsercss',
sourceCode,
checkDup: true
}).then(init, initError);
}
function initError(err) {
$('.main').insertBefore(buildWarning(err), $('.main').childNodes[0]);
}
function buildWarning(err) {
return $element({className: 'warning', appendChild: [
t('parseUsercssError'),
$element({tag: 'pre', textContent: String(err)})
]});
}
function init({style, dup}) {
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
if (versionTest < 0) {
$('.actions').parentNode.insertBefore(
$element({className: 'warning', textContent: t('versionInvalidOlder')}),
$('.actions')
);
}
$('button.install').onclick = () => {
if (dup) {
if (confirm(chrome.i18n.getMessage('styleInstallOverwrite', [
data.name, dupData.version, data.version
]))) {
install(style);
}
} else if (confirm(chrome.i18n.getMessage('styleInstall', [data.name]))) {
install(style);
}
};
const setUpdate = $('.set-update-url input[type=checkbox]');
const updateUrl = new URL(params.updateUrl);
if (dup && dup.updateUrl === updateUrl.href) {
setUpdate.checked = true;
// there is no way to "unset" updateUrl, you can only overwrite it.
setUpdate.disabled = true;
} else if (!dup && updateUrl.protocol !== 'file:') {
setUpdate.checked = true;
style.updateUrl = updateUrl.href;
}
setUpdate.onchange = e => {
if (e.target.checked) {
style.updateUrl = updateUrl.href;
} else {
delete style.updateUrl;
}
};
// update metas
document.title = data.name;
$('.install').textContent = installButtonLabel();
$('.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').textContent = data.author;
} else {
$('.meta-author').parentNode.remove();
}
if (data.license) {
$('.meta-license').textContent = data.license;
} else {
$('.meta-license').parentNode.remove();
}
getAppliesTo(style).forEach(pattern =>
$('.applies-to').appendChild($element({tag: 'li', textContent: pattern}))
);
$('.external-link').appendChild(externalLink());
function externalLink() {
const urls = [];
if (data.homepageURL) {
urls.push([data.homepageURL, t('externalHomepage')]);
}
if (data.supportURL) {
urls.push([data.supportURL, t('externalSupport')]);
}
if (urls.length) {
return $element({appendChild: [
$element({tag: 'h3', textContent: t('externalLink')}),
$element({tag: 'ul', appendChild: urls.map(args =>
$element({tag: 'li', appendChild: makeLink(...args)})
)})
]});
}
}
function installButtonLabel() {
return t(!dup ? 'installButton' :
versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall');
}
}
function getParams() {
// URL.searchParams needs chrome 51+
const {search} = location;
const result = {};
for (const param of search.slice(1).split('&')) {
let key, value;
if (param.includes('=')) {
[key, value] = param.split('=').map(decodeURIComponent);
} else {
key = decodeURIComponent(param);
value = true;
}
result[key] = value;
}
return result;
}
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;
}
})();