From 18fd15317e20c5eaa281bb30de6452b7295dfe60 Mon Sep 17 00:00:00 2001 From: eight Date: Sun, 24 Sep 2017 11:39:04 +0800 Subject: [PATCH] WIP: install page --- background/background.js | 8 + background/usercss-helper.js | 27 +- content/install-user-css.js | 266 ++---------------- .../install-usercss.css | 5 +- install-usercss/install-usercss.html | 53 ++++ install-usercss/install-usercss.js | 198 +++++++++++++ 6 files changed, 308 insertions(+), 249 deletions(-) rename content/install-user-css.css => install-usercss/install-usercss.css (92%) create mode 100644 install-usercss/install-usercss.html create mode 100644 install-usercss/install-usercss.js diff --git a/background/background.js b/background/background.js index 77d5c705..a9a6cf21 100644 --- a/background/background.js +++ b/background/background.js @@ -326,6 +326,14 @@ function onRuntimeMessage(request, sender, sendResponse) { case 'injectContent': injectContent(sender.tab.id, request).then(sendResponse); 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; } } diff --git a/background/usercss-helper.js b/background/usercss-helper.js index b2877978..acdb663b 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -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}; })(); diff --git a/content/install-user-css.js b/content/install-user-css.js index ee20f79a..29d7bc47 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -1,31 +1,5 @@ -/* global semverCompare makeLink */ - '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) { return new Promise((resolve, reject) => { 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() { let source; @@ -292,28 +68,28 @@ function createSourceLoader() { } function initUsercssInstall() { - pendingResource = runtimeSend({ - method: 'injectContent', - files: [ - '/js/dom.js', - '/js/localization.js', - '/js/usercss.js', - '/vendor/node-semver/semver.js', - '/content/install-user-css.css' - ] - }); + const pendingSource = createSourceLoader().load(); + chrome.runtime.onConnect.addListener(port => { + // FIXME: is this the correct way to reject a connection? + // https://developer.chrome.com/extensions/messaging#connect + console.assert(port.name === 'usercss-install'); - const sourceLoader = createSourceLoader(); - sourceLoader.load() - .then(() => - runtimeSend({ - method: 'buildUsercss', - sourceCode: sourceLoader.source(), - checkDup: true - }) - ) - .then(result => initInstallPage(result, sourceLoader)) - .catch(err => initErrorPage(err, sourceLoader.source())); + port.onMessage.addListener(msg => { + switch (msg.method) { + case 'getSourceCode': + pendingSource.then(sourceCode => + port.postMessage({method: msg.method + 'Response', sourceCode}) + ).catch(err => + port.postMessage({method: msg.method + 'Response', error: err.message || String(err)}) + ); + break; + } + }); + }); + return runtimeSend({ + method: 'openUsercssInstallPage', + updateUrl: location.href + }).catch(alert); } function isUsercss() { diff --git a/content/install-user-css.css b/install-usercss/install-usercss.css similarity index 92% rename from content/install-user-css.css rename to install-usercss/install-usercss.css index e6f41a3f..1922f0ec 100644 --- a/content/install-user-css.css +++ b/install-usercss/install-usercss.css @@ -1,6 +1,7 @@ body { margin: 0; font: 12px arial, sans-serif; + background: white; } * { @@ -21,7 +22,7 @@ body { overflow-wrap: break-word; } -.header :first-child { +.header > :first-child { margin-top: 0; } @@ -71,8 +72,6 @@ h1 small { .code { padding: 2em; - font-family: monospace; - white-space: pre-wrap; } .main { diff --git a/install-usercss/install-usercss.html b/install-usercss/install-usercss.html new file mode 100644 index 00000000..72490773 --- /dev/null +++ b/install-usercss/install-usercss.html @@ -0,0 +1,53 @@ + + + + + + + Loading... + + + + + + + + + + +
+
+
+ + +
+

+ + +

+

+
+

+ +
+
+

+ +
+

+
    +
+ +
+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js new file mode 100644 index 00000000..9c64f29d --- /dev/null +++ b/install-usercss/install-usercss.js @@ -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; + } +})();