Merge pull request #1101 from tophf/import-prefs
import/export options in backup json
This commit is contained in:
commit
dc4819e7d0
|
@ -59,7 +59,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
parseCss({code}) {
|
parseCss({code}) {
|
||||||
return backgroundWorker.parseMozFormat({code});
|
return backgroundWorker.parseMozFormat({code});
|
||||||
},
|
},
|
||||||
getPrefs: () => prefs.values, // will be deepCopy'd by invokeAPI handler
|
getPrefs: () => prefs.values,
|
||||||
setPref: (key, value) => prefs.set(key, value),
|
setPref: (key, value) => prefs.set(key, value),
|
||||||
|
|
||||||
openEditor,
|
openEditor,
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
/* global styleSectionsEqual prefs download tryJSONparse ignoreChromeError
|
/* global
|
||||||
calcStyleDigest getStyleWithNoCode debounce chromeLocal
|
API_METHODS
|
||||||
usercss semverCompare styleJSONseemsValid
|
calcStyleDigest
|
||||||
API_METHODS styleManager */
|
chromeLocal
|
||||||
|
debounce
|
||||||
|
download
|
||||||
|
getStyleWithNoCode
|
||||||
|
ignoreChromeError
|
||||||
|
prefs
|
||||||
|
semverCompare
|
||||||
|
styleJSONseemsValid
|
||||||
|
styleManager
|
||||||
|
tryJSONparse
|
||||||
|
usercss
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -208,7 +219,7 @@
|
||||||
delete json.enabled;
|
delete json.enabled;
|
||||||
|
|
||||||
const newStyle = Object.assign({}, style, json);
|
const newStyle = Object.assign({}, style, json);
|
||||||
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
if (json.sourceCode === style.sourceCode) {
|
||||||
// update digest even if save === false as there might be just a space added etc.
|
// update digest even if save === false as there might be just a space added etc.
|
||||||
return styleManager.installStyle(newStyle)
|
return styleManager.installStyle(newStyle)
|
||||||
.then(saved => {
|
.then(saved => {
|
||||||
|
|
|
@ -264,40 +264,31 @@
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sections are checked in successive order because it matters when many sections
|
||||||
|
* match the same URL and they have rules with the same CSS specificity
|
||||||
|
* @param {Object} a - first style object
|
||||||
|
* @param {Object} b - second style object
|
||||||
|
* @returns {?boolean}
|
||||||
|
*/
|
||||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
if (!a || !b) {
|
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
||||||
return undefined;
|
return a && b && a.length === b.length && a.every(sameSection);
|
||||||
|
function sameSection(secA, i) {
|
||||||
|
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
|
||||||
|
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
|
||||||
}
|
}
|
||||||
if (a.length !== b.length) {
|
function equalOrEmpty(a, b, type, comparator) {
|
||||||
return false;
|
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
|
||||||
|
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
|
||||||
|
return typeA && typeB && comparator(a, b) ||
|
||||||
|
(a == null || typeA && !a.length) &&
|
||||||
|
(b == null || typeB && !b.length);
|
||||||
}
|
}
|
||||||
// order of sections should be identical to account for the case of multiple
|
function arrayMirrors(a, b) {
|
||||||
// sections matching the same URL because the order of rules is part of cascading
|
return a.length === b.length &&
|
||||||
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
a.every(el => b.includes(el)) &&
|
||||||
|
b.every(el => a.includes(el));
|
||||||
function propertiesEqual(secA, secB) {
|
|
||||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
|
||||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function equalOrEmpty(a, b, telltale, comparator) {
|
|
||||||
const typeA = a && typeof a[telltale] === 'function';
|
|
||||||
const typeB = b && typeof b[telltale] === 'function';
|
|
||||||
return (
|
|
||||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
|
||||||
(b === null || b === undefined || (typeB && !b.length))
|
|
||||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayMirrors(array1, array2) {
|
|
||||||
return (
|
|
||||||
array1.every(el => array2.includes(el)) &&
|
|
||||||
array2.every(el => array1.includes(el))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
if (!linter) {
|
if (!linter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const storageName = linter === 'stylelint' ? 'editorStylelintConfig' : 'editorCSSLintConfig';
|
const storageName = chromeSync.LZ_KEY[linter];
|
||||||
const getRules = memoize(linter === 'stylelint' ?
|
const getRules = memoize(linter === 'stylelint' ?
|
||||||
editorWorker.getStylelintRules : editorWorker.getCsslintRules);
|
editorWorker.getStylelintRules : editorWorker.getCsslintRules);
|
||||||
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
(() => {
|
(() => {
|
||||||
registerLinters({
|
registerLinters({
|
||||||
csslint: {
|
csslint: {
|
||||||
storageName: 'editorCSSLintConfig',
|
storageName: chromeSync.LZ_KEY.csslint,
|
||||||
lint: csslint,
|
lint: csslint,
|
||||||
validMode: mode => mode === 'css',
|
validMode: mode => mode === 'css',
|
||||||
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config)
|
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config)
|
||||||
},
|
},
|
||||||
stylelint: {
|
stylelint: {
|
||||||
storageName: 'editorStylelintConfig',
|
storageName: chromeSync.LZ_KEY.stylelint,
|
||||||
lint: stylelint,
|
lint: stylelint,
|
||||||
validMode: () => true,
|
validMode: () => true,
|
||||||
getConfig: config => ({
|
getConfig: config => ({
|
||||||
|
|
|
@ -157,7 +157,7 @@ function SourceEditor() {
|
||||||
style.sourceCode = '';
|
style.sourceCode = '';
|
||||||
|
|
||||||
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
|
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
|
||||||
let code = await chromeSync.getLZValue('usercssTemplate');
|
let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
|
||||||
code = code || DEFAULT_CODE;
|
code = code || DEFAULT_CODE;
|
||||||
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
|
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
|
||||||
`${str}${space ? '' : ' '}${placeholderName}`);
|
`${str}${space ? '' : ' '}${placeholderName}`);
|
||||||
|
@ -247,9 +247,10 @@ function SourceEditor() {
|
||||||
|
|
||||||
// save template
|
// save template
|
||||||
if (err.code === 'missingValue' && meta.includes('@name')) {
|
if (err.code === 'missingValue' && meta.includes('@name')) {
|
||||||
|
const key = chromeSync.LZ_KEY.usercssTemplate;
|
||||||
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||||
chromeSync.setLZValue('usercssTemplate', code)
|
chromeSync.setLZValue(key, code)
|
||||||
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
.then(() => chromeSync.getLZValue(key))
|
||||||
.then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving'))));
|
.then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving'))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,9 +135,12 @@ window.INJECTED !== 1 && (() => {
|
||||||
|
|
||||||
// This direct assignment allows IDEs to provide correct autocomplete for methods
|
// This direct assignment allows IDEs to provide correct autocomplete for methods
|
||||||
const prefs = window.prefs = {
|
const prefs = window.prefs = {
|
||||||
|
STORAGE_KEY,
|
||||||
initializing,
|
initializing,
|
||||||
defaults,
|
defaults,
|
||||||
values,
|
get values() {
|
||||||
|
return deepCopy(values);
|
||||||
|
},
|
||||||
get(key) {
|
get(key) {
|
||||||
return isKnown(key) && values[key];
|
return isKnown(key) && values[key];
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,57 +23,30 @@ function styleSectionGlobal(section) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Style} a - first style object
|
* The sections are checked in successive order because it matters when many sections
|
||||||
* @param {Style} b - second style object
|
* match the same URL and they have rules with the same CSS specificity
|
||||||
* @param {Object} options
|
* @param {Object} a - first style object
|
||||||
* @param {Boolean=} options.ignoreCode -
|
* @param {Object} b - second style object
|
||||||
* true used by invalidateCache to determine if cached filters should be cleared
|
* @returns {?boolean}
|
||||||
* @param {Boolean=} options.checkSource -
|
|
||||||
* true used by update check to compare the server response
|
|
||||||
* instead of sections that depend on @preprocessor
|
|
||||||
* @returns {Boolean|undefined}
|
|
||||||
*/
|
*/
|
||||||
function styleSectionsEqual(a, b, {ignoreCode, checkSource} = {}) {
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||||
if (checkSource &&
|
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
|
||||||
typeof a.sourceCode === 'string' &&
|
return a && b && a.length === b.length && a.every(sameSection);
|
||||||
typeof b.sourceCode === 'string') {
|
function sameSection(secA, i) {
|
||||||
return a.sourceCode === b.sourceCode;
|
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
|
||||||
|
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
|
||||||
}
|
}
|
||||||
a = a.sections;
|
function equalOrEmpty(a, b, type, comparator) {
|
||||||
b = b.sections;
|
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
|
||||||
if (!a || !b) {
|
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
|
||||||
return undefined;
|
return typeA && typeB && comparator(a, b) ||
|
||||||
|
(a == null || typeA && !a.length) &&
|
||||||
|
(b == null || typeB && !b.length);
|
||||||
}
|
}
|
||||||
if (a.length !== b.length) {
|
function arrayMirrors(a, b) {
|
||||||
return false;
|
return a.length === b.length &&
|
||||||
}
|
a.every(el => b.includes(el)) &&
|
||||||
// order of sections should be identical to account for the case of multiple
|
b.every(el => a.includes(el));
|
||||||
// sections matching the same URL because the order of rules is part of cascading
|
|
||||||
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
|
||||||
|
|
||||||
function propertiesEqual(secA, secB) {
|
|
||||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
|
||||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ignoreCode || equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function equalOrEmpty(a, b, telltale, comparator) {
|
|
||||||
const typeA = a && typeof a[telltale] === 'function';
|
|
||||||
const typeB = b && typeof b[telltale] === 'function';
|
|
||||||
return (
|
|
||||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
|
||||||
(b === null || b === undefined || (typeB && !b.length))
|
|
||||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function arrayMirrors(array1, array2) {
|
|
||||||
return (
|
|
||||||
array1.every(el => array2.includes(el)) &&
|
|
||||||
array2.every(el => array1.includes(el))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const [chromeLocal, chromeSync] = (() => {
|
||||||
setValue: (key, value) => wrapper.set({[key]: value}),
|
setValue: (key, value) => wrapper.set({[key]: value}),
|
||||||
|
|
||||||
getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]),
|
getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]),
|
||||||
getLZValues: keys =>
|
getLZValues: (keys = Object.values(wrapper.LZ_KEY)) =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
wrapper.get(keys),
|
wrapper.get(keys),
|
||||||
loadLZStringScript(),
|
loadLZStringScript(),
|
||||||
|
@ -64,3 +64,9 @@ const [chromeLocal, chromeSync] = (() => {
|
||||||
(window.LZString = window.LZString || window.LZStringUnsafe));
|
(window.LZString = window.LZString || window.LZStringUnsafe));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
chromeSync.LZ_KEY = {
|
||||||
|
csslint: 'editorCSSLintConfig',
|
||||||
|
stylelint: 'editorStylelintConfig',
|
||||||
|
usercssTemplate: 'usercssTemplate',
|
||||||
|
};
|
||||||
|
|
|
@ -169,6 +169,7 @@
|
||||||
<script src="msgbox/msgbox.js"></script>
|
<script src="msgbox/msgbox.js"></script>
|
||||||
<script src="js/sections-util.js"></script>
|
<script src="js/sections-util.js"></script>
|
||||||
<script src="js/storage-util.js"></script>
|
<script src="js/storage-util.js"></script>
|
||||||
|
<script src="js/script-loader.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
/* global messageBox styleSectionsEqual API onDOMready
|
/* global
|
||||||
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
|
$
|
||||||
styleJSONseemsValid bulkChangeQueue */
|
$$
|
||||||
|
$create
|
||||||
|
animateElement
|
||||||
|
API
|
||||||
|
bulkChangeQueue
|
||||||
|
CHROME
|
||||||
|
chromeSync
|
||||||
|
deepEqual
|
||||||
|
messageBox
|
||||||
|
onDOMready
|
||||||
|
prefs
|
||||||
|
scrollElementIntoView
|
||||||
|
styleJSONseemsValid
|
||||||
|
styleSectionsEqual
|
||||||
|
t
|
||||||
|
tryJSONparse
|
||||||
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
|
@ -21,9 +37,8 @@ onDOMready().then(() => {
|
||||||
this.classList.remove('fadeout');
|
this.classList.remove('fadeout');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async ondragend() {
|
ondragend() {
|
||||||
await animateElement(this, 'fadeout', 'dropzone');
|
animateElement(this, 'fadeout', 'dropzone');
|
||||||
this.style.animationDuration = '';
|
|
||||||
},
|
},
|
||||||
ondragleave(event) {
|
ondragleave(event) {
|
||||||
try {
|
try {
|
||||||
|
@ -36,7 +51,6 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ondrop(event) {
|
ondrop(event) {
|
||||||
this.ondragend();
|
|
||||||
if (event.dataTransfer.files.length) {
|
if (event.dataTransfer.files.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ($('#only-updates input').checked) {
|
if ($('#only-updates input').checked) {
|
||||||
|
@ -44,6 +58,8 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
importFromFile({file: event.dataTransfer.files[0]});
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
}
|
}
|
||||||
|
/* Run import first for a while, then run fadeout which is very CPU-intensive in Chrome */
|
||||||
|
setTimeout(() => this.ondragend(), 250);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -69,25 +85,20 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
if (file || fileInput.value !== fileInput.initialValue) {
|
if (file || fileInput.value !== fileInput.initialValue) {
|
||||||
file = file || fileInput.files[0];
|
file = file || fileInput.files[0];
|
||||||
if (file.size > 100e6) {
|
if (file.size > 100e6) {
|
||||||
console.warn("100MB backup? I don't believe you.");
|
messageBox.alert("100MB backup? I don't believe you.");
|
||||||
importFromString('').then(resolve);
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
document.body.style.cursor = 'wait';
|
|
||||||
const fReader = new FileReader();
|
const fReader = new FileReader();
|
||||||
fReader.onloadend = event => {
|
fReader.onloadend = event => {
|
||||||
fileInput.remove();
|
fileInput.remove();
|
||||||
const text = event.target.result;
|
const text = event.target.result;
|
||||||
const maybeUsercss = !/^[\s\r\n]*\[/.test(text) &&
|
const maybeUsercss = !/^\s*\[/.test(text) && /==UserStyle==/i.test(text);
|
||||||
(text.includes('==UserStyle==') || /==UserStyle==/i.test(text));
|
|
||||||
if (maybeUsercss) {
|
if (maybeUsercss) {
|
||||||
messageBox.alert(t('dragDropUsercssTabstrip'));
|
messageBox.alert(t('dragDropUsercssTabstrip'));
|
||||||
return;
|
} else {
|
||||||
|
importFromString(text).then(resolve);
|
||||||
}
|
}
|
||||||
importFromString(text).then(numStyles => {
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
resolve(numStyles);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
fReader.readAsText(file, 'utf-8');
|
fReader.readAsText(file, 'utf-8');
|
||||||
}
|
}
|
||||||
|
@ -96,51 +107,33 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function importFromString(jsonString) {
|
async function importFromString(jsonString) {
|
||||||
const json = tryJSONparse(jsonString);
|
const json = tryJSONparse(jsonString);
|
||||||
if (!Array.isArray(json)) {
|
const oldStyles = Array.isArray(json) && json.length ? await API.getAllStyles() : [];
|
||||||
return Promise.reject(new Error('the backup is not a valid JSON file'));
|
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||||
}
|
const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style]));
|
||||||
let oldStyles;
|
const items = [];
|
||||||
let oldStylesById;
|
const infos = [];
|
||||||
let oldStylesByName;
|
|
||||||
const stats = {
|
const stats = {
|
||||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
options: {names: [], isOptions: true, legend: 'optionsHeading'},
|
||||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
added: {names: [], ids: [], legend: 'importReportLegendAdded', dirty: true},
|
||||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth', dirty: true},
|
||||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta', dirty: true},
|
||||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode', dirty: true},
|
||||||
|
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||||
};
|
};
|
||||||
|
await Promise.all(json.map(analyze));
|
||||||
return API.getAllStyles().then(styles => {
|
bulkChangeQueue.length = 0;
|
||||||
// make a copy of the current database, that may be used when we want to
|
bulkChangeQueue.time = performance.now();
|
||||||
// undo
|
(await API.importManyStyles(items))
|
||||||
oldStyles = styles;
|
.forEach((style, i) => updateStats(style, infos[i]));
|
||||||
oldStylesById = new Map(
|
return done();
|
||||||
oldStyles.map(style => [style.id, style]));
|
|
||||||
oldStylesByName = json.length && new Map(
|
|
||||||
oldStyles.map(style => [style.name.trim(), style]));
|
|
||||||
|
|
||||||
const items = [];
|
|
||||||
json.forEach((item, i) => {
|
|
||||||
const info = analyze(item, i);
|
|
||||||
if (info) {
|
|
||||||
items.push({info, item});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bulkChangeQueue.length = 0;
|
|
||||||
bulkChangeQueue.time = performance.now();
|
|
||||||
return API.importManyStyles(items.map(i => i.item))
|
|
||||||
.then(styles => {
|
|
||||||
for (let i = 0; i < styles.length; i++) {
|
|
||||||
updateStats(styles[i], items[i].info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(done);
|
|
||||||
|
|
||||||
function analyze(item, index) {
|
function analyze(item, index) {
|
||||||
|
if (item && !item.id && item[prefs.STORAGE_KEY]) {
|
||||||
|
return analyzeStorage(item);
|
||||||
|
}
|
||||||
if (typeof item !== 'object' || !styleJSONseemsValid(item)) {
|
if (typeof item !== 'object' || !styleJSONseemsValid(item)) {
|
||||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||||
return;
|
return;
|
||||||
|
@ -161,17 +154,32 @@ function importFromString(jsonString) {
|
||||||
item.id = byName.id;
|
item.id = byName.id;
|
||||||
oldStyle = byName;
|
oldStyle = byName;
|
||||||
}
|
}
|
||||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
const metaEqual = oldStyle && deepEqual(oldStyle, item, ['sections', '_rev']);
|
||||||
const metaEqual = oldStyleKeys &&
|
|
||||||
oldStyleKeys.length === Object.keys(item).length &&
|
|
||||||
oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]);
|
|
||||||
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
||||||
if (metaEqual && codeEqual) {
|
if (metaEqual && codeEqual) {
|
||||||
stats.unchanged.names.push(oldStyle.name);
|
stats.unchanged.names.push(oldStyle.name);
|
||||||
stats.unchanged.ids.push(oldStyle.id);
|
stats.unchanged.ids.push(oldStyle.id);
|
||||||
return;
|
} 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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {oldStyle, metaEqual, codeEqual};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameStyle(oldStyle, newStyle) {
|
function sameStyle(oldStyle, newStyle) {
|
||||||
|
@ -201,31 +209,14 @@ function importFromString(jsonString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function done() {
|
function done() {
|
||||||
const numChanged = stats.metaAndCode.names.length +
|
|
||||||
stats.metaOnly.names.length +
|
|
||||||
stats.codeOnly.names.length +
|
|
||||||
stats.added.names.length;
|
|
||||||
const report = Object.keys(stats)
|
|
||||||
.filter(kind => stats[kind].names.length)
|
|
||||||
.map(kind => {
|
|
||||||
const {ids, names, legend} = stats[kind];
|
|
||||||
const listItemsWithId = (name, i) =>
|
|
||||||
$create('div', {dataset: {id: ids[i]}}, name);
|
|
||||||
const listItems = name =>
|
|
||||||
$create('div', name);
|
|
||||||
const block =
|
|
||||||
$create('details', {dataset: {id: kind}}, [
|
|
||||||
$create('summary',
|
|
||||||
$create('b', names.length + ' ' + t(legend))),
|
|
||||||
$create('small',
|
|
||||||
names.map(ids ? listItemsWithId : listItems)),
|
|
||||||
]);
|
|
||||||
return block;
|
|
||||||
});
|
|
||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
|
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);
|
||||||
messageBox({
|
messageBox({
|
||||||
title: t('importReportTitle'),
|
title: t('importReportTitle'),
|
||||||
contents: report.length ? report : t('importReportUnchanged'),
|
contents: $create('#import', report.length ? report : t('importReportUnchanged')),
|
||||||
buttons: [t('confirmClose'), numChanged && t('undo')],
|
buttons: [t('confirmClose'), numChanged && t('undo')],
|
||||||
onshow: bindClick,
|
onshow: bindClick,
|
||||||
})
|
})
|
||||||
|
@ -234,7 +225,61 @@ function importFromString(jsonString) {
|
||||||
undo();
|
undo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Promise.resolve(numChanged);
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
|
@ -289,39 +334,20 @@ function importFromString(jsonString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function exportToFile() {
|
async function exportToFile() {
|
||||||
API.getAllStyles().then(styles => {
|
const data = [
|
||||||
// https://crbug.com/714373
|
Object.assign({
|
||||||
document.documentElement.appendChild(
|
[prefs.STORAGE_KEY]: prefs.values,
|
||||||
$create('iframe', {
|
}, await chromeSync.getLZValues()),
|
||||||
onload() {
|
...await API.getAllStyles(),
|
||||||
const text = JSON.stringify(styles, null, '\t');
|
];
|
||||||
const type = 'application/json';
|
const text = JSON.stringify(data, null, ' ');
|
||||||
this.onload = null;
|
const type = 'application/json';
|
||||||
this.contentDocument.body.appendChild(
|
$create('a', {
|
||||||
$create('a', {
|
href: URL.createObjectURL(new Blob([text], {type})),
|
||||||
href: URL.createObjectURL(new Blob([text], {type})),
|
download: generateFileName(),
|
||||||
download: generateFileName(),
|
type,
|
||||||
type,
|
}).dispatchEvent(new MouseEvent('click'));
|
||||||
})
|
|
||||||
).dispatchEvent(new MouseEvent('click'));
|
|
||||||
},
|
|
||||||
// we can't use display:none as some browsers are ignoring such iframes
|
|
||||||
style: `
|
|
||||||
all: unset;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: fixed;
|
|
||||||
opacity: 0;
|
|
||||||
border: none;
|
|
||||||
`.replace(/;/g, '!important;'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// we don't remove the iframe or the object URL because the browser may show
|
|
||||||
// a download dialog and we don't know how long it'll take until the user confirms it
|
|
||||||
// (some browsers like Vivaldi can't download if we revoke the URL)
|
|
||||||
});
|
|
||||||
|
|
||||||
function generateFileName() {
|
function generateFileName() {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dd = ('0' + today.getDate()).substr(-2);
|
const dd = ('0' + today.getDate()).substr(-2);
|
||||||
|
|
|
@ -913,22 +913,6 @@ a:hover {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#import ul {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#import li {
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#import pre {
|
|
||||||
background: #eee;
|
|
||||||
overflow: auto;
|
|
||||||
margin: 0 0 .5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* drag-n-drop on import button */
|
/* drag-n-drop on import button */
|
||||||
.dropzone:after {
|
.dropzone:after {
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
@ -954,18 +938,22 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* post-import report */
|
/* post-import report */
|
||||||
#message-box details:not([data-id="invalid"]) div:hover {
|
#import details:not([data-id="invalid"]) div:hover {
|
||||||
background-color: rgba(128, 128, 128, .3);
|
background-color: rgba(128, 128, 128, .3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-box details:not(:last-child) {
|
#import details:not(:last-child) {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-box details small div {
|
#import details small > * {
|
||||||
margin-left: 1.5em;
|
margin-left: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#import details > button {
|
||||||
|
margin: .5em 1.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.update-history-log {
|
.update-history-log {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
|
@ -38,25 +38,17 @@ if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CHROME) {
|
if (CHROME && !chrome.declarativeContent) {
|
||||||
// Show the option as disabled until the permission is actually granted
|
// Show the option as disabled until the permission is actually granted
|
||||||
const el = $('#styleViaXhr');
|
const el = $('#styleViaXhr');
|
||||||
|
prefs.initializing.then(() => {
|
||||||
|
el.checked = false;
|
||||||
|
});
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
if (el.checked && !chrome.declarativeContent) {
|
if (el.checked) {
|
||||||
chrome.permissions.request({permissions: ['declarativeContent']}, ok => {
|
chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError);
|
||||||
if (chrome.runtime.lastError || !ok) {
|
|
||||||
el.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!chrome.declarativeContent) {
|
|
||||||
prefs.initializing.then(() => {
|
|
||||||
if (prefs.get('styleViaXhr')) {
|
|
||||||
el.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
|
|
Loading…
Reference in New Issue
Block a user