From 7ecfd0fa80af0e8c7dea01062c2a08f1f07c2fd7 Mon Sep 17 00:00:00 2001 From: tophf Date: Sat, 19 Aug 2017 20:41:50 +0300 Subject: [PATCH] add support for freestyler.ws * handle style action buttons on the site pages * popup find-styles links with icons * auto-updater --- _locales/en/messages.json | 4 + background/background.js | 25 ++- background/update.js | 39 ++++- content/freestyler.js | 212 ++++++++++++++++++++++++++ content/{install.js => userstyles.js} | 0 images/world-freestyler.png | Bin 0 -> 2876 bytes images/world-userstyles.png | Bin 0 -> 3586 bytes js/prefs.js | 15 +- manage/manage.js | 39 ++++- manifest.json | 8 +- popup.html | 12 ++ popup/popup.css | 46 ++++++ popup/popup.js | 63 ++++++-- 13 files changed, 436 insertions(+), 27 deletions(-) create mode 100644 content/freestyler.js rename content/{install.js => userstyles.js} (100%) create mode 100644 images/world-freestyler.png create mode 100644 images/world-userstyles.png diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 44823873..96d693f1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -329,6 +329,10 @@ "message": "Find more styles for this site", "description": "Text for a link that gets a list of styles for the current site" }, + "findStylesSiteSelectorTooltip": { + "message": "Choose where to search for the styles.\n'Find more styles' will also use the choice.", + "description": "Short text for a link that gets a list of styles for the current site" + }, "helpAlt": { "message": "Help", "description": "Alternate text for help buttons" diff --git a/background/background.js b/background/background.js index 60942b61..ae7eba3b 100644 --- a/background/background.js +++ b/background/background.js @@ -1,4 +1,4 @@ -/* global dbExec, getStyles, saveStyle */ +/* global dbExec, getStyles, saveStyle, deleteStyle, calcStyleDigest */ /* global handleCssTransitionBug */ /* global usercssHelper openEditor */ /* global styleViaAPI */ @@ -339,6 +339,29 @@ function onRuntimeMessage(request, sender, sendResponse) { case 'openEditor': openEditor(request.id); return; + + case 'openManager': + openURL({url: 'manage.html'}).then(function onReady(tab) { + if (tab && tab.status === 'complete') { + chrome.tabs.sendMessage(tab.id, { + method: 'highlightStyle', + id: request.styleId, + }); + } else if (tab) { + setTimeout(() => chrome.tabs.get(tab.id, onReady), 100); + } + }); + return; + + case 'deleteStyle': + deleteStyle(request); + return; + + case 'calcStyleDigest': + getStyles({id: request.id}) + .then(([style]) => style && calcStyleDigest(style)) + .then(sendResponse); + return KEEP_CHANNEL_OPEN; } } diff --git a/background/update.js b/background/update.js index 36adcde7..bea6f693 100644 --- a/background/update.js +++ b/background/update.js @@ -56,7 +56,9 @@ var updater = { 'ignoreDigest' option is set on the second manual individual update check on the manage page. */ - const maybeUpdate = style.usercssData ? maybeUpdateUsercss : maybeUpdateUSO; + const maybeUpdate = style.usercssData ? maybeUpdateUsercss : + style.freestylerData ? maybeUpdateFWS : + maybeUpdateUSO; return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style)) .then(checkIfEdited) .then(maybeUpdate) @@ -114,6 +116,29 @@ var updater = { }); } + function maybeUpdateFWS() { + return updater.invokeFreestylerAPI('check_updates', { + json: [style.freestylerData] + }).then(data => ( + !data || !data[0] ? Promise.reject(updater.ERROR_JSON) : + !data[0].isUpdated ? Promise.reject(updater.SAME_MD5) : + true + )).then(() => updater.invokeFreestylerAPI('get_updates', { + json: [style.freestylerData] + })).then(data => { + data = data && data[0] || {}; + const newStyle = tryJSONparse(data.newJson); + if (newStyle) { + newStyle.freestylerData = { + id: data.id, + hash: data.newHash, + params: data.newParams, + }; + } + return newStyle; + }); + } + function maybeValidate(json) { if (json.usercssData) { // usercss is already validated while building @@ -196,6 +221,18 @@ var updater = { }); } })(), + + invokeFreestylerAPI(method, params) { + return new Promise(resolve => { + const encodeParam = k => + encodeURIComponent(k === 'json' ? JSON.stringify(params[k]) : params[k]); + const query = Object.keys(params) + .map(k => k + '=' + encodeParam(k)) + .join('&'); + download(`https://freestyler.ws/api/v2/${method}.php?${query}`) + .then(text => resolve(params.json ? tryJSONparse(text) : text)); + }); + } }; updater.schedule(); diff --git a/content/freestyler.js b/content/freestyler.js new file mode 100644 index 00000000..c118c38b --- /dev/null +++ b/content/freestyler.js @@ -0,0 +1,212 @@ +'use strict'; + +// IIFE simplifies garbage-collection on orphaning or non-style pages +(() => { + if (!getPageData().id) { + return; + } + getInstalledStyle().then(setPageDataAndNotify); + notifyPage(chrome.runtime.id); + + const pageListeners = { + install: onUpdate, + update: onUpdate, + applyParams: onUpdate, + uninstall: onUninstall, + stylesManager: onStylesManager, + [chrome.runtime.id]: orphanCheck, + }; + + for (const name of Object.keys(pageListeners)) { + window.addEventListener(name, pageListeners[name]); + } + + + function onUpdate(event) { + const pageData = getPageData(); + let installedStyle; + getInstalledStyle() + .then(checkIfEdited) + .then(makeSiteRequest) + .then(maybeSaveStyle) + .then(setPageDataAndNotify) + .catch(() => notifyPage( + event.type === 'install' ? 'installFailed' : + event.type === 'update' ? 'updateFailed' : + event.type === 'applyParams' ? 'applyFailed' : '' + )); + + function checkIfEdited(style) { + return style && invokeBG('calcStyleDigest', {id: style.id}) + .then(digest => { + if (digest === style.originalDigest || + confirm(chrome.i18n.getMessage('updateCheckManualUpdateForce'))) { + return style; + } else { + setPageDataAndNotify(style); + return Promise.reject(); + } + }); + } + + function makeSiteRequest(style) { + installedStyle = style; + return invokeFreestylerAPI('get_styles_json', { + json: [Object.assign( + pickProps(pageData, [ + 'id', + 'params' + ]), installedStyle && { + 'installed_params': pickProps(installedStyle.freestylerData, [ + 'params', + 'hash', + ]), + 'installed_hash': installedStyle.freestylerData.hash, + } + )] + }); + } + + function maybeSaveStyle(data) { + data = data && data[0] || {}; + const style = tryJSONparse(data.json); + if (!styleJSONseemsValid(style)) { + return Promise.reject(); + } + return invokeBG('saveStyle', { + reason: 'update', + url: getStyleUrl(), + id: installedStyle && installedStyle.id, + name: !installedStyle && style.name, + sections: style.sections, + freestylerData: { + id: data.id, + hash: data.jsonHash, + params: pageData.params, + }, + // use a dummmy URL to make this style updatable + updateUrl: location.origin, + }); + } + } + + + function onUninstall() { + getInstalledStyle().then(style => { + if (style && confirm(chrome.i18n.getMessage('deleteStyleConfirm'))) { + invokeBG('deleteStyle', style); + style = null; + } + setPageDataAndNotify(style); + }); + } + + + function onStylesManager() { + getInstalledStyle().then(style => { + invokeBG('openManager', { + styleId: (style || {}).id, + }); + }); + } + + + function getInstalledStyle() { + return invokeBG('getStyles', { + url: getStyleUrl(), + }).then(styles => styles[0]); + } + + + function styleJSONseemsValid(style) { + return ( + style && + style.name && typeof style.name === 'string' && + style.sections && typeof style.sections.splice === 'function' && + typeof (style.sections[0] || {}).code === 'string' + ); + } + + + function setPageDataAndNotify(style) { + $id('plugin-data-container').value = JSON.stringify(style ? [style.freestylerData] : []); + $id('plugin-info-container').value = JSON.stringify({version: '2.4.1.3'}); + notifyPage('pluginReady'); + } + + + function invokeFreestylerAPI(method, params) { + return new Promise(resolve => { + const encodeParam = k => + encodeURIComponent(k === 'json' ? JSON.stringify(params[k]) : params[k]); + const query = Object.keys(params) + .map(k => k + '=' + encodeParam(k)) + .join('&'); + invokeBG('download', { + url: `https://${location.hostname}/api/v2/${method}.php?${query}`, + }).then(text => { + resolve(params.json ? tryJSONparse(text) : text); + }); + }); + } + + + function notifyPage(message) { + if (message) { + window.dispatchEvent(new CustomEvent(message)); + } + } + + + function getPageData() { + // the data may change during page lifetime + return tryJSONparse($id('site-data-container').value) || ''; + } + + + function getStyleUrl() { + return location.href.replace(/#.*/, ''); + } + + + function $id(id) { + return document.getElementById(id) || ''; + } + + + function tryJSONparse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (e) {} + } + + + function pickProps(obj, names) { + const result = {}; + for (const name of names) { + result[name] = obj[name]; + } + return result; + } + + + function invokeBG(method, params) { + return new Promise(resolve => { + params.method = method; + chrome.runtime.sendMessage(params, resolve); + }); + } + + + function orphanCheck() { + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + } else { + // we're orphaned due to an extension update + for (const name of Object.keys(pageListeners)) { + window.removeEventListener(name, pageListeners[name]); + } + } + } +})(); diff --git a/content/install.js b/content/userstyles.js similarity index 100% rename from content/install.js rename to content/userstyles.js diff --git a/images/world-freestyler.png b/images/world-freestyler.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7d777f878c023ff0bfd40443e909e511be3e41 GIT binary patch literal 2876 zcmV-C3&Zq@P)Px#8FWQhbW?9;ba!ELWdK2BZ(?O2No`?gWm08fWO;GP zWjp`?0EbXaR7Ff_as1xv{>?$%&Y#)I{VUG{@7>eAcyCadND|NrP8hX2k$=pKguz5)Ne0O%iw|KOGX^z88W{e{Ty|I})1 zzwF-b`cAj$|LeOsvFFF*_ny)5|MJ!U#2~WT^zn|m>|dekI*|9i*JcXy_y7O^4|Gya zQvmvXXz#T>9JX7r?_ja9=t^?l000UTNklU`EKmPKOFhJAJYpR@KGsn8 za^SpzzIW5J6-jBQS5DwRq2lG^h9=;p4Uyyc*vplRuqf4hf2 zE+)Usvqw_?n(lb4dxVbdD2@%&8i@N~hRNi&dF&)HgzHIR-6eF&IAN>`TBV)F>q&&V z>&F|U*%E2qB{YI@AZ2td6GznQ(t!VbuLhNMV||5C9&J{}Vn9cEiKcK&Neo39KLP6< zj-%cVnf#k(t{9su#WeM$?lnmLEcmRy`~-gXxqi-|z4BkaW?0jMc7I zD?L6*ZZ>64Hol%K;_KoI&bm5Gxp(r?{SWTbzT&KppsukP*k63fSsy_aJ-_$EnJJGi zMAjEr4fIUC*rv!jeG|$L3wu~7|6)PY>nesymwdSX2Md~Uxmm_-wI3*7i|<>|_#$k* zmSkV-+v%G1;`vsEcAe_;S15buoHY?BqNQhUr6yO$a`o3JHsV<8%ruQA{x!{xYv1ay{3oPs1PNSh z{AX=~GeRj0O5CTo3^zI+`b}2*C5XMtlV_xK5S~p#jN%KC+Pb_J>)K<{*DXiefw{Ld ze{={KAhMU%(0q5Zpf!pe$)4)YN@vKFxxaTMh-Hpi9Zq*g8!#oL0U z%1t1YBmg_GbUtCSDmW)?z;G7S#Mt_Z7Zk2hgRrX!pJhQxUvU-zG8`wO1P5_tAw6hx zTktQhFBwq7ce!^Cf>)$x!55K3}?spV`D zUB#jRjq6g;X%529_9AP4ymBvL#p!kmqQLwNnXH?uBt(UHI6Pi&6gI8Ry~XqesxPEi zP0|jriv>unXg8CO#$lgi@v1o;E>KjvkXE|;q|-_D6WdBcimfCmPM=9Q~6s=hk~>?R~dmok`=?# z<(&;UZPwUSY88f-1^X9h1`;Iu>E^A95Sh{}QzP6+EVVH~81SDehpp5>SC*zCekEtK z_TDdH;#uZZ)x?Mh)^9fxE{(6a+d1w*hPEhP4m6Un_zvKEO$f6p|FH@4>C26oQWb6x zEChGi1w#dfcuv=B1Syl4Rip+|Vd55^F0nbIM}u3Q!`@j3Ei1G>&SFnPcWZBg^=28M z5*LXTqbMuGt!-)s*Dr9tu2d_Z`y(Lmv)ioP1M-_7;l^B}8VZd;##7@>0?Sr*N!*1=7NRgj7l<1I-^S|0O zqzGOH$ig$FpZd@2$%vggi8gM*BraIaHr(h^qA>1Rq4wUWt@ay~r~lx~QN-9qb7 zc12H}&L7xS6=grtIlGIvi>{xt9!or?s2REMr+J)mJQZ1kpsa^~HxnAo@v<{+;l54Lrj!53 zS21j6N~#v+ljqXATk`+`EK;1lzK8;gI}@;PQw5$ya}_C|NQuPo!6auD8Mo0K*lO8z z#kP}NcBHsc>k*Z@W>s=hypKOF5)Y9!UiDW)t?ldzVP6-=iV~Bi^clCBV8_HN{dn(D z@BKt>Q1ruwp>PgX`gE4c)uonP2f-zJtcjE{50I3wyGXkri`7u$mp!&*XQ9+B literal 0 HcmV?d00001 diff --git a/images/world-userstyles.png b/images/world-userstyles.png new file mode 100644 index 0000000000000000000000000000000000000000..ee123eefdabdd0a3a8144e9b77a3247e05d5f4e7 GIT binary patch literal 3586 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 IM6N<$f+HFY)Bpeg literal 0 HcmV?d00001 diff --git a/js/prefs.js b/js/prefs.js index d90cea8e..a1f1bb2f 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -16,6 +16,7 @@ var prefs = new function Prefs() { 'popup.enabledFirst': true, // display enabled styles before disabled styles 'popup.stylesFirst': true, // display enabled styles before disabled styles 'popup.borders': false, // add white borders on the sides + 'popup.findStylesSource': 'userstyles', 'manage.onlyEnabled': false, // display only enabled styles 'manage.onlyLocal': false, // display only styles created locally @@ -104,8 +105,12 @@ var prefs = new function Prefs() { return deepCopy(values); }, - set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) { - const oldValue = values[key]; + set(key, value, { + broadcast = true, + sync = true, + onlyIfChanged = false, + fromBroadcast, + } = {}) { switch (typeof defaults[key]) { case typeof value: break; @@ -119,9 +124,13 @@ var prefs = new function Prefs() { value = value === true || value === 'true'; break; } + const oldValue = values[key]; + const hasChanged = !equal(value, oldValue); + if (!hasChanged && onlyIfChanged) { + return; + } values[key] = value; defineReadonlyProperty(this.readOnlyValues, key, value); - const hasChanged = !equal(value, oldValue); if (!fromBroadcast) { if (BG && BG !== window) { BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); diff --git a/manage/manage.js b/manage/manage.js index 0b1f41ae..34717854 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -48,6 +48,11 @@ function onRuntimeMessage(msg) { case 'styleDeleted': handleDelete(msg.id); break; + case 'highlightStyle': + if (!highlightEntry(msg) && showStyles.inProgress) { + once(window, 'showStyles:done', () => highlightEntry(msg)); + } + break; } } @@ -102,6 +107,7 @@ function initGlobalEvents() { function showStyles(styles = []) { + showStyles.inProgress = true; const sorted = styles .map(style => ({name: style.name.toLocaleLowerCase(), style})) .sort((a, b) => (a.name < b.name ? -1 : a.name === b.name ? 0 : 1)); @@ -137,13 +143,11 @@ function showStyles(styles = []) { debounce(handleEvent.loadFavicons, 16); } if (sessionStorage.justEditedStyleId) { - const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId); + highlightEntry({id: sessionStorage.justEditedStyleId}); delete sessionStorage.justEditedStyleId; - if (entry) { - animateElement(entry); - scrollElementIntoView(entry); - } } + window.dispatchEvent(new CustomEvent('showStyles:done')); + showStyles.inProgress = false; } } @@ -428,8 +432,7 @@ function handleUpdate(style, {reason, method} = {}) { } filterAndAppend({entry}); if (!entry.matches('.hidden') && reason !== 'import') { - animateElement(entry); - scrollElementIntoView(entry); + highlightEntry({entry}); } function handleToggledOrCodeOnly() { @@ -575,6 +578,28 @@ function usePrefsDuringPageLoad() { } +// TODO: move to dom.js and use where applicable +function once(element, event, callback, options) { + element.addEventListener(event, onEvent, options); + function onEvent(...args) { + element.removeEventListener(event, onEvent); + callback.call(element, ...args); + } +} + + +function highlightEntry({ + id, + entry = $(ENTRY_ID_PREFIX + id), +}) { + if (entry) { + animateElement(entry); + scrollElementIntoView(entry); + return true; + } +} + + // TODO: remove when these bugs are fixed in FF function dieOnNullBackground() { if (!FIREFOX || BG) { diff --git a/manifest.json b/manifest.json index cfd6d428..c1646981 100644 --- a/manifest.json +++ b/manifest.json @@ -54,7 +54,13 @@ "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "run_at": "document_start", "all_frames": false, - "js": ["content/install.js"] + "js": ["content/userstyles.js"] + }, + { + "matches": ["http://freestyler.ws/*", "https://freestyler.ws/*"], + "run_at": "document_end", + "all_frames": false, + "js": ["content/freestyler.js"] }, { "matches": [""], diff --git a/popup.html b/popup.html index fb0899eb..906ddec3 100644 --- a/popup.html +++ b/popup.html @@ -116,6 +116,18 @@
+ + + + + +
diff --git a/popup/popup.css b/popup/popup.css index 21482997..4e00d99a 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -239,6 +239,48 @@ body.blocked .actions > .left-gutter { display: none; } +#find-styles svg { + vertical-align: sub; + pointer-events: auto; + cursor: pointer; +} + +#find-styles-sources { + display: flex; + transition: opacity .5s .1s cubic-bezier(.25,.02,1,.21); + flex-direction: column; + position: absolute; + background-color: white; + box-shadow: 3px 3px 10px rgba(0, 0, 0, .5); + border: 1px solid darkgray; + bottom: .75em; + right: .75em; + padding: .5em 0; +} + +#find-styles-sources > * { + padding: .5em 1em; +} + +#find-styles-sources > *:hover { + background-color: lightgray; +} + +#find-styles img { + max-height: 18px; + -webkit-filter: grayscale(1) brightness(1.15); + filter: grayscale(1) brightness(1.15); + transition: -webkit-filter .5s; + transition: filter .5s; + margin-right: 4px; + vertical-align: middle; +} + +#find-styles a:hover img { + -webkit-filter: none; + filter: none; +} + /* Never shown, but can be enabled with a style */ .enable, @@ -429,6 +471,10 @@ body.blocked .actions > .left-gutter { margin: 0; } +.hidden { + display: none !important; +} + @keyframes lights-off { from { background-color: transparent; diff --git a/popup/popup.js b/popup/popup.js index eff7f6db..f9dfff38 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -18,9 +18,7 @@ getActiveTab().then(tab => tabURL = URLS.supported(url) ? url : ''; Promise.all([ tabURL && getStylesSafe({matchUrl: tabURL}), - onDOMready().then(() => { - initPopup(tabURL); - }), + onDOMready().then(initPopup), ]).then(([styles]) => { showStyles(styles); }); @@ -74,7 +72,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) { } -function initPopup(url) { +function initPopup() { installed = $('#installed'); setPopupWidth(); @@ -106,18 +104,55 @@ function initPopup(url) { installed); } - $('#find-styles-link').onclick = handleEvent.openURLandHide; - $('#find-styles-link').href += - url.startsWith(location.protocol) ? - '?search_terms=Stylus' : - 'all/' + encodeURIComponent(url.startsWith('file:') ? 'file:' : url); + $$('[data-toggle-on-click]').forEach(el => { + // dataset on SVG doesn't work in Chrome 49-??, works in 57+ + const target = $(el.getAttribute('data-toggle-on-click')); + el.onclick = () => target.classList.toggle('hidden'); + }); - if (!url) { + if (!tabURL) { document.body.classList.add('blocked'); document.body.insertBefore(template.unavailableInfo, document.body.firstChild); return; } + const findStylesElement = $('#find-styles-link'); + findStylesElement.onclick = handleEvent.openURLandHide; + function openAndRememberSource(event) { + prefs.set('popup.findStylesSource', this.dataset.prefValue, {onlyIfChanged: true}); + handleEvent.openURLandHide.call(this, event); + } + $$('#find-styles-sources a').forEach(a => (a.onclick = openAndRememberSource)); + // touch devices don't have onHover events so the element we'll be toggled via clicking (touching) + if ('ontouchstart' in document.body) { + const menu = $('#find-styles-sources'); + const menuData = menu.dataset; + const closeOnOutsideTouch = event => { + if (!menu.contains(event.target)) { + delete menuData.show; + window.removeEventListener('touchstart', closeOnOutsideTouch); + } + }; + findStylesElement.onclick = event => { + if (menuData.show) { + closeOnOutsideTouch(event); + } else { + menuData.show = true; + window.addEventListener('touchstart', closeOnOutsideTouch); + event.preventDefault(); + } + }; + } + // freestyler: strip 'www.' when hostname has 3+ parts + $('#find-styles a[href*="freestyler"]').href += + encodeURIComponent(new URL(tabURL).hostname.replace(/^www\.(?=.+?\.)/, '')); + // userstyles: send just 'file:' for file:// links + $('#find-styles a[href*="userstyles"]').href += + encodeURIComponent(tabURL.startsWith('file:') ? 'file:' : tabURL); + // set the default link to the last used one + $$(`#find-styles a[data-pref-value="${(prefs.get('popup.findStylesSource') || 'userstyles')}"]`) + .forEach(a => (findStylesElement.href = a.href)); + getActiveTab().then(function ping(tab, retryCountdown = 10) { chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => { if (pong) { @@ -150,10 +185,10 @@ function initPopup(url) { // For this URL const urlLink = template.writeStyle.cloneNode(true); Object.assign(urlLink, { - href: 'edit.html?url-prefix=' + encodeURIComponent(url), - title: `url-prefix("${url}")`, + href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL), + title: `url-prefix("${tabURL}")`, textContent: prefs.get('popup.breadcrumbs.usePath') - ? new URL(url).pathname.slice(1) + ? new URL(tabURL).pathname.slice(1) // this URL : t('writeStyleForURL').replace(/ /g, '\u00a0'), onclick: handleEvent.openLink, @@ -167,7 +202,7 @@ function initPopup(url) { matchTargets.appendChild(urlLink); // For domain - const domains = BG.getDomains(url); + const domains = BG.getDomains(tabURL); for (const domain of domains) { const numParts = domain.length - domain.replace(/\./g, '').length + 1; // Don't include TLD