Restructure folders
This commit is contained in:
parent
ed83f8f77e
commit
7a9d629ec8
|
@ -1,3 +1,2 @@
|
||||||
beautify/
|
vendor/
|
||||||
codemirror/
|
vendor-overwrites/
|
||||||
csslint/
|
|
||||||
|
|
|
@ -1,371 +1,371 @@
|
||||||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||||
const KEEP_CHANNEL_OPEN = true;
|
const KEEP_CHANNEL_OPEN = true;
|
||||||
|
|
||||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||||
const OPERA = /OPR/.test(navigator.userAgent);
|
const OPERA = /OPR/.test(navigator.userAgent);
|
||||||
|
|
||||||
const URLS = {
|
const URLS = {
|
||||||
ownOrigin: chrome.runtime.getURL(''),
|
ownOrigin: chrome.runtime.getURL(''),
|
||||||
|
|
||||||
optionsUI: [
|
optionsUI: [
|
||||||
chrome.runtime.getURL('options/index.html'),
|
chrome.runtime.getURL('options/index.html'),
|
||||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||||
],
|
],
|
||||||
|
|
||||||
configureCommands:
|
configureCommands:
|
||||||
OPERA ? 'opera://settings/configureCommands'
|
OPERA ? 'opera://settings/configureCommands'
|
||||||
: 'chrome://extensions/configureCommands',
|
: 'chrome://extensions/configureCommands',
|
||||||
|
|
||||||
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
||||||
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
||||||
chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : (
|
chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : (
|
||||||
OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/'
|
OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/'
|
||||||
),
|
),
|
||||||
|
|
||||||
supported: new RegExp(
|
supported: new RegExp(
|
||||||
'^(file|ftps?|http)://|' +
|
'^(file|ftps?|http)://|' +
|
||||||
`^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : (
|
`^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : (
|
||||||
OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)'
|
OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)'
|
||||||
)}|` +
|
)}|` +
|
||||||
'^' + chrome.runtime.getURL('')),
|
'^' + chrome.runtime.getURL('')),
|
||||||
};
|
};
|
||||||
|
|
||||||
let BG = chrome.extension.getBackgroundPage();
|
let BG = chrome.extension.getBackgroundPage();
|
||||||
|
|
||||||
if (!BG || BG != window) {
|
if (!BG || BG != window) {
|
||||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||||
document.documentElement.classList.toggle('opera', OPERA);
|
document.documentElement.classList.toggle('opera', OPERA);
|
||||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||||
if (navigator.userAgent.includes('Chrome/49.')) {
|
if (navigator.userAgent.includes('Chrome/49.')) {
|
||||||
getActiveTab().then(BG.updateIcon);
|
getActiveTab().then(BG.updateIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifyAllTabs(msg) {
|
function notifyAllTabs(msg) {
|
||||||
const originalMessage = msg;
|
const originalMessage = msg;
|
||||||
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
|
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
|
||||||
// apply/popup/manage use only meta for these two methods,
|
// apply/popup/manage use only meta for these two methods,
|
||||||
// editor may need the full code but can fetch it directly,
|
// editor may need the full code but can fetch it directly,
|
||||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
||||||
msg = Object.assign({}, msg, {
|
msg = Object.assign({}, msg, {
|
||||||
style: getStyleWithNoCode(msg.style)
|
style: getStyleWithNoCode(msg.style)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const affectsAll = !msg.affects || msg.affects.all;
|
const affectsAll = !msg.affects || msg.affects.all;
|
||||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
||||||
const affectsIcon = affectsAll || msg.affects.icon;
|
const affectsIcon = affectsAll || msg.affects.icon;
|
||||||
const affectsPopup = affectsAll || msg.affects.popup;
|
const affectsPopup = affectsAll || msg.affects.popup;
|
||||||
const affectsSelf = affectsPopup || msg.prefs;
|
const affectsSelf = affectsPopup || msg.prefs;
|
||||||
if (affectsTabs || affectsIcon) {
|
if (affectsTabs || affectsIcon) {
|
||||||
const notifyTab = tab => {
|
const notifyTab = tab => {
|
||||||
// own pages will be notified via runtime.sendMessage later
|
// own pages will be notified via runtime.sendMessage later
|
||||||
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
||||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
||||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||||
&& (!FIREFOX || tab.width)) {
|
&& (!FIREFOX || tab.width)) {
|
||||||
chrome.tabs.sendMessage(tab.id, msg);
|
chrome.tabs.sendMessage(tab.id, msg);
|
||||||
}
|
}
|
||||||
if (affectsIcon && BG) {
|
if (affectsIcon && BG) {
|
||||||
BG.updateIcon(tab);
|
BG.updateIcon(tab);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// list all tabs including chrome-extension:// which can be ours
|
// list all tabs including chrome-extension:// which can be ours
|
||||||
Promise.all([
|
Promise.all([
|
||||||
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
||||||
getActiveTab(),
|
getActiveTab(),
|
||||||
]).then(([tabs, activeTab]) => {
|
]).then(([tabs, activeTab]) => {
|
||||||
const activeTabId = activeTab && activeTab.id;
|
const activeTabId = activeTab && activeTab.id;
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// notify self: the message no longer is sent to the origin in new Chrome
|
// notify self: the message no longer is sent to the origin in new Chrome
|
||||||
if (typeof onRuntimeMessage != 'undefined') {
|
if (typeof onRuntimeMessage != 'undefined') {
|
||||||
onRuntimeMessage(originalMessage);
|
onRuntimeMessage(originalMessage);
|
||||||
}
|
}
|
||||||
// notify apply.js on own pages
|
// notify apply.js on own pages
|
||||||
if (typeof applyOnMessage != 'undefined') {
|
if (typeof applyOnMessage != 'undefined') {
|
||||||
applyOnMessage(originalMessage);
|
applyOnMessage(originalMessage);
|
||||||
}
|
}
|
||||||
// notify background page and all open popups
|
// notify background page and all open popups
|
||||||
if (affectsSelf) {
|
if (affectsSelf) {
|
||||||
chrome.runtime.sendMessage(msg);
|
chrome.runtime.sendMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function queryTabs(options = {}) {
|
function queryTabs(options = {}) {
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
chrome.tabs.query(options, tabs =>
|
chrome.tabs.query(options, tabs =>
|
||||||
resolve(tabs)));
|
resolve(tabs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getTab(id) {
|
function getTab(id) {
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
chrome.tabs.get(id, tab =>
|
chrome.tabs.get(id, tab =>
|
||||||
!chrome.runtime.lastError && resolve(tab)));
|
!chrome.runtime.lastError && resolve(tab)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getOwnTab() {
|
function getOwnTab() {
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
chrome.tabs.getCurrent(tab => resolve(tab)));
|
chrome.tabs.getCurrent(tab => resolve(tab)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getActiveTab() {
|
function getActiveTab() {
|
||||||
return queryTabs({currentWindow: true, active: true})
|
return queryTabs({currentWindow: true, active: true})
|
||||||
.then(tabs => tabs[0]);
|
.then(tabs => tabs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getActiveTabRealURL() {
|
function getActiveTabRealURL() {
|
||||||
return getActiveTab()
|
return getActiveTab()
|
||||||
.then(getTabRealURL);
|
.then(getTabRealURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getTabRealURL(tab) {
|
function getTabRealURL(tab) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (tab.url != 'chrome://newtab/') {
|
if (tab.url != 'chrome://newtab/') {
|
||||||
resolve(tab.url);
|
resolve(tab.url);
|
||||||
} else {
|
} else {
|
||||||
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
||||||
resolve(frame && frame.url || '');
|
resolve(frame && frame.url || '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// opens a tab or activates the already opened one,
|
// opens a tab or activates the already opened one,
|
||||||
// reuses the New Tab page if it's focused now
|
// reuses the New Tab page if it's focused now
|
||||||
function openURL({url, currentWindow = true}) {
|
function openURL({url, currentWindow = true}) {
|
||||||
if (!url.includes('://')) {
|
if (!url.includes('://')) {
|
||||||
url = chrome.runtime.getURL(url);
|
url = chrome.runtime.getURL(url);
|
||||||
}
|
}
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// [some] chromium forks don't handle their fake branded protocols
|
// [some] chromium forks don't handle their fake branded protocols
|
||||||
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
||||||
// FF doesn't handle moz-extension:// URLs (bug)
|
// FF doesn't handle moz-extension:// URLs (bug)
|
||||||
// API doesn't handle the hash-fragment part
|
// API doesn't handle the hash-fragment part
|
||||||
const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
|
const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
|
||||||
queryTabs({url: urlQuery, currentWindow}).then(tabs => {
|
queryTabs({url: urlQuery, currentWindow}).then(tabs => {
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (tab.url == url) {
|
if (tab.url == url) {
|
||||||
activateTab(tab).then(resolve);
|
activateTab(tab).then(resolve);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getActiveTab().then(tab => {
|
getActiveTab().then(tab => {
|
||||||
if (tab && tab.url == 'chrome://newtab/'
|
if (tab && tab.url == 'chrome://newtab/'
|
||||||
// prevent redirecting incognito NTP to a chrome URL as it crashes Chrome
|
// prevent redirecting incognito NTP to a chrome URL as it crashes Chrome
|
||||||
&& (!url.startsWith('chrome') || !tab.incognito)) {
|
&& (!url.startsWith('chrome') || !tab.incognito)) {
|
||||||
chrome.tabs.update({url}, resolve);
|
chrome.tabs.update({url}, resolve);
|
||||||
} else {
|
} else {
|
||||||
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function activateTab(tab) {
|
function activateTab(tab) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
chrome.tabs.update(tab.id, {active: true}, resolve);
|
chrome.tabs.update(tab.id, {active: true}, resolve);
|
||||||
}),
|
}),
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function stringAsRegExp(s, flags) {
|
function stringAsRegExp(s, flags) {
|
||||||
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ignoreChromeError() {
|
function ignoreChromeError() {
|
||||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getStyleWithNoCode(style) {
|
function getStyleWithNoCode(style) {
|
||||||
const stripped = Object.assign({}, style, {sections: []});
|
const stripped = Object.assign({}, style, {sections: []});
|
||||||
for (const section of style.sections) {
|
for (const section of style.sections) {
|
||||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||||
}
|
}
|
||||||
return stripped;
|
return stripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// js engine can't optimize the entire function if it contains try-catch
|
// js engine can't optimize the entire function if it contains try-catch
|
||||||
// so we should keep it isolated from normal code in a minimal wrapper
|
// so we should keep it isolated from normal code in a minimal wrapper
|
||||||
// Update: might get fixed in V8 TurboFan in the future
|
// Update: might get fixed in V8 TurboFan in the future
|
||||||
function tryCatch(func, ...args) {
|
function tryCatch(func, ...args) {
|
||||||
try {
|
try {
|
||||||
return func(...args);
|
return func(...args);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function tryRegExp(regexp) {
|
function tryRegExp(regexp) {
|
||||||
try {
|
try {
|
||||||
return new RegExp(regexp);
|
return new RegExp(regexp);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function tryJSONparse(jsonString) {
|
function tryJSONparse(jsonString) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(jsonString);
|
return JSON.parse(jsonString);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const debounce = Object.assign((fn, delay, ...args) => {
|
const debounce = Object.assign((fn, delay, ...args) => {
|
||||||
clearTimeout(debounce.timers.get(fn));
|
clearTimeout(debounce.timers.get(fn));
|
||||||
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||||
}, {
|
}, {
|
||||||
timers: new Map(),
|
timers: new Map(),
|
||||||
run(fn, ...args) {
|
run(fn, ...args) {
|
||||||
debounce.timers.delete(fn);
|
debounce.timers.delete(fn);
|
||||||
fn(...args);
|
fn(...args);
|
||||||
},
|
},
|
||||||
unregister(fn) {
|
unregister(fn) {
|
||||||
clearTimeout(debounce.timers.get(fn));
|
clearTimeout(debounce.timers.get(fn));
|
||||||
debounce.timers.delete(fn);
|
debounce.timers.delete(fn);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function deepCopy(obj) {
|
function deepCopy(obj) {
|
||||||
return obj !== null && obj !== undefined && typeof obj == 'object'
|
return obj !== null && obj !== undefined && typeof obj == 'object'
|
||||||
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
|
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
|
||||||
: obj;
|
: obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deepMerge(target, ...args) {
|
function deepMerge(target, ...args) {
|
||||||
const isArray = typeof target.slice == 'function';
|
const isArray = typeof target.slice == 'function';
|
||||||
for (const obj of args) {
|
for (const obj of args) {
|
||||||
if (isArray && obj !== null && obj !== undefined) {
|
if (isArray && obj !== null && obj !== undefined) {
|
||||||
for (const element of obj) {
|
for (const element of obj) {
|
||||||
target.push(deepCopy(element));
|
target.push(deepCopy(element));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const k in obj) {
|
for (const k in obj) {
|
||||||
const value = obj[k];
|
const value = obj[k];
|
||||||
if (k in target && typeof value == 'object' && value !== null) {
|
if (k in target && typeof value == 'object' && value !== null) {
|
||||||
deepMerge(target[k], value);
|
deepMerge(target[k], value);
|
||||||
} else {
|
} else {
|
||||||
target[k] = deepCopy(value);
|
target[k] = deepCopy(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sessionStorageHash(name) {
|
function sessionStorageHash(name) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
|
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
|
||||||
set(k, v) {
|
set(k, v) {
|
||||||
this.value[k] = v;
|
this.value[k] = v;
|
||||||
this.updateStorage();
|
this.updateStorage();
|
||||||
},
|
},
|
||||||
unset(k) {
|
unset(k) {
|
||||||
delete this.value[k];
|
delete this.value[k];
|
||||||
this.updateStorage();
|
this.updateStorage();
|
||||||
},
|
},
|
||||||
updateStorage() {
|
updateStorage() {
|
||||||
sessionStorage[this.name] = JSON.stringify(this.value);
|
sessionStorage[this.name] = JSON.stringify(this.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onBackgroundReady() {
|
function onBackgroundReady() {
|
||||||
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||||
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
||||||
if (health !== undefined) {
|
if (health !== undefined) {
|
||||||
BG = chrome.extension.getBackgroundPage();
|
BG = chrome.extension.getBackgroundPage();
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(ping, 0, resolve);
|
setTimeout(ping, 0, resolve);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||||
function getStylesSafe(options) {
|
function getStylesSafe(options) {
|
||||||
return onBackgroundReady()
|
return onBackgroundReady()
|
||||||
.then(() => BG.getStyles(options));
|
.then(() => BG.getStyles(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveStyleSafe(style) {
|
function saveStyleSafe(style) {
|
||||||
return onBackgroundReady()
|
return onBackgroundReady()
|
||||||
.then(() => BG.saveStyle(BG.deepCopy(style)))
|
.then(() => BG.saveStyle(BG.deepCopy(style)))
|
||||||
.then(savedStyle => {
|
.then(savedStyle => {
|
||||||
if (style.notify === false) {
|
if (style.notify === false) {
|
||||||
handleUpdate(savedStyle, style);
|
handleUpdate(savedStyle, style);
|
||||||
}
|
}
|
||||||
return savedStyle;
|
return savedStyle;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deleteStyleSafe({id, notify = true} = {}) {
|
function deleteStyleSafe({id, notify = true} = {}) {
|
||||||
return onBackgroundReady()
|
return onBackgroundReady()
|
||||||
.then(() => BG.deleteStyle({id, notify}))
|
.then(() => BG.deleteStyle({id, notify}))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!notify) {
|
if (!notify) {
|
||||||
handleDelete(id);
|
handleDelete(id);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function download(url) {
|
function download(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.timeout = 10e3;
|
xhr.timeout = 10e3;
|
||||||
xhr.onloadend = () => (xhr.status == 200
|
xhr.onloadend = () => (xhr.status == 200
|
||||||
? resolve(xhr.responseText)
|
? resolve(xhr.responseText)
|
||||||
: reject(xhr.status));
|
: reject(xhr.status));
|
||||||
const [mainUrl, query] = url.split('?');
|
const [mainUrl, query] = url.split('?');
|
||||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
||||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||||
xhr.send(query);
|
xhr.send(query);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function doTimeout(ms = 0, ...args) {
|
function doTimeout(ms = 0, ...args) {
|
||||||
return ms > 0
|
return ms > 0
|
||||||
? () => new Promise(resolve => setTimeout(resolve, ms, ...args))
|
? () => new Promise(resolve => setTimeout(resolve, ms, ...args))
|
||||||
: new Promise(resolve => setTimeout(resolve, 0, ...args));
|
: new Promise(resolve => setTimeout(resolve, 0, ...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function invokeOrPostpone(isInvoke, fn, ...args) {
|
function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||||
return isInvoke
|
return isInvoke
|
||||||
? fn(...args)
|
? fn(...args)
|
||||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||||
}
|
}
|
|
@ -1,395 +0,0 @@
|
||||||
/* global messageBox, handleUpdate, applyOnMessage */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
|
||||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
|
||||||
|
|
||||||
|
|
||||||
function importFromFile({fileTypeFilter, file} = {}) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const fileInput = document.createElement('input');
|
|
||||||
if (file) {
|
|
||||||
readFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileInput.style.display = 'none';
|
|
||||||
fileInput.type = 'file';
|
|
||||||
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
|
||||||
fileInput.acceptCharset = 'utf-8';
|
|
||||||
|
|
||||||
document.body.appendChild(fileInput);
|
|
||||||
fileInput.initialValue = fileInput.value;
|
|
||||||
fileInput.onchange = readFile;
|
|
||||||
fileInput.click();
|
|
||||||
|
|
||||||
function readFile() {
|
|
||||||
if (file || fileInput.value !== fileInput.initialValue) {
|
|
||||||
file = file || fileInput.files[0];
|
|
||||||
if (file.size > 100e6) {
|
|
||||||
console.warn("100MB backup? I don't believe you.");
|
|
||||||
importFromString('').then(resolve);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.body.style.cursor = 'wait';
|
|
||||||
const fReader = new FileReader();
|
|
||||||
fReader.onloadend = event => {
|
|
||||||
fileInput.remove();
|
|
||||||
importFromString(event.target.result).then(numStyles => {
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
resolve(numStyles);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
fReader.readAsText(file, 'utf-8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function importFromString(jsonString) {
|
|
||||||
if (!BG) {
|
|
||||||
onBackgroundReady().then(() => importFromString(jsonString));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// create objects in background context
|
|
||||||
const json = BG.tryJSONparse(jsonString) || [];
|
|
||||||
if (typeof json.slice != 'function') {
|
|
||||||
json.length = 0;
|
|
||||||
}
|
|
||||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
|
||||||
const oldStylesByName = json.length && new Map(
|
|
||||||
oldStyles.map(style => [style.name.trim(), style]));
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
|
||||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
|
||||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
|
||||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
|
||||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
|
||||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
|
||||||
};
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
let lastRenderTime = performance.now();
|
|
||||||
const renderQueue = [];
|
|
||||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
|
||||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
|
||||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
|
||||||
|
|
||||||
return new Promise(proceed);
|
|
||||||
|
|
||||||
function proceed(resolve) {
|
|
||||||
while (index < json.length) {
|
|
||||||
const item = json[index++];
|
|
||||||
const info = analyze(item);
|
|
||||||
if (info) {
|
|
||||||
// using saveStyle directly since json was parsed in background page context
|
|
||||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
|
||||||
.then(style => account({style, info, resolve}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
|
||||||
renderQueue.length = 0;
|
|
||||||
done(resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
function analyze(item) {
|
|
||||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
|
||||||
|| (item.sections && typeof item.sections.slice != 'function')) {
|
|
||||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
item.name = item.name.trim();
|
|
||||||
const byId = BG.cachedStyles.byId.get(item.id);
|
|
||||||
const byName = oldStylesByName.get(item.name);
|
|
||||||
oldStylesByName.delete(item.name);
|
|
||||||
let oldStyle;
|
|
||||||
if (byId) {
|
|
||||||
if (sameStyle(byId, item)) {
|
|
||||||
oldStyle = byId;
|
|
||||||
} else {
|
|
||||||
item.id = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!oldStyle && byName) {
|
|
||||||
item.id = byName.id;
|
|
||||||
oldStyle = byName;
|
|
||||||
}
|
|
||||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
|
||||||
const metaEqual = oldStyleKeys &&
|
|
||||||
oldStyleKeys.length == Object.keys(item).length &&
|
|
||||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
|
||||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
|
||||||
if (metaEqual && codeEqual) {
|
|
||||||
stats.unchanged.names.push(oldStyle.name);
|
|
||||||
stats.unchanged.ids.push(oldStyle.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {oldStyle, metaEqual, codeEqual};
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameStyle(oldStyle, newStyle) {
|
|
||||||
return oldStyle.name.trim() === newStyle.name.trim() ||
|
|
||||||
['updateUrl', 'originalMd5', 'originalDigest']
|
|
||||||
.some(field => oldStyle[field] && oldStyle[field] == newStyle[field]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function account({style, info, resolve}) {
|
|
||||||
renderQueue.push(style);
|
|
||||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
|
||||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
|
||||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
|
||||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
|
||||||
renderQueue.length = 0;
|
|
||||||
lastRenderTime = performance.now();
|
|
||||||
}
|
|
||||||
setTimeout(proceed, 0, resolve);
|
|
||||||
const {oldStyle, metaEqual, codeEqual} = info;
|
|
||||||
if (!oldStyle) {
|
|
||||||
stats.added.names.push(style.name);
|
|
||||||
stats.added.ids.push(style.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!metaEqual && !codeEqual) {
|
|
||||||
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
|
||||||
stats.metaAndCode.ids.push(style.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!codeEqual) {
|
|
||||||
stats.codeOnly.names.push(style.name);
|
|
||||||
stats.codeOnly.ids.push(style.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
|
||||||
stats.metaOnly.ids.push(style.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function done(resolve) {
|
|
||||||
const numChanged = stats.metaAndCode.names.length +
|
|
||||||
stats.metaOnly.names.length +
|
|
||||||
stats.codeOnly.names.length +
|
|
||||||
stats.added.names.length;
|
|
||||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
|
||||||
const report = Object.keys(stats)
|
|
||||||
.filter(kind => stats[kind].names.length)
|
|
||||||
.map(kind => {
|
|
||||||
const {ids, names, legend} = stats[kind];
|
|
||||||
const listItemsWithId = (name, i) =>
|
|
||||||
$element({dataset: {id: ids[i]}, textContent: name});
|
|
||||||
const listItems = name =>
|
|
||||||
$element({textContent: name});
|
|
||||||
const block =
|
|
||||||
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
|
||||||
$element({tag: 'summary', appendChild:
|
|
||||||
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
|
||||||
}),
|
|
||||||
$element({tag: 'small', appendChild:
|
|
||||||
names.map(ids ? listItemsWithId : listItems)
|
|
||||||
}),
|
|
||||||
]});
|
|
||||||
return block;
|
|
||||||
});
|
|
||||||
scrollTo(0, 0);
|
|
||||||
messageBox({
|
|
||||||
title: t('importReportTitle'),
|
|
||||||
contents: report.length ? report : t('importReportUnchanged'),
|
|
||||||
buttons: [t('confirmOK'), numChanged && t('undo')],
|
|
||||||
onshow: bindClick,
|
|
||||||
}).then(({button, enter, esc}) => {
|
|
||||||
if (button == 1) {
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve(numChanged);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function undo() {
|
|
||||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
|
||||||
const newIds = [
|
|
||||||
...stats.metaAndCode.ids,
|
|
||||||
...stats.metaOnly.ids,
|
|
||||||
...stats.codeOnly.ids,
|
|
||||||
...stats.added.ids,
|
|
||||||
];
|
|
||||||
let resolve;
|
|
||||||
index = 0;
|
|
||||||
return new Promise(resolve_ => {
|
|
||||||
resolve = resolve_;
|
|
||||||
undoNextId();
|
|
||||||
}).then(refreshAllTabs)
|
|
||||||
.then(() => messageBox({
|
|
||||||
title: t('importReportUndoneTitle'),
|
|
||||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
|
||||||
buttons: [t('confirmOK')],
|
|
||||||
}));
|
|
||||||
function undoNextId() {
|
|
||||||
if (index == newIds.length) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = newIds[index++];
|
|
||||||
deleteStyleSafe({id, notify: false}).then(id => {
|
|
||||||
const oldStyle = oldStylesById.get(id);
|
|
||||||
if (oldStyle) {
|
|
||||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
|
||||||
.then(undoNextId);
|
|
||||||
} else {
|
|
||||||
undoNextId();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindClick(box) {
|
|
||||||
const highlightElement = event => {
|
|
||||||
const styleElement = $('#style-' + event.target.dataset.id);
|
|
||||||
if (styleElement) {
|
|
||||||
scrollElementIntoView(styleElement);
|
|
||||||
animateElement(styleElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (const block of $$('details')) {
|
|
||||||
if (block.dataset.id != 'invalid') {
|
|
||||||
block.style.cursor = 'pointer';
|
|
||||||
block.onclick = highlightElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function limitString(s, limit = 100) {
|
|
||||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportNameChange(oldStyle, newStyle) {
|
|
||||||
return newStyle.name != oldStyle.name
|
|
||||||
? oldStyle.name + ' —> ' + newStyle.name
|
|
||||||
: oldStyle.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshAllTabs() {
|
|
||||||
return Promise.all([
|
|
||||||
getActiveTab(),
|
|
||||||
getOwnTab(),
|
|
||||||
]).then(([activeTab, ownTab]) => new Promise(resolve => {
|
|
||||||
// list all tabs including chrome-extension:// which can be ours
|
|
||||||
queryTabs().then(tabs => {
|
|
||||||
const lastTab = tabs[tabs.length - 1];
|
|
||||||
for (const tab of tabs) {
|
|
||||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
|
||||||
if (FIREFOX && !tab.width) {
|
|
||||||
if (tab == lastTab) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
|
||||||
const message = {method: 'styleReplaceAll', styles};
|
|
||||||
if (tab.id == ownTab.id) {
|
|
||||||
applyOnMessage(message);
|
|
||||||
} else {
|
|
||||||
invokeOrPostpone(tab.id == activeTab.id,
|
|
||||||
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
|
|
||||||
}
|
|
||||||
setTimeout(BG.updateIcon, 0, tab, styles);
|
|
||||||
if (tab == lastTab) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$('#file-all-styles').onclick = () => {
|
|
||||||
getStylesSafe().then(styles => {
|
|
||||||
const text = JSON.stringify(styles, null, '\t');
|
|
||||||
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
|
||||||
return url;
|
|
||||||
// for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600
|
|
||||||
}).then(fetch)
|
|
||||||
.then(res => res.blob())
|
|
||||||
.then(blob => {
|
|
||||||
const objectURL = URL.createObjectURL(blob);
|
|
||||||
let link = $element({
|
|
||||||
tag:'a',
|
|
||||||
href: objectURL,
|
|
||||||
type: 'application/json',
|
|
||||||
download: generateFileName(),
|
|
||||||
});
|
|
||||||
// TODO: remove the fallback when FF multi-process bug is fixed
|
|
||||||
if (!FIREFOX) {
|
|
||||||
link.dispatchEvent(new MouseEvent('click'));
|
|
||||||
setTimeout(() => URL.revokeObjectURL(objectURL));
|
|
||||||
} else {
|
|
||||||
const iframe = document.body.appendChild($element({
|
|
||||||
tag: 'iframe',
|
|
||||||
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
|
|
||||||
}));
|
|
||||||
doTimeout().then(() => {
|
|
||||||
link = iframe.contentDocument.importNode(link, true);
|
|
||||||
iframe.contentDocument.body.appendChild(link);
|
|
||||||
})
|
|
||||||
.then(doTimeout)
|
|
||||||
.then(() => link.dispatchEvent(new MouseEvent('click')))
|
|
||||||
.then(doTimeout(1000))
|
|
||||||
.then(() => {
|
|
||||||
URL.revokeObjectURL(objectURL);
|
|
||||||
iframe.remove();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function generateFileName() {
|
|
||||||
const today = new Date();
|
|
||||||
const dd = ('0' + today.getDate()).substr(-2);
|
|
||||||
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
|
||||||
const yyyy = today.getFullYear();
|
|
||||||
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$('#unfile-all-styles').onclick = () => {
|
|
||||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(document.body, {
|
|
||||||
ondragover(event) {
|
|
||||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
|
||||||
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
|
||||||
this.classList.toggle('dropzone', hasFiles);
|
|
||||||
if (hasFiles) {
|
|
||||||
event.preventDefault();
|
|
||||||
clearTimeout(this.fadeoutTimer);
|
|
||||||
this.classList.remove('fadeout');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ondragend(event) {
|
|
||||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
|
||||||
this.style.animationDuration = '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ondragleave(event) {
|
|
||||||
try {
|
|
||||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
|
||||||
if (event.target === this) {
|
|
||||||
this.ondragend();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.ondragend();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ondrop(event) {
|
|
||||||
this.ondragend();
|
|
||||||
if (event.dataTransfer.files.length) {
|
|
||||||
event.preventDefault();
|
|
||||||
if ($('#onlyUpdates input').checked) {
|
|
||||||
$('#onlyUpdates input').click();
|
|
||||||
}
|
|
||||||
importFromFile({file: event.dataTransfer.files[0]});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,360 +1,360 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium
|
const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium
|
||||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||||
const VIVALDI = /Vivaldi/.test(navigator.userAgent);
|
const VIVALDI = /Vivaldi/.test(navigator.userAgent);
|
||||||
const OPERA = /OPR/.test(navigator.userAgent);
|
const OPERA = /OPR/.test(navigator.userAgent);
|
||||||
|
|
||||||
document.addEventListener('stylishUpdate', onUpdateClicked);
|
document.addEventListener('stylishUpdate', onUpdateClicked);
|
||||||
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
|
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||||
document.addEventListener('stylishUpdateOpera', onUpdateClicked);
|
document.addEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||||
|
|
||||||
document.addEventListener('stylishInstall', onInstallClicked);
|
document.addEventListener('stylishInstall', onInstallClicked);
|
||||||
document.addEventListener('stylishInstallChrome', onInstallClicked);
|
document.addEventListener('stylishInstallChrome', onInstallClicked);
|
||||||
document.addEventListener('stylishInstallOpera', onInstallClicked);
|
document.addEventListener('stylishInstallOpera', onInstallClicked);
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
// orphaned content script check
|
// orphaned content script check
|
||||||
if (msg.method == 'ping') {
|
if (msg.method == 'ping') {
|
||||||
sendResponse(true);
|
sendResponse(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: remove the following statement when USO is fixed
|
// TODO: remove the following statement when USO is fixed
|
||||||
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
||||||
function() {
|
function() {
|
||||||
let settings;
|
let settings;
|
||||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
||||||
});
|
});
|
||||||
const originalResponseJson = Response.prototype.json;
|
const originalResponseJson = Response.prototype.json;
|
||||||
Response.prototype.json = function(...args) {
|
Response.prototype.json = function(...args) {
|
||||||
return originalResponseJson.call(this, ...args).then(json => {
|
return originalResponseJson.call(this, ...args).then(json => {
|
||||||
Response.prototype.json = originalResponseJson;
|
Response.prototype.json = originalResponseJson;
|
||||||
if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') {
|
if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
const images = new Map();
|
const images = new Map();
|
||||||
for (const jsonSetting of json.style_settings) {
|
for (const jsonSetting of json.style_settings) {
|
||||||
let value = settings.get('ik-' + jsonSetting.install_key);
|
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||||
if (!value
|
if (!value
|
||||||
|| !jsonSetting.style_setting_options
|
|| !jsonSetting.style_setting_options
|
||||||
|| !jsonSetting.style_setting_options[0]) {
|
|| !jsonSetting.style_setting_options[0]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (value.startsWith('ik-')) {
|
if (value.startsWith('ik-')) {
|
||||||
value = value.replace(/^ik-/, '');
|
value = value.replace(/^ik-/, '');
|
||||||
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||||
if (!defaultItem || defaultItem.install_key != value) {
|
if (!defaultItem || defaultItem.install_key != value) {
|
||||||
if (defaultItem) {
|
if (defaultItem) {
|
||||||
defaultItem.default = false;
|
defaultItem.default = false;
|
||||||
}
|
}
|
||||||
jsonSetting.style_setting_options.some(item => {
|
jsonSetting.style_setting_options.some(item => {
|
||||||
if (item.install_key == value) {
|
if (item.install_key == value) {
|
||||||
item.default = true;
|
item.default = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (jsonSetting.setting_type == 'image') {
|
} else if (jsonSetting.setting_type == 'image') {
|
||||||
jsonSetting.style_setting_options.some(item => {
|
jsonSetting.style_setting_options.some(item => {
|
||||||
if (item.default) {
|
if (item.default) {
|
||||||
item.default = false;
|
item.default = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
images.set(jsonSetting.install_key, value);
|
images.set(jsonSetting.install_key, value);
|
||||||
} else {
|
} else {
|
||||||
const item = jsonSetting.style_setting_options[0];
|
const item = jsonSetting.style_setting_options[0];
|
||||||
if (item.value !== value && item.install_key == 'placeholder') {
|
if (item.value !== value && item.install_key == 'placeholder') {
|
||||||
item.value = value;
|
item.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (images.size) {
|
if (images.size) {
|
||||||
new MutationObserver((_, observer) => {
|
new MutationObserver((_, observer) => {
|
||||||
if (!document.getElementById('style-settings')) {
|
if (!document.getElementById('style-settings')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
for (const [name, url] of images.entries()) {
|
for (const [name, url] of images.entries()) {
|
||||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||||
if (elUrl) {
|
if (elUrl) {
|
||||||
elUrl.value = url;
|
elUrl.value = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}).observe(document, {childList: true, subtree: true});
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} + ')()';
|
} + ')()';
|
||||||
|
|
||||||
// TODO: remove the following statement when USO pagination is fixed
|
// TODO: remove the following statement when USO pagination is fixed
|
||||||
if (location.search.includes('category=')) {
|
if (location.search.includes('category=')) {
|
||||||
document.addEventListener('DOMContentLoaded', function _() {
|
document.addEventListener('DOMContentLoaded', function _() {
|
||||||
document.removeEventListener('DOMContentLoaded', _);
|
document.removeEventListener('DOMContentLoaded', _);
|
||||||
new MutationObserver((_, observer) => {
|
new MutationObserver((_, observer) => {
|
||||||
if (!document.getElementById('pagination')) {
|
if (!document.getElementById('pagination')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||||
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||||
for (let i = 0; i < links.length; i++) {
|
for (let i = 0; i < links.length; i++) {
|
||||||
links[i].href += category;
|
links[i].href += category;
|
||||||
}
|
}
|
||||||
}).observe(document, {childList: true, subtree: true});
|
}).observe(document, {childList: true, subtree: true});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
if (document.body) {
|
if (document.body) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
// TODO: remove the following statement when USO pagination title is fixed
|
// TODO: remove the following statement when USO pagination title is fixed
|
||||||
document.title = document.title.replace(/^\d+&category=/, '');
|
document.title = document.title.replace(/^\d+&category=/, '');
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
method: 'getStyles',
|
method: 'getStyles',
|
||||||
url: getMeta('stylish-id-url') || location.href
|
url: getMeta('stylish-id-url') || location.href
|
||||||
}, checkUpdatability);
|
}, checkUpdatability);
|
||||||
}
|
}
|
||||||
}).observe(document.documentElement, {childList: true});
|
}).observe(document.documentElement, {childList: true});
|
||||||
|
|
||||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||||
we need to fix this URL using "stylish-update-url" meta key
|
we need to fix this URL using "stylish-update-url" meta key
|
||||||
*/
|
*/
|
||||||
function getStyleURL() {
|
function getStyleURL() {
|
||||||
const url = getMeta('stylish-code-chrome');
|
const url = getMeta('stylish-code-chrome');
|
||||||
// TODO: remove when USO is fixed
|
// TODO: remove when USO is fixed
|
||||||
const directUrl = getMeta('stylish-update-url');
|
const directUrl = getMeta('stylish-update-url');
|
||||||
if (directUrl.includes('?') && !url.includes('?')) {
|
if (directUrl.includes('?') && !url.includes('?')) {
|
||||||
/* get custom settings from the update url */
|
/* get custom settings from the update url */
|
||||||
return Object.assign(new URL(url), {
|
return Object.assign(new URL(url), {
|
||||||
search: (new URL(directUrl)).search
|
search: (new URL(directUrl)).search
|
||||||
}).href;
|
}).href;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkUpdatability([installedStyle]) {
|
function checkUpdatability([installedStyle]) {
|
||||||
// TODO: remove the following statement when USO is fixed
|
// TODO: remove the following statement when USO is fixed
|
||||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||||
detail: installedStyle && installedStyle.updateUrl,
|
detail: installedStyle && installedStyle.updateUrl,
|
||||||
}));
|
}));
|
||||||
if (!installedStyle) {
|
if (!installedStyle) {
|
||||||
sendEvent('styleCanBeInstalledChrome');
|
sendEvent('styleCanBeInstalledChrome');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const md5Url = getMeta('stylish-md5-url');
|
const md5Url = getMeta('stylish-md5-url');
|
||||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||||
getResource(md5Url).then(md5 => {
|
getResource(md5Url).then(md5 => {
|
||||||
reportUpdatable(md5 != installedStyle.originalMd5);
|
reportUpdatable(md5 != installedStyle.originalMd5);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getResource(getStyleURL()).then(code => {
|
getResource(getStyleURL()).then(code => {
|
||||||
reportUpdatable(code === null ||
|
reportUpdatable(code === null ||
|
||||||
!styleSectionsEqual(JSON.parse(code), installedStyle));
|
!styleSectionsEqual(JSON.parse(code), installedStyle));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportUpdatable(isUpdatable) {
|
function reportUpdatable(isUpdatable) {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
isUpdatable
|
isUpdatable
|
||||||
? 'styleCanBeUpdatedChrome'
|
? 'styleCanBeUpdatedChrome'
|
||||||
: 'styleAlreadyInstalledChrome',
|
: 'styleAlreadyInstalledChrome',
|
||||||
{
|
{
|
||||||
updateUrl: installedStyle.updateUrl
|
updateUrl: installedStyle.updateUrl
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sendEvent(type, detail = null) {
|
function sendEvent(type, detail = null) {
|
||||||
if (FIREFOX) {
|
if (FIREFOX) {
|
||||||
type = type.replace('Chrome', '');
|
type = type.replace('Chrome', '');
|
||||||
} else if (OPERA || VIVALDI) {
|
} else if (OPERA || VIVALDI) {
|
||||||
type = type.replace('Chrome', 'Opera');
|
type = type.replace('Chrome', 'Opera');
|
||||||
}
|
}
|
||||||
detail = {detail};
|
detail = {detail};
|
||||||
if (typeof cloneInto != 'undefined') {
|
if (typeof cloneInto != 'undefined') {
|
||||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||||
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||||
}
|
}
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
document.dispatchEvent(new CustomEvent(type, detail));
|
document.dispatchEvent(new CustomEvent(type, detail));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onInstallClicked() {
|
function onInstallClicked() {
|
||||||
if (!orphanCheck || !orphanCheck()) {
|
if (!orphanCheck || !orphanCheck()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getResource(getMeta('stylish-description'))
|
getResource(getMeta('stylish-description'))
|
||||||
.then(name => saveStyleCode('styleInstall', name))
|
.then(name => saveStyleCode('styleInstall', name))
|
||||||
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onUpdateClicked() {
|
function onUpdateClicked() {
|
||||||
if (!orphanCheck || !orphanCheck()) {
|
if (!orphanCheck || !orphanCheck()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
method: 'getStyles',
|
method: 'getStyles',
|
||||||
url: getMeta('stylish-id-url') || location.href,
|
url: getMeta('stylish-id-url') || location.href,
|
||||||
}, ([style]) => {
|
}, ([style]) => {
|
||||||
saveStyleCode('styleUpdate', style.name, {id: style.id});
|
saveStyleCode('styleUpdate', style.name, {id: style.id});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveStyleCode(message, name, addProps) {
|
function saveStyleCode(message, name, addProps) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
|
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enableUpdateButton(false);
|
enableUpdateButton(false);
|
||||||
getResource(getStyleURL()).then(code => {
|
getResource(getStyleURL()).then(code => {
|
||||||
chrome.runtime.sendMessage(
|
chrome.runtime.sendMessage(
|
||||||
Object.assign(JSON.parse(code), addProps, {
|
Object.assign(JSON.parse(code), addProps, {
|
||||||
method: 'saveStyle',
|
method: 'saveStyle',
|
||||||
reason: 'update',
|
reason: 'update',
|
||||||
}),
|
}),
|
||||||
style => {
|
style => {
|
||||||
if (message == 'styleUpdate' && style.updateUrl.includes('?')) {
|
if (message == 'styleUpdate' && style.updateUrl.includes('?')) {
|
||||||
enableUpdateButton(true);
|
enableUpdateButton(true);
|
||||||
} else {
|
} else {
|
||||||
sendEvent('styleInstalledChrome');
|
sendEvent('styleInstalledChrome');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function enableUpdateButton(state) {
|
function enableUpdateButton(state) {
|
||||||
const button = document.getElementById('update_style_button');
|
const button = document.getElementById('update_style_button');
|
||||||
if (button) {
|
if (button) {
|
||||||
button.style.cssText = state ? '' :
|
button.style.cssText = state ? '' :
|
||||||
'pointer-events: none !important; opacity: .25 !important;';
|
'pointer-events: none !important; opacity: .25 !important;';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getMeta(name) {
|
function getMeta(name) {
|
||||||
const e = document.querySelector(`link[rel="${name}"]`);
|
const e = document.querySelector(`link[rel="${name}"]`);
|
||||||
return e ? e.getAttribute('href') : null;
|
return e ? e.getAttribute('href') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getResource(url) {
|
function getResource(url) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (url.startsWith('#')) {
|
if (url.startsWith('#')) {
|
||||||
resolve(document.getElementById(url.slice(1)).textContent);
|
resolve(document.getElementById(url.slice(1)).textContent);
|
||||||
} else {
|
} else {
|
||||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (a.length != b.length) {
|
if (a.length != b.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const checkedInB = [];
|
const checkedInB = [];
|
||||||
return a.every(sectionA => b.some(sectionB => {
|
return a.every(sectionA => b.some(sectionB => {
|
||||||
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
|
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
|
||||||
checkedInB.push(sectionB);
|
checkedInB.push(sectionB);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function propertiesEqual(secA, secB) {
|
function propertiesEqual(secA, secB) {
|
||||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function equalOrEmpty(a, b, telltale, comparator) {
|
function equalOrEmpty(a, b, telltale, comparator) {
|
||||||
const typeA = a && typeof a[telltale] == 'function';
|
const typeA = a && typeof a[telltale] == 'function';
|
||||||
const typeB = b && typeof b[telltale] == 'function';
|
const typeB = b && typeof b[telltale] == 'function';
|
||||||
return (
|
return (
|
||||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||||
(b === null || b === undefined || (typeB && !b.length))
|
(b === null || b === undefined || (typeB && !b.length))
|
||||||
) || typeA && typeB && a.length == b.length && comparator(a, b);
|
) || typeA && typeB && a.length == b.length && comparator(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayMirrors(array1, array2) {
|
function arrayMirrors(array1, array2) {
|
||||||
for (const el of array1) {
|
for (const el of array1) {
|
||||||
if (array2.indexOf(el) < 0) {
|
if (array2.indexOf(el) < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const el of array2) {
|
for (const el of array2) {
|
||||||
if (array1.indexOf(el) < 0) {
|
if (array1.indexOf(el) < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onDOMready() {
|
function onDOMready() {
|
||||||
if (document.readyState != 'loading') {
|
if (document.readyState != 'loading') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
document.addEventListener('DOMContentLoaded', function _() {
|
document.addEventListener('DOMContentLoaded', function _() {
|
||||||
document.removeEventListener('DOMContentLoaded', _);
|
document.removeEventListener('DOMContentLoaded', _);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function orphanCheck() {
|
function orphanCheck() {
|
||||||
const port = chrome.runtime.connect();
|
const port = chrome.runtime.connect();
|
||||||
if (port) {
|
if (port) {
|
||||||
port.disconnect();
|
port.disconnect();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// we're orphaned due to an extension update
|
// we're orphaned due to an extension update
|
||||||
// we can detach event listeners
|
// we can detach event listeners
|
||||||
document.removeEventListener('stylishUpdate', onUpdateClicked);
|
document.removeEventListener('stylishUpdate', onUpdateClicked);
|
||||||
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
|
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||||
document.removeEventListener('stylishUpdateOpera', onUpdateClicked);
|
document.removeEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||||
|
|
||||||
document.removeEventListener('stylishInstall', onInstallClicked);
|
document.removeEventListener('stylishInstall', onInstallClicked);
|
||||||
document.removeEventListener('stylishInstallChrome', onInstallClicked);
|
document.removeEventListener('stylishInstallChrome', onInstallClicked);
|
||||||
document.removeEventListener('stylishInstallOpera', onInstallClicked);
|
document.removeEventListener('stylishInstallOpera', onInstallClicked);
|
||||||
|
|
||||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||||
// we can destroy global functions in this context to free up memory
|
// we can destroy global functions in this context to free up memory
|
||||||
[
|
[
|
||||||
'checkUpdatability',
|
'checkUpdatability',
|
||||||
'getMeta',
|
'getMeta',
|
||||||
'getResource',
|
'getResource',
|
||||||
'onDOMready',
|
'onDOMready',
|
||||||
'onInstallClicked',
|
'onInstallClicked',
|
||||||
'onUpdateClicked',
|
'onUpdateClicked',
|
||||||
'orphanCheck',
|
'orphanCheck',
|
||||||
'saveStyleCode',
|
'saveStyleCode',
|
||||||
'sendEvent',
|
'sendEvent',
|
||||||
'styleSectionsEqual',
|
'styleSectionsEqual',
|
||||||
].forEach(fn => (window[fn] = null));
|
].forEach(fn => (window[fn] = null));
|
||||||
}
|
}
|
791
manage/fileSaveLoad.js
Normal file
791
manage/fileSaveLoad.js
Normal file
|
@ -0,0 +1,791 @@
|
||||||
|
/* global messageBox, handleUpdate, applyOnMessage */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
|
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||||
|
|
||||||
|
|
||||||
|
function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
if (file) {
|
||||||
|
readFile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
||||||
|
fileInput.acceptCharset = 'utf-8';
|
||||||
|
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
fileInput.initialValue = fileInput.value;
|
||||||
|
fileInput.onchange = readFile;
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
function readFile() {
|
||||||
|
if (file || fileInput.value !== fileInput.initialValue) {
|
||||||
|
file = file || fileInput.files[0];
|
||||||
|
if (file.size > 100e6) {
|
||||||
|
console.warn("100MB backup? I don't believe you.");
|
||||||
|
importFromString('').then(resolve);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.body.style.cursor = 'wait';
|
||||||
|
const fReader = new FileReader();
|
||||||
|
fReader.onloadend = event => {
|
||||||
|
fileInput.remove();
|
||||||
|
importFromString(event.target.result).then(numStyles => {
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
resolve(numStyles);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fReader.readAsText(file, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function importFromString(jsonString) {
|
||||||
|
if (!BG) {
|
||||||
|
onBackgroundReady().then(() => importFromString(jsonString));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create objects in background context
|
||||||
|
const json = BG.tryJSONparse(jsonString) || [];
|
||||||
|
if (typeof json.slice != 'function') {
|
||||||
|
json.length = 0;
|
||||||
|
}
|
||||||
|
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||||
|
const oldStylesByName = json.length && new Map(
|
||||||
|
oldStyles.map(style => [style.name.trim(), style]));
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||||
|
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||||
|
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||||
|
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||||
|
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||||
|
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let lastRenderTime = performance.now();
|
||||||
|
const renderQueue = [];
|
||||||
|
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||||
|
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||||
|
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||||
|
|
||||||
|
return new Promise(proceed);
|
||||||
|
|
||||||
|
function proceed(resolve) {
|
||||||
|
while (index < json.length) {
|
||||||
|
const item = json[index++];
|
||||||
|
const info = analyze(item);
|
||||||
|
if (info) {
|
||||||
|
// using saveStyle directly since json was parsed in background page context
|
||||||
|
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||||
|
.then(style => account({style, info, resolve}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||||
|
renderQueue.length = 0;
|
||||||
|
done(resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyze(item) {
|
||||||
|
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||||
|
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||||
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.name = item.name.trim();
|
||||||
|
const byId = BG.cachedStyles.byId.get(item.id);
|
||||||
|
const byName = oldStylesByName.get(item.name);
|
||||||
|
oldStylesByName.delete(item.name);
|
||||||
|
let oldStyle;
|
||||||
|
if (byId) {
|
||||||
|
if (sameStyle(byId, item)) {
|
||||||
|
oldStyle = byId;
|
||||||
|
} else {
|
||||||
|
item.id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oldStyle && byName) {
|
||||||
|
item.id = byName.id;
|
||||||
|
oldStyle = byName;
|
||||||
|
}
|
||||||
|
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||||
|
const metaEqual = oldStyleKeys &&
|
||||||
|
oldStyleKeys.length == Object.keys(item).length &&
|
||||||
|
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||||
|
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||||
|
if (metaEqual && codeEqual) {
|
||||||
|
stats.unchanged.names.push(oldStyle.name);
|
||||||
|
stats.unchanged.ids.push(oldStyle.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return {oldStyle, metaEqual, codeEqual};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameStyle(oldStyle, newStyle) {
|
||||||
|
return oldStyle.name.trim() === newStyle.name.trim() ||
|
||||||
|
['updateUrl', 'originalMd5', 'originalDigest']
|
||||||
|
.some(field => oldStyle[field] && oldStyle[field] == newStyle[field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function account({style, info, resolve}) {
|
||||||
|
renderQueue.push(style);
|
||||||
|
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||||
|
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||||
|
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||||
|
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||||
|
renderQueue.length = 0;
|
||||||
|
lastRenderTime = performance.now();
|
||||||
|
}
|
||||||
|
setTimeout(proceed, 0, resolve);
|
||||||
|
const {oldStyle, metaEqual, codeEqual} = info;
|
||||||
|
if (!oldStyle) {
|
||||||
|
stats.added.names.push(style.name);
|
||||||
|
stats.added.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!metaEqual && !codeEqual) {
|
||||||
|
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaAndCode.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!codeEqual) {
|
||||||
|
stats.codeOnly.names.push(style.name);
|
||||||
|
stats.codeOnly.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaOnly.ids.push(style.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(resolve) {
|
||||||
|
const numChanged = stats.metaAndCode.names.length +
|
||||||
|
stats.metaOnly.names.length +
|
||||||
|
stats.codeOnly.names.length +
|
||||||
|
stats.added.names.length;
|
||||||
|
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||||
|
const report = Object.keys(stats)
|
||||||
|
.filter(kind => stats[kind].names.length)
|
||||||
|
.map(kind => {
|
||||||
|
const {ids, names, legend} = stats[kind];
|
||||||
|
const listItemsWithId = (name, i) =>
|
||||||
|
$element({dataset: {id: ids[i]}, textContent: name});
|
||||||
|
const listItems = name =>
|
||||||
|
$element({textContent: name});
|
||||||
|
const block =
|
||||||
|
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||||
|
$element({tag: 'summary', appendChild:
|
||||||
|
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||||
|
}),
|
||||||
|
$element({tag: 'small', appendChild:
|
||||||
|
names.map(ids ? listItemsWithId : listItems)
|
||||||
|
}),
|
||||||
|
]});
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
scrollTo(0, 0);
|
||||||
|
messageBox({
|
||||||
|
title: t('importReportTitle'),
|
||||||
|
contents: report.length ? report : t('importReportUnchanged'),
|
||||||
|
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||||
|
onshow: bindClick,
|
||||||
|
}).then(({button, enter, esc}) => {
|
||||||
|
if (button == 1) {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(numChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||||
|
const newIds = [
|
||||||
|
...stats.metaAndCode.ids,
|
||||||
|
...stats.metaOnly.ids,
|
||||||
|
...stats.codeOnly.ids,
|
||||||
|
...stats.added.ids,
|
||||||
|
];
|
||||||
|
let resolve;
|
||||||
|
index = 0;
|
||||||
|
return new Promise(resolve_ => {
|
||||||
|
resolve = resolve_;
|
||||||
|
undoNextId();
|
||||||
|
}).then(refreshAllTabs)
|
||||||
|
.then(() => messageBox({
|
||||||
|
title: t('importReportUndoneTitle'),
|
||||||
|
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||||
|
buttons: [t('confirmOK')],
|
||||||
|
}));
|
||||||
|
function undoNextId() {
|
||||||
|
if (index == newIds.length) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = newIds[index++];
|
||||||
|
deleteStyleSafe({id, notify: false}).then(id => {
|
||||||
|
const oldStyle = oldStylesById.get(id);
|
||||||
|
if (oldStyle) {
|
||||||
|
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||||
|
.then(undoNextId);
|
||||||
|
} else {
|
||||||
|
undoNextId();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindClick(box) {
|
||||||
|
const highlightElement = event => {
|
||||||
|
const styleElement = $('#style-' + event.target.dataset.id);
|
||||||
|
if (styleElement) {
|
||||||
|
scrollElementIntoView(styleElement);
|
||||||
|
animateElement(styleElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const block of $$('details')) {
|
||||||
|
if (block.dataset.id != 'invalid') {
|
||||||
|
block.style.cursor = 'pointer';
|
||||||
|
block.onclick = highlightElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function limitString(s, limit = 100) {
|
||||||
|
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportNameChange(oldStyle, newStyle) {
|
||||||
|
return newStyle.name != oldStyle.name
|
||||||
|
? oldStyle.name + ' —> ' + newStyle.name
|
||||||
|
: oldStyle.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAllTabs() {
|
||||||
|
return Promise.all([
|
||||||
|
getActiveTab(),
|
||||||
|
getOwnTab(),
|
||||||
|
]).then(([activeTab, ownTab]) => new Promise(resolve => {
|
||||||
|
// list all tabs including chrome-extension:// which can be ours
|
||||||
|
queryTabs().then(tabs => {
|
||||||
|
const lastTab = tabs[tabs.length - 1];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||||
|
if (FIREFOX && !tab.width) {
|
||||||
|
if (tab == lastTab) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||||
|
const message = {method: 'styleReplaceAll', styles};
|
||||||
|
if (tab.id == ownTab.id) {
|
||||||
|
applyOnMessage(message);
|
||||||
|
} else {
|
||||||
|
invokeOrPostpone(tab.id == activeTab.id,
|
||||||
|
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
|
||||||
|
}
|
||||||
|
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||||
|
if (tab == lastTab) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$('#file-all-styles').onclick = () => {
|
||||||
|
getStylesSafe().then(styles => {
|
||||||
|
const text = JSON.stringify(styles, null, '\t');
|
||||||
|
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||||
|
return url;
|
||||||
|
// for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600
|
||||||
|
}).then(fetch)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const objectURL = URL.createObjectURL(blob);
|
||||||
|
let link = $element({
|
||||||
|
tag:'a',
|
||||||
|
href: objectURL,
|
||||||
|
type: 'application/json',
|
||||||
|
download: generateFileName(),
|
||||||
|
});
|
||||||
|
// TODO: remove the fallback when FF multi-process bug is fixed
|
||||||
|
if (!FIREFOX) {
|
||||||
|
link.dispatchEvent(new MouseEvent('click'));
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||||
|
} else {
|
||||||
|
const iframe = document.body.appendChild($element({
|
||||||
|
tag: 'iframe',
|
||||||
|
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
|
||||||
|
}));
|
||||||
|
doTimeout().then(() => {
|
||||||
|
link = iframe.contentDocument.importNode(link, true);
|
||||||
|
iframe.contentDocument.body.appendChild(link);
|
||||||
|
})
|
||||||
|
.then(doTimeout)
|
||||||
|
.then(() => link.dispatchEvent(new MouseEvent('click')))
|
||||||
|
.then(doTimeout(1000))
|
||||||
|
.then(() => {
|
||||||
|
URL.revokeObjectURL(objectURL);
|
||||||
|
iframe.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateFileName() {
|
||||||
|
const today = new Date();
|
||||||
|
const dd = ('0' + today.getDate()).substr(-2);
|
||||||
|
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||||
|
const yyyy = today.getFullYear();
|
||||||
|
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
$('#unfile-all-styles').onclick = () => {
|
||||||
|
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(document.body, {
|
||||||
|
ondragover(event) {
|
||||||
|
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||||
|
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||||
|
this.classList.toggle('dropzone', hasFiles);
|
||||||
|
if (hasFiles) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(this.fadeoutTimer);
|
||||||
|
this.classList.remove('fadeout');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondragend(event) {
|
||||||
|
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||||
|
this.style.animationDuration = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ondragleave(event) {
|
||||||
|
try {
|
||||||
|
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||||
|
if (event.target === this) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondrop(event) {
|
||||||
|
this.ondragend();
|
||||||
|
if (event.dataTransfer.files.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#onlyUpdates input').checked) {
|
||||||
|
$('#onlyUpdates input').click();
|
||||||
|
}
|
||||||
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
=======
|
||||||
|
/* global messageBox, handleUpdate, applyOnMessage */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
|
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||||
|
|
||||||
|
|
||||||
|
function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
if (file) {
|
||||||
|
readFile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
||||||
|
fileInput.acceptCharset = 'utf-8';
|
||||||
|
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
fileInput.initialValue = fileInput.value;
|
||||||
|
fileInput.onchange = readFile;
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
function readFile() {
|
||||||
|
if (file || fileInput.value !== fileInput.initialValue) {
|
||||||
|
file = file || fileInput.files[0];
|
||||||
|
if (file.size > 100e6) {
|
||||||
|
console.warn("100MB backup? I don't believe you.");
|
||||||
|
importFromString('').then(resolve);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.body.style.cursor = 'wait';
|
||||||
|
const fReader = new FileReader();
|
||||||
|
fReader.onloadend = event => {
|
||||||
|
fileInput.remove();
|
||||||
|
importFromString(event.target.result).then(numStyles => {
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
resolve(numStyles);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fReader.readAsText(file, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function importFromString(jsonString) {
|
||||||
|
if (!BG) {
|
||||||
|
onBackgroundReady().then(() => importFromString(jsonString));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create objects in background context
|
||||||
|
const json = BG.tryJSONparse(jsonString) || [];
|
||||||
|
if (typeof json.slice != 'function') {
|
||||||
|
json.length = 0;
|
||||||
|
}
|
||||||
|
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||||
|
const oldStylesByName = json.length && new Map(
|
||||||
|
oldStyles.map(style => [style.name.trim(), style]));
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||||
|
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||||
|
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||||
|
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||||
|
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||||
|
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let lastRenderTime = performance.now();
|
||||||
|
const renderQueue = [];
|
||||||
|
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||||
|
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||||
|
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||||
|
|
||||||
|
return new Promise(proceed);
|
||||||
|
|
||||||
|
function proceed(resolve) {
|
||||||
|
while (index < json.length) {
|
||||||
|
const item = json[index++];
|
||||||
|
const info = analyze(item);
|
||||||
|
if (info) {
|
||||||
|
// using saveStyle directly since json was parsed in background page context
|
||||||
|
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||||
|
.then(style => account({style, info, resolve}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||||
|
renderQueue.length = 0;
|
||||||
|
done(resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyze(item) {
|
||||||
|
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||||
|
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||||
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.name = item.name.trim();
|
||||||
|
const byId = BG.cachedStyles.byId.get(item.id);
|
||||||
|
const byName = oldStylesByName.get(item.name);
|
||||||
|
oldStylesByName.delete(item.name);
|
||||||
|
let oldStyle;
|
||||||
|
if (byId) {
|
||||||
|
if (sameStyle(byId, item)) {
|
||||||
|
oldStyle = byId;
|
||||||
|
} else {
|
||||||
|
item.id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oldStyle && byName) {
|
||||||
|
item.id = byName.id;
|
||||||
|
oldStyle = byName;
|
||||||
|
}
|
||||||
|
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||||
|
const metaEqual = oldStyleKeys &&
|
||||||
|
oldStyleKeys.length == Object.keys(item).length &&
|
||||||
|
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||||
|
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||||
|
if (metaEqual && codeEqual) {
|
||||||
|
stats.unchanged.names.push(oldStyle.name);
|
||||||
|
stats.unchanged.ids.push(oldStyle.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return {oldStyle, metaEqual, codeEqual};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameStyle(oldStyle, newStyle) {
|
||||||
|
return oldStyle.name.trim() === newStyle.name.trim() ||
|
||||||
|
['updateUrl', 'originalMd5', 'originalDigest']
|
||||||
|
.some(field => oldStyle[field] && oldStyle[field] == newStyle[field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function account({style, info, resolve}) {
|
||||||
|
renderQueue.push(style);
|
||||||
|
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||||
|
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||||
|
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||||
|
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||||
|
renderQueue.length = 0;
|
||||||
|
lastRenderTime = performance.now();
|
||||||
|
}
|
||||||
|
setTimeout(proceed, 0, resolve);
|
||||||
|
const {oldStyle, metaEqual, codeEqual} = info;
|
||||||
|
if (!oldStyle) {
|
||||||
|
stats.added.names.push(style.name);
|
||||||
|
stats.added.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!metaEqual && !codeEqual) {
|
||||||
|
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaAndCode.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!codeEqual) {
|
||||||
|
stats.codeOnly.names.push(style.name);
|
||||||
|
stats.codeOnly.ids.push(style.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaOnly.ids.push(style.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(resolve) {
|
||||||
|
const numChanged = stats.metaAndCode.names.length +
|
||||||
|
stats.metaOnly.names.length +
|
||||||
|
stats.codeOnly.names.length +
|
||||||
|
stats.added.names.length;
|
||||||
|
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||||
|
const report = Object.keys(stats)
|
||||||
|
.filter(kind => stats[kind].names.length)
|
||||||
|
.map(kind => {
|
||||||
|
const {ids, names, legend} = stats[kind];
|
||||||
|
const listItemsWithId = (name, i) =>
|
||||||
|
$element({dataset: {id: ids[i]}, textContent: name});
|
||||||
|
const listItems = name =>
|
||||||
|
$element({textContent: name});
|
||||||
|
const block =
|
||||||
|
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||||
|
$element({tag: 'summary', appendChild:
|
||||||
|
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||||
|
}),
|
||||||
|
$element({tag: 'small', appendChild:
|
||||||
|
names.map(ids ? listItemsWithId : listItems)
|
||||||
|
}),
|
||||||
|
]});
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
scrollTo(0, 0);
|
||||||
|
messageBox({
|
||||||
|
title: t('importReportTitle'),
|
||||||
|
contents: report.length ? report : t('importReportUnchanged'),
|
||||||
|
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||||
|
onshow: bindClick,
|
||||||
|
}).then(({button, enter, esc}) => {
|
||||||
|
if (button == 1) {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(numChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||||
|
const newIds = [
|
||||||
|
...stats.metaAndCode.ids,
|
||||||
|
...stats.metaOnly.ids,
|
||||||
|
...stats.codeOnly.ids,
|
||||||
|
...stats.added.ids,
|
||||||
|
];
|
||||||
|
let resolve;
|
||||||
|
index = 0;
|
||||||
|
return new Promise(resolve_ => {
|
||||||
|
resolve = resolve_;
|
||||||
|
undoNextId();
|
||||||
|
}).then(refreshAllTabs)
|
||||||
|
.then(() => messageBox({
|
||||||
|
title: t('importReportUndoneTitle'),
|
||||||
|
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||||
|
buttons: [t('confirmOK')],
|
||||||
|
}));
|
||||||
|
function undoNextId() {
|
||||||
|
if (index == newIds.length) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = newIds[index++];
|
||||||
|
deleteStyleSafe({id, notify: false}).then(id => {
|
||||||
|
const oldStyle = oldStylesById.get(id);
|
||||||
|
if (oldStyle) {
|
||||||
|
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||||
|
.then(undoNextId);
|
||||||
|
} else {
|
||||||
|
undoNextId();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindClick(box) {
|
||||||
|
const highlightElement = event => {
|
||||||
|
const styleElement = $('#style-' + event.target.dataset.id);
|
||||||
|
if (styleElement) {
|
||||||
|
scrollElementIntoView(styleElement);
|
||||||
|
animateElement(styleElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const block of $$('details')) {
|
||||||
|
if (block.dataset.id != 'invalid') {
|
||||||
|
block.style.cursor = 'pointer';
|
||||||
|
block.onclick = highlightElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function limitString(s, limit = 100) {
|
||||||
|
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportNameChange(oldStyle, newStyle) {
|
||||||
|
return newStyle.name != oldStyle.name
|
||||||
|
? oldStyle.name + ' —> ' + newStyle.name
|
||||||
|
: oldStyle.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAllTabs() {
|
||||||
|
return Promise.all([
|
||||||
|
getActiveTab(),
|
||||||
|
getOwnTab(),
|
||||||
|
]).then(([activeTab, ownTab]) => new Promise(resolve => {
|
||||||
|
// list all tabs including chrome-extension:// which can be ours
|
||||||
|
queryTabs().then(tabs => {
|
||||||
|
const lastTab = tabs[tabs.length - 1];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||||
|
if (FIREFOX && !tab.width) {
|
||||||
|
if (tab == lastTab) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||||
|
const message = {method: 'styleReplaceAll', styles};
|
||||||
|
if (tab.id == ownTab.id) {
|
||||||
|
applyOnMessage(message);
|
||||||
|
} else {
|
||||||
|
invokeOrPostpone(tab.id == activeTab.id,
|
||||||
|
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
|
||||||
|
}
|
||||||
|
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||||
|
if (tab == lastTab) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$('#file-all-styles').onclick = () => {
|
||||||
|
getStylesSafe().then(styles => {
|
||||||
|
const text = JSON.stringify(styles, null, '\t');
|
||||||
|
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||||
|
return url;
|
||||||
|
// for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600
|
||||||
|
}).then(fetch)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => {
|
||||||
|
const objectURL = URL.createObjectURL(blob);
|
||||||
|
let link = $element({
|
||||||
|
tag:'a',
|
||||||
|
href: objectURL,
|
||||||
|
type: 'application/json',
|
||||||
|
download: generateFileName(),
|
||||||
|
});
|
||||||
|
// TODO: remove the fallback when FF multi-process bug is fixed
|
||||||
|
if (!FIREFOX) {
|
||||||
|
link.dispatchEvent(new MouseEvent('click'));
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||||
|
} else {
|
||||||
|
const iframe = document.body.appendChild($element({
|
||||||
|
tag: 'iframe',
|
||||||
|
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
|
||||||
|
}));
|
||||||
|
doTimeout().then(() => {
|
||||||
|
link = iframe.contentDocument.importNode(link, true);
|
||||||
|
iframe.contentDocument.body.appendChild(link);
|
||||||
|
})
|
||||||
|
.then(doTimeout)
|
||||||
|
.then(() => link.dispatchEvent(new MouseEvent('click')))
|
||||||
|
.then(doTimeout(1000))
|
||||||
|
.then(() => {
|
||||||
|
URL.revokeObjectURL(objectURL);
|
||||||
|
iframe.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateFileName() {
|
||||||
|
const today = new Date();
|
||||||
|
const dd = ('0' + today.getDate()).substr(-2);
|
||||||
|
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||||
|
const yyyy = today.getFullYear();
|
||||||
|
return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
$('#unfile-all-styles').onclick = () => {
|
||||||
|
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(document.body, {
|
||||||
|
ondragover(event) {
|
||||||
|
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||||
|
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||||
|
this.classList.toggle('dropzone', hasFiles);
|
||||||
|
if (hasFiles) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(this.fadeoutTimer);
|
||||||
|
this.classList.remove('fadeout');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondragend(event) {
|
||||||
|
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||||
|
this.style.animationDuration = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ondragleave(event) {
|
||||||
|
try {
|
||||||
|
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||||
|
if (event.target === this) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondrop(event) {
|
||||||
|
this.ondragend();
|
||||||
|
if (event.dataTransfer.files.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#onlyUpdates input').checked) {
|
||||||
|
$('#onlyUpdates input').click();
|
||||||
|
}
|
||||||
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
0
pull_locales.sh → tools/pull_locales.sh
Executable file → Normal file
0
pull_locales.sh → tools/pull_locales.sh
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user