Compare commits

...

3 Commits

Author SHA1 Message Date
tophf
7ecfd0fa80 add support for freestyler.ws
* handle style action buttons on the site pages
* popup find-styles links with icons
* auto-updater
2017-11-14 13:18:39 +03:00
tophf
291c3a3ec5 stop current animation first in animateElement() 2017-11-14 13:08:19 +03:00
tophf
83dcc488dd fix activateTab to produce a tab instead of [tab, window] 2017-11-14 13:08:18 +03:00
15 changed files with 443 additions and 29 deletions

View File

@ -329,6 +329,10 @@
"message": "Find more styles for this site", "message": "Find more styles for this site",
"description": "Text for a link that gets a list of styles for the current 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": { "helpAlt": {
"message": "Help", "message": "Help",
"description": "Alternate text for help buttons" "description": "Alternate text for help buttons"

View File

@ -1,4 +1,4 @@
/* global dbExec, getStyles, saveStyle */ /* global dbExec, getStyles, saveStyle, deleteStyle, calcStyleDigest */
/* global handleCssTransitionBug */ /* global handleCssTransitionBug */
/* global usercssHelper openEditor */ /* global usercssHelper openEditor */
/* global styleViaAPI */ /* global styleViaAPI */
@ -339,6 +339,29 @@ function onRuntimeMessage(request, sender, sendResponse) {
case 'openEditor': case 'openEditor':
openEditor(request.id); openEditor(request.id);
return; 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;
} }
} }

View File

@ -56,7 +56,9 @@ var updater = {
'ignoreDigest' option is set on the second manual individual update check on the manage page. '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)) return (ignoreDigest ? Promise.resolve() : calcStyleDigest(style))
.then(checkIfEdited) .then(checkIfEdited)
.then(maybeUpdate) .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) { function maybeValidate(json) {
if (json.usercssData) { if (json.usercssData) {
// usercss is already validated while building // 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(); updater.schedule();

212
content/freestyler.js Normal file
View File

@ -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]);
}
}
}
})();

BIN
images/world-freestyler.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
images/world-userstyles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -119,7 +119,12 @@ function animateElement(
} }
resolve(); resolve();
}); });
if (element.classList.contains(className)) {
element.classList.remove(className);
setTimeout(() => element.classList.add(className));
} else {
element.classList.add(className); element.classList.add(className);
}
}); });
} }

View File

@ -223,7 +223,7 @@ function activateTab(tab) {
new Promise(resolve => { new Promise(resolve => {
chrome.windows.update(tab.windowId, {focused: true}, resolve); chrome.windows.update(tab.windowId, {focused: true}, resolve);
}), }),
]); ]).then(([tab]) => tab);
} }

View File

