diff --git a/edit/linter-config-dialog.js b/edit/linter-config-dialog.js index c570963a..d357cb79 100644 --- a/edit/linter-config-dialog.js +++ b/edit/linter-config-dialog.js @@ -27,7 +27,7 @@ if (!linter) { return; } - const storageName = linter === 'stylelint' ? 'editorStylelintConfig' : 'editorCSSLintConfig'; + const storageName = chromeSync.LZ_KEY[linter]; const getRules = memoize(linter === 'stylelint' ? editorWorker.getStylelintRules : editorWorker.getCsslintRules); const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint'; diff --git a/edit/linter-engines.js b/edit/linter-engines.js index 65755434..8ec09144 100644 --- a/edit/linter-engines.js +++ b/edit/linter-engines.js @@ -4,13 +4,13 @@ (() => { registerLinters({ csslint: { - storageName: 'editorCSSLintConfig', + storageName: chromeSync.LZ_KEY.csslint, lint: csslint, validMode: mode => mode === 'css', getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config) }, stylelint: { - storageName: 'editorStylelintConfig', + storageName: chromeSync.LZ_KEY.stylelint, lint: stylelint, validMode: () => true, getConfig: config => ({ diff --git a/edit/source-editor.js b/edit/source-editor.js index 2e2d5be3..ca8cd183 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -157,7 +157,7 @@ function SourceEditor() { style.sourceCode = ''; 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.replace(/@name(\s*)(?=[\r\n])/, (str, space) => `${str}${space ? '' : ' '}${placeholderName}`); @@ -247,9 +247,10 @@ function SourceEditor() { // save template if (err.code === 'missingValue' && meta.includes('@name')) { + const key = chromeSync.LZ_KEY.usercssTemplate; messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok && - chromeSync.setLZValue('usercssTemplate', code) - .then(() => chromeSync.getLZValue('usercssTemplate')) + chromeSync.setLZValue(key, code) + .then(() => chromeSync.getLZValue(key)) .then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving')))); return; } diff --git a/js/prefs.js b/js/prefs.js index cba6e0d9..dc7e812a 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -135,6 +135,7 @@ window.INJECTED !== 1 && (() => { // This direct assignment allows IDEs to provide correct autocomplete for methods const prefs = window.prefs = { + STORAGE_KEY, initializing, defaults, get values() { diff --git a/js/storage-util.js b/js/storage-util.js index 2e966523..42e2931a 100644 --- a/js/storage-util.js +++ b/js/storage-util.js @@ -35,7 +35,7 @@ const [chromeLocal, chromeSync] = (() => { setValue: (key, value) => wrapper.set({[key]: value}), getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]), - getLZValues: keys => + getLZValues: (keys = Object.values(wrapper.LZ_KEY)) => Promise.all([ wrapper.get(keys), loadLZStringScript(), @@ -64,3 +64,9 @@ const [chromeLocal, chromeSync] = (() => { (window.LZString = window.LZString || window.LZStringUnsafe)); } })(); + +chromeSync.LZ_KEY = { + csslint: 'editorCSSLintConfig', + stylelint: 'editorStylelintConfig', + usercssTemplate: 'usercssTemplate', +}; diff --git a/manage.html b/manage.html index 28b885f5..7f57c575 100644 --- a/manage.html +++ b/manage.html @@ -169,6 +169,7 @@ +
diff --git a/manage/import-export.js b/manage/import-export.js index 5c19b140..7c29d53c 100644 --- a/manage/import-export.js +++ b/manage/import-export.js @@ -1,6 +1,22 @@ -/* global messageBox styleSectionsEqual API onDOMready - tryJSONparse scrollElementIntoView $ $$ API $create t animateElement - styleJSONseemsValid bulkChangeQueue */ +/* global + $ + $$ + $create + animateElement + API + bulkChangeQueue + CHROME + chromeSync + deepEqual + messageBox + onDOMready + prefs + scrollElementIntoView + styleJSONseemsValid + styleSectionsEqual + t + tryJSONparse +*/ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -21,9 +37,8 @@ onDOMready().then(() => { this.classList.remove('fadeout'); } }, - async ondragend() { - await animateElement(this, 'fadeout', 'dropzone'); - this.style.animationDuration = ''; + ondragend() { + animateElement(this, 'fadeout', 'dropzone'); }, ondragleave(event) { try { @@ -36,7 +51,6 @@ onDOMready().then(() => { } }, ondrop(event) { - this.ondragend(); if (event.dataTransfer.files.length) { event.preventDefault(); if ($('#only-updates input').checked) { @@ -44,6 +58,8 @@ onDOMready().then(() => { } 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) { file = file || fileInput.files[0]; if (file.size > 100e6) { - console.warn("100MB backup? I don't believe you."); - importFromString('').then(resolve); + messageBox.alert("100MB backup? I don't believe you."); + resolve(); return; } - document.body.style.cursor = 'wait'; const fReader = new FileReader(); fReader.onloadend = event => { fileInput.remove(); const text = event.target.result; - const maybeUsercss = !/^[\s\r\n]*\[/.test(text) && - (text.includes('==UserStyle==') || /==UserStyle==/i.test(text)); + const maybeUsercss = !/^\s*\[/.test(text) && /==UserStyle==/i.test(text); if (maybeUsercss) { 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'); } @@ -96,51 +107,33 @@ function importFromFile({fileTypeFilter, file} = {}) { } -function importFromString(jsonString) { +async function importFromString(jsonString) { const json = tryJSONparse(jsonString); - if (!Array.isArray(json)) { - return Promise.reject(new Error('the backup is not a valid JSON file')); - } - let oldStyles; - let oldStylesById; - let oldStylesByName; + 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 = []; 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'}, + 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'}, }; - - return API.getAllStyles().then(styles => { - // make a copy of the current database, that may be used when we want to - // undo - oldStyles = styles; - oldStylesById = new Map( - 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); + 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(); function analyze(item, index) { + if (item && !item.id && item[prefs.STORAGE_KEY]) { + return analyzeStorage(item); + } if (typeof item !== 'object' || !styleJSONseemsValid(item)) { stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); return; @@ -161,17 +154,32 @@ function importFromString(jsonString) { 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 metaEqual = oldStyle && deepEqual(oldStyle, item, ['sections', '_rev']); const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item); if (metaEqual && codeEqual) { stats.unchanged.names.push(oldStyle.name); 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) { @@ -201,31 +209,14 @@ function importFromString(jsonString) { } 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); + 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({ title: t('importReportTitle'), - contents: report.length ? report : t('importReportUnchanged'), + contents: $create('#import', report.length ? report : t('importReportUnchanged')), buttons: [t('confirmClose'), numChanged && t('undo')], onshow: bindClick, }) @@ -234,7 +225,61 @@ function importFromString(jsonString) { 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.