2020-11-08 10:31:07 +00:00
|
|
|
/* global
|
|
|
|
$
|
|
|
|
$$
|
|
|
|
$create
|
|
|
|
animateElement
|
|
|
|
API
|
|
|
|
bulkChangeQueue
|
|
|
|
CHROME
|
|
|
|
chromeSync
|
|
|
|
deepEqual
|
|
|
|
messageBox
|
|
|
|
onDOMready
|
|
|
|
prefs
|
|
|
|
scrollElementIntoView
|
|
|
|
styleJSONseemsValid
|
|
|
|
styleSectionsEqual
|
|
|
|
t
|
|
|
|
tryJSONparse
|
|
|
|
*/
|
2017-07-12 18:17:04 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
|
|
|
const STYLUS_BACKUP_FILE_EXT = '.json';
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
onDOMready().then(() => {
|
2020-08-24 16:27:23 +00:00
|
|
|
$('#file-all-styles').onclick = () => exportToFile();
|
|
|
|
$('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
2018-11-07 06:09:29 +00:00
|
|
|
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
},
|
2020-11-08 10:31:07 +00:00
|
|
|
ondragend() {
|
|
|
|
animateElement(this, 'fadeout', 'dropzone');
|
2018-11-07 06:09:29 +00:00
|
|
|
},
|
|
|
|
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) {
|
|
|
|
if (event.dataTransfer.files.length) {
|
|
|
|
event.preventDefault();
|
|
|
|
if ($('#only-updates input').checked) {
|
|
|
|
$('#only-updates input').click();
|
|
|
|
}
|
|
|
|
importFromFile({file: event.dataTransfer.files[0]});
|
|
|
|
}
|
2020-11-08 10:31:07 +00:00
|
|
|
/* Run import first for a while, then run fadeout which is very CPU-intensive in Chrome */
|
|
|
|
setTimeout(() => this.ondragend(), 250);
|
2018-11-07 06:09:29 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
2017-07-12 18:17:04 +00:00
|
|
|
|
|
|
|
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) {
|
2020-11-08 10:31:07 +00:00
|
|
|
messageBox.alert("100MB backup? I don't believe you.");
|
|
|
|
resolve();
|
2017-07-12 18:17:04 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const fReader = new FileReader();
|
|
|
|
fReader.onloadend = event => {
|
|
|
|
fileInput.remove();
|
2017-12-06 08:03:43 +00:00
|
|
|
const text = event.target.result;
|
2020-11-08 10:31:07 +00:00
|
|
|
const maybeUsercss = !/^\s*\[/.test(text) && /==UserStyle==/i.test(text);
|
2020-02-23 15:43:26 +00:00
|
|
|
if (maybeUsercss) {
|
|
|
|
messageBox.alert(t('dragDropUsercssTabstrip'));
|
2020-11-08 10:31:07 +00:00
|
|
|
} else {
|
|
|
|
importFromString(text).then(resolve);
|
2020-02-23 15:43:26 +00:00
|
|
|
}
|
2017-07-12 18:17:04 +00:00
|
|
|
};
|
|
|
|
fReader.readAsText(file, 'utf-8');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-11-08 10:31:07 +00:00
|
|
|
async function importFromString(jsonString) {
|
2018-11-07 06:09:29 +00:00
|
|
|
const json = tryJSONparse(jsonString);
|
2020-11-08 10:31:07 +00:00
|
|
|
const oldStyles = Array.isArray(json) && json.length ? await API.getAllStyles() : [];
|
|
|
|
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
|
|
|
const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style]));
|
|
|
|
const items = [];
|
|
|
|
const infos = [];
|
2017-07-12 18:17:04 +00:00
|
|
|
const stats = {
|
2020-11-08 10:31:07 +00:00
|
|
|
options: {names: [], isOptions: true, legend: 'optionsHeading'},
|
|
|
|
added: {names: [], ids: [], legend: 'importReportLegendAdded', dirty: true},
|
|
|
|
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
|
|
|
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth', dirty: true},
|
|
|
|
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta', dirty: true},
|
|
|
|
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode', dirty: true},
|
|
|
|
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
2017-07-12 18:17:04 +00:00
|
|
|
};
|
2020-11-08 10:31:07 +00:00
|
|
|
await Promise.all(json.map(analyze));
|
|
|
|
bulkChangeQueue.length = 0;
|
|
|
|
bulkChangeQueue.time = performance.now();
|
|
|
|
(await API.importManyStyles(items))
|
|
|
|
.forEach((style, i) => updateStats(style, infos[i]));
|
|
|
|
return done();
|
2017-07-12 18:17:04 +00:00
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function analyze(item, index) {
|
2020-11-08 10:31:07 +00:00
|
|
|
if (item && !item.id && item[prefs.STORAGE_KEY]) {
|
|
|
|
return analyzeStorage(item);
|
|
|
|
}
|
2019-04-16 12:32:06 +00:00
|
|
|
if (typeof item !== 'object' || !styleJSONseemsValid(item)) {
|
2017-07-12 18:17:04 +00:00
|
|
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
item.name = item.name.trim();
|
2018-01-01 17:02:49 +00:00
|
|
|
const byId = oldStylesById.get(item.id);
|
2017-07-12 18:17:04 +00:00
|
|
|
const byName = oldStylesByName.get(item.name);
|
|
|
|
oldStylesByName.delete(item.name);
|
|
|
|
let oldStyle;
|
|
|
|
if (byId) {
|
|
|
|
if (sameStyle(byId, item)) {
|
|
|
|
oldStyle = byId;
|
|
|
|
} else {
|
2018-12-08 03:57:43 +00:00
|
|
|
delete item.id;
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!oldStyle && byName) {
|
|
|
|
item.id = byName.id;
|
|
|
|
oldStyle = byName;
|
|
|
|
}
|
2020-11-08 10:31:07 +00:00
|
|
|
const metaEqual = oldStyle && deepEqual(oldStyle, item, ['sections', '_rev']);
|
2018-01-01 17:02:49 +00:00
|
|
|
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
2017-07-12 18:17:04 +00:00
|
|
|
if (metaEqual && codeEqual) {
|
|
|
|
stats.unchanged.names.push(oldStyle.name);
|
|
|
|
stats.unchanged.ids.push(oldStyle.id);
|
2020-11-08 10:31:07 +00:00
|
|
|
} else {
|
|
|
|
items.push(item);
|
|
|
|
infos.push({oldStyle, metaEqual, codeEqual});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function analyzeStorage(storage) {
|
|
|
|
analyzePrefs(storage[prefs.STORAGE_KEY], Object.keys(prefs.defaults), prefs.values, true);
|
|
|
|
delete storage[prefs.STORAGE_KEY];
|
|
|
|
if (Object.keys(storage).length) {
|
|
|
|
analyzePrefs(storage, Object.values(chromeSync.LZ_KEY), await chromeSync.getLZValues());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function analyzePrefs(obj, validKeys, values, isPref) {
|
|
|
|
for (const [key, val] of Object.entries(obj || {})) {
|
|
|
|
const isValid = validKeys.includes(key);
|
|
|
|
if (!isValid || !deepEqual(val, values[key])) {
|
|
|
|
stats.options.names.push({name: key, val, isValid, isPref});
|
|
|
|
}
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function sameStyle(oldStyle, newStyle) {
|
|
|
|
return oldStyle.name.trim() === newStyle.name.trim() ||
|
|
|
|
['updateUrl', 'originalMd5', 'originalDigest']
|
2017-07-16 18:02:00 +00:00
|
|
|
.some(field => oldStyle[field] && oldStyle[field] === newStyle[field]);
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function updateStats(style, {oldStyle, metaEqual, codeEqual}) {
|
2017-07-12 18:17:04 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-11-07 06:09:29 +00:00
|
|
|
function done() {
|
|
|
|
scrollTo(0, 0);
|
2020-11-08 10:31:07 +00:00
|
|
|
const entries = Object.entries(stats);
|
|
|
|
const numChanged = entries.reduce((sum, [, val]) =>
|
|
|
|
sum + (val.dirty ? val.names.length : 0), 0);
|
|
|
|
const report = entries.map(renderStats).filter(Boolean);
|
2018-11-07 06:09:29 +00:00
|
|
|
messageBox({
|
|
|
|
title: t('importReportTitle'),
|
2020-11-08 10:31:07 +00:00
|
|
|
contents: $create('#import', report.length ? report : t('importReportUnchanged')),
|
2018-11-07 06:09:29 +00:00
|
|
|
buttons: [t('confirmClose'), numChanged && t('undo')],
|
|
|
|
onshow: bindClick,
|
|
|
|
})
|
|
|
|
.then(({button}) => {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (button === 1) {
|
2017-07-12 18:17:04 +00:00
|
|
|
undo();
|
|
|
|
}
|
|
|
|
});
|
2020-11-08 10:31:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function renderStats([id, {ids, names, legend, isOptions}]) {
|
|
|
|
return names.length &&
|
|
|
|
$create('details', {dataset: {id}, open: isOptions}, [
|
|
|
|
$create('summary',
|
|
|
|
$create('b', (isOptions ? '' : names.length + ' ') + t(legend))),
|
|
|
|
$create('small',
|
|
|
|
names.map(ids ? listItemsWithId : isOptions ? listOptions : listItems, ids)),
|
|
|
|
isOptions && names.some(_ => _.isValid) &&
|
|
|
|
$create('button', {onclick: importOptions}, t('importLabel')),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
function listOptions({name, isValid}) {
|
|
|
|
return $create(isValid ? 'div' : 'del',
|
|
|
|
name + (isValid ? '' : ` (${t(stats.invalid.legend)})`));
|
|
|
|
}
|
|
|
|
|
|
|
|
function listItems(name) {
|
|
|
|
return $create('div', name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @this stats.<item>.ids */
|
|
|
|
function listItemsWithId(name, i) {
|
|
|
|
return $create('div', {dataset: {id: this[i]}}, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function importOptions() {
|
|
|
|
// Must acquire the permission before setting the pref
|
|
|
|
if (CHROME && !chrome.declarativeContent &&
|
|
|
|
stats.options.names.find(_ => _.name === 'styleViaXhr' && _.isValid && _.val)) {
|
|
|
|
await new Promise(resolve =>
|
|
|
|
chrome.permissions.request({permissions: ['declarativeContent']}, resolve));
|
|
|
|
}
|
|
|
|
const oldStorage = await chromeSync.get();
|
|
|
|
for (const {name, val, isValid, isPref} of stats.options.names) {
|
|
|
|
if (isValid) {
|
|
|
|
if (isPref) {
|
|
|
|
prefs.set(name, val);
|
|
|
|
} else {
|
|
|
|
chromeSync.setLZValue(name, val);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const label = this.textContent;
|
|
|
|
this.textContent = t('undo');
|
|
|
|
this.onclick = async () => {
|
|
|
|
const curKeys = Object.keys(await chromeSync.get());
|
|
|
|
const keysToRemove = curKeys.filter(k => !oldStorage.hasOwnProperty(k));
|
|
|
|
await chromeSync.set(oldStorage);
|
|
|
|
await chromeSync.remove(keysToRemove);
|
|
|
|
this.textContent = label;
|
|
|
|
this.onclick = importOptions;
|
|
|
|
};
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function undo() {
|
|
|
|
const newIds = [
|
|
|
|
...stats.metaAndCode.ids,
|
|
|
|
...stats.metaOnly.ids,
|
|
|
|
...stats.codeOnly.ids,
|
|
|
|
...stats.added.ids,
|
|
|
|
];
|
2018-01-03 15:26:31 +00:00
|
|
|
let tasks = Promise.resolve();
|
|
|
|
for (const id of newIds) {
|
2018-11-07 06:09:29 +00:00
|
|
|
tasks = tasks.then(() => API.deleteStyle(id));
|
2018-01-03 15:26:31 +00:00
|
|
|
const oldStyle = oldStylesById.get(id);
|
|
|
|
if (oldStyle) {
|
2018-11-07 06:09:29 +00:00
|
|
|
tasks = tasks.then(() => API.importStyle(oldStyle));
|
2018-01-03 15:26:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// taskUI is superfast and updates style list only in this page,
|
|
|
|
// which should account for 99.99999999% of cases, supposedly
|
2018-11-07 06:09:29 +00:00
|
|
|
return tasks.then(() => messageBox({
|
|
|
|
title: t('importReportUndoneTitle'),
|
|
|
|
contents: newIds.length + ' ' + t('importReportUndone'),
|
|
|
|
buttons: [t('confirmClose')],
|
|
|
|
}));
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
2017-07-16 19:40:13 +00:00
|
|
|
function bindClick() {
|
2017-07-12 18:17:04 +00:00
|
|
|
const highlightElement = event => {
|
|
|
|
const styleElement = $('#style-' + event.target.dataset.id);
|
|
|
|
if (styleElement) {
|
|
|
|
scrollElementIntoView(styleElement);
|
|
|
|
animateElement(styleElement);
|
|
|
|
}
|
|
|
|
};
|
2017-12-11 05:44:41 +00:00
|
|
|
for (const block of $$('#message-box details')) {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (block.dataset.id !== 'invalid') {
|
2017-07-12 18:17:04 +00:00
|
|
|
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) {
|
2017-07-16 18:02:00 +00:00
|
|
|
return newStyle.name !== oldStyle.name
|
2017-07-12 18:17:04 +00:00
|
|
|
? oldStyle.name + ' —> ' + newStyle.name
|
|
|
|
: oldStyle.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-11-08 10:31:07 +00:00
|
|
|
async function exportToFile() {
|
|
|
|
const data = [
|
|
|
|
Object.assign({
|
|
|
|
[prefs.STORAGE_KEY]: prefs.values,
|
|
|
|
}, await chromeSync.getLZValues()),
|
|
|
|
...await API.getAllStyles(),
|
|
|
|
];
|
|
|
|
const text = JSON.stringify(data, null, ' ');
|
|
|
|
const type = 'application/json';
|
|
|
|
$create('a', {
|
|
|
|
href: URL.createObjectURL(new Blob([text], {type})),
|
|
|
|
download: generateFileName(),
|
|
|
|
type,
|
|
|
|
}).dispatchEvent(new MouseEvent('click'));
|
2017-07-12 18:17:04 +00:00
|
|
|
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}`;
|
|
|
|
}
|
2018-11-07 06:09:29 +00:00
|
|
|
}
|