@ -16,6 +16,7 @@ var prefs = new function Prefs() {
'popup.enabledFirst': true, // display enabled styles before disabled styles 'popup.enabledFirst': true, // display enabled styles before disabled styles
'popup.stylesFirst': 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.borders': false, // add white borders on the sides
'popup.findStylesSource': 'userstyles',
'manage.onlyEnabled': false, // display only enabled styles 'manage.onlyEnabled': false, // display only enabled styles
'manage.onlyLocal': false, // display only styles created locally 'manage.onlyLocal': false, // display only styles created locally
@ -104,8 +105,12 @@ var prefs = new function Prefs() {
return deepCopy(values); return deepCopy(values);
}, },
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) { set(key, value, {
const oldValue = values[key]; broadcast = true,
sync = true,
onlyIfChanged = false,
fromBroadcast,
} = {}) {
switch (typeof defaults[key]) { switch (typeof defaults[key]) {
case typeof value: case typeof value:
break; break;
@ -119,9 +124,13 @@ var prefs = new function Prefs() {
value = value === true || value === 'true'; value = value === true || value === 'true';
break; break;
} }
const oldValue = values[key];
const hasChanged = !equal(value, oldValue);
if (!hasChanged && onlyIfChanged) {
return;
}
values[key] = value; values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value); defineReadonlyProperty(this.readOnlyValues, key, value);
const hasChanged = !equal(value, oldValue);
if (!fromBroadcast) { if (!fromBroadcast) {
if (BG && BG !== window) { if (BG && BG !== window) {
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});

View File

@ -48,6 +48,11 @@ function onRuntimeMessage(msg) {
case 'styleDeleted': case 'styleDeleted':
handleDelete(msg.id); handleDelete(msg.id);
break; break;
case 'highlightStyle':
if (!highlightEntry(msg) && showStyles.inProgress) {
once(window, 'showStyles:done', () => highlightEntry(msg));
}
break;
} }
} }
@ -102,6 +107,7 @@ function initGlobalEvents() {
function showStyles(styles = []) { function showStyles(styles = []) {
showStyles.inProgress = true;
const sorted = styles const sorted = styles
.map(style => ({name: style.name.toLocaleLowerCase(), style})) .map(style => ({name: style.name.toLocaleLowerCase(), style}))
.sort((a, b) => (a.name < b.name ? -1 : a.name === b.name ? 0 : 1)); .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); debounce(handleEvent.loadFavicons, 16);
} }
if (sessionStorage.justEditedStyleId) { if (sessionStorage.justEditedStyleId) {
const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId); highlightEntry({id: sessionStorage.justEditedStyleId});
delete 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}); filterAndAppend({entry});
if (!entry.matches('.hidden') && reason !== 'import') { if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry); highlightEntry({entry});
scrollElementIntoView(entry);
} }
function handleToggledOrCodeOnly() { 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 // TODO: remove when these bugs are fixed in FF
function dieOnNullBackground() { function dieOnNullBackground() {
if (!FIREFOX || BG) { if (!FIREFOX || BG) {

View File

@ -54,7 +54,13 @@
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"], "matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
"run_at": "document_start", "run_at": "document_start",
"all_frames": false, "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": ["<all_urls>"], "matches": ["<all_urls>"],

View File

@ -116,6 +116,18 @@
<div id="find-styles"> <div id="find-styles">
<a id="find-styles-link" href="https://userstyles.org/styles/browse/" <a id="find-styles-link" href="https://userstyles.org/styles/browse/"
i18n-text="findStylesForSite"></a> i18n-text="findStylesForSite"></a>
<span i18n-title="findStylesSiteSelectorTooltip"
data-toggle-on-click="#find-styles-sources">
<svg class="svg-icon info" viewBox="0 0 14 16" i18n-alt="helpAlt">
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
</svg>
</span>
<div id="find-styles-sources" class="hidden">
<a href="https://userstyles.org/styles/browse/all/"
data-pref-value="userstyles"><img src="images/world-userstyles.png"><span>userstyles.org</span></a>
<a href="http://freestyler.ws/search?q="
data-pref-value="freestyler"><img src="images/world-freestyler.png"><span>freestyler.ws</span></a>
</div>
</div> </div>
<div id="write-style"> <div id="write-style">
<span id="write-style-for" i18n-text="writeStyleFor"></span> <span id="write-style-for" i18n-text="writeStyleFor"></span>

View File

@ -239,6 +239,48 @@ body.blocked .actions > .left-gutter {
display: none; 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 */ /* Never shown, but can be enabled with a style */
.enable, .enable,
@ -429,6 +471,10 @@ body.blocked .actions > .left-gutter {
margin: 0; margin: 0;
} }
.hidden {
display: none !important;
}
@keyframes lights-off { @keyframes lights-off {
from { from {
background-color: transparent; background-color: transparent;

View File

@ -18,9 +18,7 @@ getActiveTab().then(tab =>
tabURL = URLS.supported(url) ? url : ''; tabURL = URLS.supported(url) ? url : '';
Promise.all([ Promise.all([
tabURL && getStylesSafe({matchUrl: tabURL}), tabURL && getStylesSafe({matchUrl: tabURL}),
onDOMready().then(() => { onDOMready().then(initPopup),
initPopup(tabURL);
}),
]).then(([styles]) => { ]).then(([styles]) => {
showStyles(styles); showStyles(styles);
}); });
@ -74,7 +72,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
} }
function initPopup(url) { function initPopup() {
installed = $('#installed'); installed = $('#installed');
setPopupWidth(); setPopupWidth();
@ -106,18 +104,55 @@ function initPopup(url) {
installed); installed);
} }
$('#find-styles-link').onclick = handleEvent.openURLandHide; $$('[data-toggle-on-click]').forEach(el => {
$('#find-styles-link').href += // dataset on SVG doesn't work in Chrome 49-??, works in 57+
url.startsWith(location.protocol) ? const target = $(el.getAttribute('data-toggle-on-click'));
'?search_terms=Stylus' : el.onclick = () => target.classList.toggle('hidden');
'all/' + encodeURIComponent(url.startsWith('file:') ? 'file:' : url); });
if (!url) { if (!tabURL) {
document.body.classList.add('blocked'); document.body.classList.add('blocked');
document.body.insertBefore(template.unavailableInfo, document.body.firstChild); document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
return; 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) { getActiveTab().then(function ping(tab, retryCountdown = 10) {
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => { chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => {
if (pong) { if (pong) {
@ -150,10 +185,10 @@ function initPopup(url) {
// For this URL // For this URL
const urlLink = template.writeStyle.cloneNode(true); const urlLink = template.writeStyle.cloneNode(true);
Object.assign(urlLink, { Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url), href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL),
title: `url-prefix("${url}")`, title: `url-prefix("${tabURL}")`,
textContent: prefs.get('popup.breadcrumbs.usePath') textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(url).pathname.slice(1) ? new URL(tabURL).pathname.slice(1)
// this&nbsp;URL // this&nbsp;URL
: t('writeStyleForURL').replace(/ /g, '\u00a0'), : t('writeStyleForURL').replace(/ /g, '\u00a0'),
onclick: handleEvent.openLink, onclick: handleEvent.openLink,
@ -167,7 +202,7 @@ function initPopup(url) {
matchTargets.appendChild(urlLink); matchTargets.appendChild(urlLink);
// For domain // For domain
const domains = BG.getDomains(url); const domains = BG.getDomains(tabURL);
for (const domain of domains) { for (const domain of domains) {
const numParts = domain.length - domain.replace(/\./g, '').length + 1; const numParts = domain.length - domain.replace(/\./g, '').length + 1;
// Don't include TLD // Don't include TLD