diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 95c6ce81..793003bb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -393,6 +393,10 @@ "message": "Install", "description": "Label for install button" }, + "installButtonInstalled": { + "message": "Installed", + "description": "Text displayed when the style is successfully installed" + }, "installButtonUpdate": { "message": "Update", "description": "Label for update button" diff --git a/content/install-user-css.js b/content/install-user-css.js index 29d7bc47..dc542c5c 100644 --- a/content/install-user-css.js +++ b/content/install-user-css.js @@ -68,7 +68,10 @@ function createSourceLoader() { } function initUsercssInstall() { - const pendingSource = createSourceLoader().load(); + const sourceLoader = createSourceLoader(); + const pendingSource = sourceLoader.load(); + let watcher; + chrome.runtime.onConnect.addListener(port => { // FIXME: is this the correct way to reject a connection? // https://developer.chrome.com/extensions/messaging#connect @@ -83,6 +86,19 @@ function initUsercssInstall() { port.postMessage({method: msg.method + 'Response', error: err.message || String(err)}) ); break; + + case 'liveReloadStart': + if (!watcher) { + watcher = sourceLoader.watch(sourceCode => { + port.postMessage({method: 'sourceCodeChanged', sourceCode}); + }); + } + watcher.start(); + break; + + case 'liveReloadStop': + watcher.stop(); + break; } }); }); diff --git a/install-usercss.html b/install-usercss.html index b04f9be0..b2f6b78a 100644 --- a/install-usercss.html +++ b/install-usercss.html @@ -54,6 +54,10 @@ +

diff --git a/install-usercss/install-usercss.css b/install-usercss/install-usercss.css index b9bf9307..5e373b30 100644 --- a/install-usercss/install-usercss.css +++ b/install-usercss/install-usercss.css @@ -85,6 +85,18 @@ h1 small { overflow: hidden; overflow-wrap: break-word; min-width: 0; + + display: flex; + flex-direction: column; +} + +.main > :first-child { + flex: 0 0 auto; +} + +.main > :last-child { + flex: 1 1 auto; + min-height: 0; } .main .code, diff --git a/install-usercss/install-usercss.js b/install-usercss/install-usercss.js index fd2ef383..d4597092 100644 --- a/install-usercss/install-usercss.js +++ b/install-usercss/install-usercss.js @@ -4,6 +4,8 @@ (function () { const params = getParams(); + let liveReload = false; + let installed = false; const port = chrome.tabs.connect( Number(params.tabId), @@ -19,11 +21,104 @@ initSourceCode(msg.sourceCode); } break; + case 'sourceCodeChanged': + if (msg.error) { + alert(msg.error); + } else { + liveReloadUpdate(msg.sourceCode); + } + break; } }); port.onDisconnect.addListener(closeCurrentTab); const cm = CodeMirror.fromTextArea($('.code textarea'), {readOnly: true}); + let liveReloadPending = Promise.resolve(); + + function liveReloadUpdate(sourceCode) { + liveReloadPending = liveReloadPending.then(() => { + const scrollInfo = cm.getScrollInfo(); + const cursor = cm.getCursor(); + cm.setValue(sourceCode); + cm.setCursor(cursor); + cm.scrollTo(scrollInfo.left, scrollInfo.top); + + return runtimeSend({ + method: 'saveUsercss', + reason: 'update', + sourceCode + }).then(updateMeta).catch(showError); + }); + } + + function updateMeta(style, dup) { + $$('.main .warning').forEach(e => e.remove()); + + const data = style.usercssData; + const dupData = dup && dup.usercssData; + const versionTest = dup && semverCompare(data.version, dupData.version); + + // update editor + cm.setPreprocessor(data.preprocessor); + + // update metas + document.title = `${installButtonLabel()} ${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; + + $('.meta-author').parentNode.style.display = data.author ? '' : 'none'; + $('.meta-author').textContent = data.author; + + $('.meta-license').parentNode.style.display = data.license ? '' : 'none'; + $('.meta-license').textContent = data.license; + + $('.applies-to').textContent = ''; + getAppliesTo(style).forEach(pattern => + $('.applies-to').appendChild($element({tag: 'li', textContent: pattern})) + ); + + $('.external-link').textContent = ''; + const externalLink = makeExternalLink(); + if (externalLink) { + $('.external-link').appendChild(externalLink); + } + + function makeExternalLink() { + 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( + installed ? 'installButtonInstalled' : + !dup ? 'installButton' : + versionTest > 0 ? 'installButtonUpdate' : 'installButtonReinstall' + ); + } + } + + function showError(err) { + $$('.main .warning').forEach(e => e.remove()); + const main = $('.main'); + main.insertBefore(buildWarning(err), main.firstChild); + } function runtimeSend(request) { return new Promise((resolve, reject) => { @@ -41,15 +136,25 @@ }); return runtimeSend(request) .then(result => { + installed = true; + $$('.warning') .forEach(el => el.remove()); - $('.install').textContent = 'Installed'; $('.install').disabled = true; $('.install').classList.add('installed'); $('.set-update-url input[type=checkbox]').disabled = true; $('.set-update-url').title = result.updateUrl ? t('installUpdateFrom', result.updateUrl) : ''; - window.dispatchEvent(new CustomEvent('installed', {detail: result})); + + updateMeta(result); + + if (liveReload) { + port.postMessage({method: 'liveReloadStart'}); + } + $('.live-reload').addEventListener('change', () => { + const method = 'liveReload' + (liveReload ? 'Start' : 'Stop'); + port.postMessage({method}); + }); }) .catch(err => { alert(chrome.i18n.getMessage('styleInstallFailed', String(err))); @@ -81,6 +186,8 @@ const dupData = dup && dup.usercssData; const versionTest = dup && semverCompare(data.version, dupData.version); + updateMeta(style, dup); + // update UI if (versionTest < 0) { $('.actions').parentNode.insertBefore( @@ -120,59 +227,14 @@ } }; - // update editor - cm.setPreprocessor(data.preprocessor); - - // update metas - document.title = `${installButtonLabel()} ${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; + // live reload + const setLiveReload = $('.live-reload input[type=checkbox]'); + if (updateUrl.protocol !== 'file:') { + setLiveReload.parentNode.remove(); } 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})) - ); - - const externalLink = makeExternalLink(); - if (externalLink) { - $('.external-link').appendChild(externalLink); - } - - function makeExternalLink() { - 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'); + setLiveReload.addEventListener('change', () => { + liveReload = setLiveReload.checked; + }); } }