Restructure folders

This commit is contained in:
Rob Garrison 2017-07-12 13:17:04 -05:00
parent ed83f8f77e
commit 7a9d629ec8
111 changed files with 1524 additions and 1129 deletions

View File

@ -1,3 +1,2 @@
beautify/ vendor/
codemirror/ vendor-overwrites/
csslint/

View File

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

View File

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

View File

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

View File

791
manage/fileSaveLoad.js Normal file
View 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
View File

Some files were not shown because too many files have changed in this diff Show More