From 0076dd538d62ebb773a102fb60916ccd65e65f25 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 3 Apr 2023 12:34:56 +0800 Subject: [PATCH 01/18] Add import/export functionality --- index.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 12 +++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index 850a8ec..8d98cd5 100644 --- a/index.ts +++ b/index.ts @@ -172,6 +172,9 @@ class OptionsSync { this._form.addEventListener('submit', this._handleFormSubmit); chrome.storage.onChanged.addListener(this._handleStorageChangeOnForm); this._updateForm(this._form, await this.getAll()); + + this._form.querySelector('.js-export')?.addEventListener('click', this.exportToFile); + this._form.querySelector('.js-import')?.addEventListener('click', this.importFromFile); } /** @@ -181,11 +184,67 @@ class OptionsSync { if (this._form) { this._form.removeEventListener('input', this._handleFormInput); this._form.removeEventListener('submit', this._handleFormSubmit); + this._form.querySelector('.js-export')?.addEventListener('click', this.exportToFile); + this._form.querySelector('.js-import')?.addEventListener('click', this.importFromFile); chrome.storage.onChanged.removeListener(this._handleStorageChangeOnForm); delete this._form; } } + private get _jsonIdentityHelper() { + return '__optionsForExtension'; + } + + /** + Opens the browser’s file picker to import options from a previously-saved JSON file + */ + importFromFile = async (): Promise => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.addEventListener('change', this._handleImportFile, {once: true}); + input.click(); + }; + + /** + Opens the browser’s "save file" dialog to export options to a JSON file + */ + exportToFile = async (): Promise => { + const {name} = chrome.runtime.getManifest(); + const options = { + [this._jsonIdentityHelper]: name, + ...await this.getAll(), + }; + + const text = JSON.stringify(options, null, '\t'); + const url = URL.createObjectURL(new Blob([text], {type: 'application/json'})); + const link = document.createElement('a'); + link.download = name + ' options.json'; + link.href = url; + link.click(); + }; + + private readonly _handleImportFile = async (event: Event): Promise => { + const file = (event.target as HTMLInputElement).files![0]; + if (!file) { + return; + } + + const text = await file.text(); + try { + JSON.parse(text); + } catch { + // eslint-disable-next-line no-alert + alert('The file is not a valid JSON file.'); + return; + } + + const options = JSON.parse(text) as UserOptions; + delete options[this._jsonIdentityHelper]; + + await this.setAll(options); + }; + private _log(method: 'log' | 'info', ...args: any[]): void { console[method](...args); } diff --git a/readme.md b/readme.md index 4710575..d49b212 100644 --- a/readme.md +++ b/readme.md @@ -255,7 +255,7 @@ What storage area type to use (sync storage vs local storage). Sync storage is u - Sync is default as it's likely more convenient for users. - Firefox requires [`browser_specific_settings.gecko.id`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings) for the `sync` storage to work locally. -- Sync storage is subject to much tighter [quota limitations](https://developer.chrome.com/docs/extensions/reference/storage/#property-sync), and may cause privacy concerns if the data being stored is confidential. +- Sync storage is subject to much tighter [quota limitations](https://developer.chrome.com/docs/extensions/reference/storage/#property-sync), and may cause privacy concerns if the data being stored is confidential. #### optionsStorage.set(options) @@ -281,7 +281,7 @@ This returns a Promise that will resolve with all the options. #### optionsStorage.syncForm(form) -Any defaults or saved options will be loaded into the `
` and any change will automatically be saved via `chrome.storage.sync` +Any defaults or saved options will be loaded into the `` and any change will automatically be saved via `chrome.storage.sync`. It also looks for any buttons with `js-import` or `js-export` classes that when clicked will allow the user to export and import the options to a JSON file. ##### form @@ -293,6 +293,14 @@ It's the `` that needs to be synchronized or a CSS selector (one element). Removes any listeners added by `syncForm`. +#### optionsStorage.exportFromFile() + +Opens the browser’s "save file" dialog to export options to a JSON file. If your form has a `.js-export` element, this listener will be attached automatically. + +#### optionsStorage.importFromFile() + +Opens the browser’s file picker to import options from a previously-saved JSON file. If your form has a `.js-import` element, this listener will be attached automatically. + ## Related - [webext-options-sync-per-domain](https://github.com/fregante/webext-options-sync-per-domain) - Wrapper for `webext-options-sync` to have different options for each domain your extension supports. From 82835394d07d68a33c76e2b75fd3770b9df66a6b Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Mon, 3 Apr 2023 12:37:39 +0800 Subject: [PATCH 02/18] 4.1.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a50b838..5b18420 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.0.1", + "version": "4.1.0-0", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser", From 5a385d3ff6ff6ff06be763e7868fad684b9073e4 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 15:33:41 +0800 Subject: [PATCH 03/18] Don't export defaults --- index.ts | 18 +++++++++++------- readme.md | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index 8d98cd5..1abb10b 100644 --- a/index.ts +++ b/index.ts @@ -213,7 +213,7 @@ class OptionsSync { const {name} = chrome.runtime.getManifest(); const options = { [this._jsonIdentityHelper]: name, - ...await this.getAll(), + ...await this._getAll(false), }; const text = JSON.stringify(options, null, '\t'); @@ -249,9 +249,9 @@ class OptionsSync { console[method](...args); } - private async _getAll(): Promise { + private async _getAll(defaults = true): Promise { const result = await this.storage.get(this.storageName); - return this._decode(result[this.storageName]); + return this._decode(result[this.storageName], defaults); } private async _setAll(newOptions: UserOptions): Promise { @@ -274,13 +274,17 @@ class OptionsSync { return compressToEncodedURIComponent(JSON.stringify(thinnedOptions)); } - private _decode(options: string | UserOptions): UserOptions { + private _decode(options: string | UserOptions, defaults = true): UserOptions { let decompressed = options; - if (typeof options === 'string') { - decompressed = JSON.parse(decompressFromEncodedURIComponent(options)!) as UserOptions; + if (typeof decompressed === 'string') { + decompressed = JSON.parse(decompressFromEncodedURIComponent(decompressed)!) as UserOptions; } - return {...this.defaults, ...decompressed as UserOptions}; + if (defaults) { + return {...this.defaults, ...decompressed}; + } + + return decompressed; } private async _runMigrations(migrations: Array>): Promise { diff --git a/readme.md b/readme.md index d49b212..18b5176 100644 --- a/readme.md +++ b/readme.md @@ -293,7 +293,7 @@ It's the `` that needs to be synchronized or a CSS selector (one element). Removes any listeners added by `syncForm`. -#### optionsStorage.exportFromFile() +#### optionsStorage.exportToFile() Opens the browser’s "save file" dialog to export options to a JSON file. If your form has a `.js-export` element, this listener will be attached automatically. From d06260b44d0f685bda74a19b15bb1876caeb6675 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 15:34:12 +0800 Subject: [PATCH 04/18] Revert "Don't export defaults" This reverts commit 5a385d3ff6ff6ff06be763e7868fad684b9073e4. --- index.ts | 18 +++++++----------- readme.md | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/index.ts b/index.ts index 1abb10b..8d98cd5 100644 --- a/index.ts +++ b/index.ts @@ -213,7 +213,7 @@ class OptionsSync { const {name} = chrome.runtime.getManifest(); const options = { [this._jsonIdentityHelper]: name, - ...await this._getAll(false), + ...await this.getAll(), }; const text = JSON.stringify(options, null, '\t'); @@ -249,9 +249,9 @@ class OptionsSync { console[method](...args); } - private async _getAll(defaults = true): Promise { + private async _getAll(): Promise { const result = await this.storage.get(this.storageName); - return this._decode(result[this.storageName], defaults); + return this._decode(result[this.storageName]); } private async _setAll(newOptions: UserOptions): Promise { @@ -274,17 +274,13 @@ class OptionsSync { return compressToEncodedURIComponent(JSON.stringify(thinnedOptions)); } - private _decode(options: string | UserOptions, defaults = true): UserOptions { + private _decode(options: string | UserOptions): UserOptions { let decompressed = options; - if (typeof decompressed === 'string') { - decompressed = JSON.parse(decompressFromEncodedURIComponent(decompressed)!) as UserOptions; + if (typeof options === 'string') { + decompressed = JSON.parse(decompressFromEncodedURIComponent(options)!) as UserOptions; } - if (defaults) { - return {...this.defaults, ...decompressed}; - } - - return decompressed; + return {...this.defaults, ...decompressed as UserOptions}; } private async _runMigrations(migrations: Array>): Promise { diff --git a/readme.md b/readme.md index 18b5176..d49b212 100644 --- a/readme.md +++ b/readme.md @@ -293,7 +293,7 @@ It's the `` that needs to be synchronized or a CSS selector (one element). Removes any listeners added by `syncForm`. -#### optionsStorage.exportToFile() +#### optionsStorage.exportFromFile() Opens the browser’s "save file" dialog to export options to a JSON file. If your form has a `.js-export` element, this listener will be attached automatically. From b9ce8b7b5818b64bdced54b70b5cf1a31fd8afaf Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 15:34:34 +0800 Subject: [PATCH 05/18] Just allow partial imports --- index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 8d98cd5..be5552d 100644 --- a/index.ts +++ b/index.ts @@ -242,7 +242,7 @@ class OptionsSync { const options = JSON.parse(text) as UserOptions; delete options[this._jsonIdentityHelper]; - await this.setAll(options); + await this.set(options); }; private _log(method: 'log' | 'info', ...args: any[]): void { From f95f793a6f1a0d03455b773e70df0f6f3ce74a55 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 15:43:05 +0800 Subject: [PATCH 06/18] Update form after import --- index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.ts b/index.ts index be5552d..75c1617 100644 --- a/index.ts +++ b/index.ts @@ -243,6 +243,9 @@ class OptionsSync { delete options[this._jsonIdentityHelper]; await this.set(options); + if (this._form) { + this._updateForm(this._form, options); + } }; private _log(method: 'log' | 'info', ...args: any[]): void { From 607290b0434a3445c5268bae627a7f0b09d73003 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 21:03:14 +0800 Subject: [PATCH 07/18] 4.1.0-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b18420..06e5d85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.1.0-0", + "version": "4.1.0-1", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser", From 7f937370b79ccd9fc66920e2d0f84747087220d8 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 21:25:57 +0800 Subject: [PATCH 08/18] Use File System Access API --- index.ts | 69 +++++++++++++++++++++++++++++----------------------- package.json | 1 + 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/index.ts b/index.ts index 75c1617..566bc8d 100644 --- a/index.ts +++ b/index.ts @@ -199,47 +199,27 @@ class OptionsSync { Opens the browser’s file picker to import options from a previously-saved JSON file */ importFromFile = async (): Promise => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.addEventListener('change', this._handleImportFile, {once: true}); - input.click(); - }; - - /** - Opens the browser’s "save file" dialog to export options to a JSON file - */ - exportToFile = async (): Promise => { - const {name} = chrome.runtime.getManifest(); - const options = { - [this._jsonIdentityHelper]: name, - ...await this.getAll(), - }; - - const text = JSON.stringify(options, null, '\t'); - const url = URL.createObjectURL(new Blob([text], {type: 'application/json'})); - const link = document.createElement('a'); - link.download = name + ' options.json'; - link.href = url; - link.click(); - }; - - private readonly _handleImportFile = async (event: Event): Promise => { - const file = (event.target as HTMLInputElement).files![0]; - if (!file) { + let fileHandle: FileSystemFileHandle; + try { + [fileHandle] = await showOpenFilePicker(); + } catch { + // eslint-disable-next-line no-alert + alert('This feature doesn’t seem to be supported by your browser. Make sure you’re using its latest version.'); return; } + const file = await fileHandle.getFile(); const text = await file.text(); + let options: UserOptions; + try { - JSON.parse(text); + options = JSON.parse(text) as UserOptions; } catch { // eslint-disable-next-line no-alert alert('The file is not a valid JSON file.'); return; } - const options = JSON.parse(text) as UserOptions; delete options[this._jsonIdentityHelper]; await this.set(options); @@ -248,6 +228,35 @@ class OptionsSync { } }; + /** + Opens the browser’s "save file" dialog to export options to a JSON file + */ + exportToFile = async (): Promise => { + const {name} = chrome.runtime.getManifest(); + const options = { + [this._jsonIdentityHelper]: name, + ...await this.getAll(), + }; + + const text = JSON.stringify(options, null, '\t'); + + const fileHandle = await window.showSaveFilePicker({ + suggestedName: name + ' options.json', + types: [ + { + accept: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'application/json': ['.json'], + }, + }, + ], + }); + + const writable = await fileHandle.createWritable(); + await writable.write(new Blob([text], {type: 'application/json'})); + await writable.close(); + }; + private _log(method: 'log' | 'info', ...args: any[]): void { console[method](...args); } diff --git a/package.json b/package.json index 06e5d85..a5184b6 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/chrome": "0.0.210", "@types/lz-string": "^1.3.34", "@types/throttle-debounce": "^5.0.0", + "@types/wicg-file-system-access": "^2020.9.5", "ava": "^5.1.1", "npm-run-all": "^4.1.5", "sinon": "^15.0.1", From 890a1034349d6ec5835164b703d8cc3c8a4a15df Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 21:26:33 +0800 Subject: [PATCH 09/18] 4.1.0-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5184b6..e16a198 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.1.0-1", + "version": "4.1.0-2", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser", From 6aafef8ed61933fea24db0b7e69625e4e0bbae55 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 22:19:43 +0800 Subject: [PATCH 10/18] Cleanup --- index.ts | 74 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/index.ts b/index.ts index 566bc8d..e556423 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,23 @@ import LZString from 'lz-string'; // eslint-disable-next-line @typescript-eslint/naming-convention -- CJS in ESM imports const {compressToEncodedURIComponent, decompressFromEncodedURIComponent} = LZString; +function alertAndThrow(message: string): never { + // eslint-disable-next-line no-alert + alert(message); + throw new Error(message); +} + +const filePickerOptions: FilePickerOptions = { + types: [ + { + accept: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'application/json': '.json', + }, + }, + ], +}; + async function shouldRunMigrations(): Promise { const self = await chromeP.management?.getSelf(); @@ -91,6 +108,8 @@ class OptionsSync { _form: HTMLFormElement | undefined; + isExportSupported = typeof showOpenFilePicker === 'function'; + private readonly _migrations: Promise; /** @@ -192,22 +211,22 @@ class OptionsSync { } private get _jsonIdentityHelper() { - return '__optionsForExtension'; + return '__webextOptionsSync'; } + assertExportSupported = (): void => { + if (!this.isExportSupported) { + alertAndThrow('This feature doesn’t seem to be supported by your browser. Make sure you’re using its latest version.'); + } + }; + /** Opens the browser’s file picker to import options from a previously-saved JSON file */ importFromFile = async (): Promise => { - let fileHandle: FileSystemFileHandle; - try { - [fileHandle] = await showOpenFilePicker(); - } catch { - // eslint-disable-next-line no-alert - alert('This feature doesn’t seem to be supported by your browser. Make sure you’re using its latest version.'); - return; - } + this.assertExportSupported(); + const [fileHandle] = await showOpenFilePicker(filePickerOptions); const file = await fileHandle.getFile(); const text = await file.text(); let options: UserOptions; @@ -215,9 +234,11 @@ class OptionsSync { try { options = JSON.parse(text) as UserOptions; } catch { - // eslint-disable-next-line no-alert - alert('The file is not a valid JSON file.'); - return; + alertAndThrow('The file is not a valid JSON file.'); + } + + if (!(this._jsonIdentityHelper in options)) { + alertAndThrow('The file selected is not a valid recognized options file.'); } delete options[this._jsonIdentityHelper]; @@ -232,28 +253,21 @@ class OptionsSync { Opens the browser’s "save file" dialog to export options to a JSON file */ exportToFile = async (): Promise => { - const {name} = chrome.runtime.getManifest(); - const options = { - [this._jsonIdentityHelper]: name, + this.assertExportSupported(); + + const extension = chrome.runtime.getManifest(); + const text = JSON.stringify({ + [this._jsonIdentityHelper]: extension.name, ...await this.getAll(), - }; - - const text = JSON.stringify(options, null, '\t'); - - const fileHandle = await window.showSaveFilePicker({ - suggestedName: name + ' options.json', - types: [ - { - accept: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'application/json': ['.json'], - }, - }, - ], + }, null, '\t'); + + const fileHandle = await showSaveFilePicker({ + ...filePickerOptions, + suggestedName: extension.name + ' options.json', }); const writable = await fileHandle.createWritable(); - await writable.write(new Blob([text], {type: 'application/json'})); + await writable.write(text); await writable.close(); }; From 06fafa8a6f002908e2ef43683a4d8181baf612da Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 22:20:18 +0800 Subject: [PATCH 11/18] 4.1.0-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e16a198..8e295c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.1.0-2", + "version": "4.1.0-3", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser", From 6f5f5c4f7a19fba615bfa35c5a06c284f81e7c28 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Tue, 4 Apr 2023 22:26:47 +0800 Subject: [PATCH 12/18] Update tests --- test/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/index.js b/test/index.js index c8315e8..958612d 100644 --- a/test/index.js +++ b/test/index.js @@ -15,6 +15,7 @@ function compressOptions(options) { const defaultSetup = { _migrations: {}, defaults: {}, + isExportSupported: false, storageName: 'options', storageType: 'sync', }; From 09a665ae3696f6c034ecea79590028cc7a315f2b Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 19:26:20 +0800 Subject: [PATCH 13/18] Extract load/save and add fallback --- .gitignore | 2 ++ file.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.ts | 36 +++---------------------------- test/index.js | 1 - 4 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 file.ts diff --git a/.gitignore b/.gitignore index 0908dcb..447f092 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ logs *.map index.js index.d.ts +file.js +file.d.ts !*/index.js diff --git a/file.ts b/file.ts new file mode 100644 index 0000000..426fbe6 --- /dev/null +++ b/file.ts @@ -0,0 +1,59 @@ + +const filePickerOptions: FilePickerOptions = { + types: [ + { + accept: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'application/json': '.json', + }, + }, + ], +}; + +const isModern = typeof showOpenFilePicker === 'function'; + +async function loadFileOld(): Promise { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + const eventPromise = new Promise(resolve => { + input.addEventListener('change', resolve, {once: true}); + }); + + input.click(); + const event = await eventPromise; + const file = (event.target as HTMLInputElement).files![0]; + if (!file) { + throw new Error('No file selected'); + } + + return file.text(); +} + +async function saveFileOld(text: string, suggestedName: string): Promise { + const url = URL.createObjectURL(new Blob([text], {type: 'application/json'})); + const link = document.createElement('a'); + link.download = suggestedName; + link.href = url; + link.click(); +} + +async function loadFileModern(): Promise { + const [fileHandle] = await showOpenFilePicker(filePickerOptions); + const file = await fileHandle.getFile(); + return file.text(); +} + +async function saveFileModern(text: string, suggestedName: string) { + const fileHandle = await showSaveFilePicker({ + ...filePickerOptions, + suggestedName, + }); + + const writable = await fileHandle.createWritable(); + await writable.write(text); + await writable.close(); +} + +export const loadFile = isModern ? loadFileModern : loadFileOld; +export const saveFile = isModern ? saveFileModern : saveFileOld; diff --git a/index.ts b/index.ts index e556423..b838ca3 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import chromeP from 'webext-polyfill-kinda'; import {isBackground} from 'webext-detect-page'; import {serialize, deserialize} from 'dom-form-serializer/dist/dom-form-serializer.mjs'; import LZString from 'lz-string'; +import {loadFile, saveFile} from './file.js'; // eslint-disable-next-line @typescript-eslint/naming-convention -- CJS in ESM imports const {compressToEncodedURIComponent, decompressFromEncodedURIComponent} = LZString; @@ -13,17 +14,6 @@ function alertAndThrow(message: string): never { throw new Error(message); } -const filePickerOptions: FilePickerOptions = { - types: [ - { - accept: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'application/json': '.json', - }, - }, - ], -}; - async function shouldRunMigrations(): Promise { const self = await chromeP.management?.getSelf(); @@ -108,8 +98,6 @@ class OptionsSync { _form: HTMLFormElement | undefined; - isExportSupported = typeof showOpenFilePicker === 'function'; - private readonly _migrations: Promise; /** @@ -214,21 +202,12 @@ class OptionsSync { return '__webextOptionsSync'; } - assertExportSupported = (): void => { - if (!this.isExportSupported) { - alertAndThrow('This feature doesn’t seem to be supported by your browser. Make sure you’re using its latest version.'); - } - }; - /** Opens the browser’s file picker to import options from a previously-saved JSON file */ importFromFile = async (): Promise => { - this.assertExportSupported(); + const text = await loadFile(); - const [fileHandle] = await showOpenFilePicker(filePickerOptions); - const file = await fileHandle.getFile(); - const text = await file.text(); let options: UserOptions; try { @@ -253,22 +232,13 @@ class OptionsSync { Opens the browser’s "save file" dialog to export options to a JSON file */ exportToFile = async (): Promise => { - this.assertExportSupported(); - const extension = chrome.runtime.getManifest(); const text = JSON.stringify({ [this._jsonIdentityHelper]: extension.name, ...await this.getAll(), }, null, '\t'); - const fileHandle = await showSaveFilePicker({ - ...filePickerOptions, - suggestedName: extension.name + ' options.json', - }); - - const writable = await fileHandle.createWritable(); - await writable.write(text); - await writable.close(); + await saveFile(text, extension.name + ' options.json'); }; private _log(method: 'log' | 'info', ...args: any[]): void { diff --git a/test/index.js b/test/index.js index 958612d..c8315e8 100644 --- a/test/index.js +++ b/test/index.js @@ -15,7 +15,6 @@ function compressOptions(options) { const defaultSetup = { _migrations: {}, defaults: {}, - isExportSupported: false, storageName: 'options', storageType: 'sync', }; From f54efe480c4c1a6c6b49984b132bf4a25a5d8390 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 19:28:09 +0800 Subject: [PATCH 14/18] Publish new files --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e295c7..8bb3bda 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "main": "index.js", "files": [ "index.js", - "index.d.ts" + "index.d.ts", + "file.js", + "file.d.ts" ], "scripts": { "build": "tsc", From 7cf1c25080528a177192d8563b68c5a68afb5f46 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 19:30:03 +0800 Subject: [PATCH 15/18] 4.1.0-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bb3bda..6704f81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.1.0-3", + "version": "4.1.0-4", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser", From c2456f170f8863d9d56f5d3b0963a4d767c23df8 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 20:29:58 +0800 Subject: [PATCH 16/18] Safari support --- file.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/file.ts b/file.ts index 426fbe6..f232698 100644 --- a/file.ts +++ b/file.ts @@ -31,7 +31,8 @@ async function loadFileOld(): Promise { } async function saveFileOld(text: string, suggestedName: string): Promise { - const url = URL.createObjectURL(new Blob([text], {type: 'application/json'})); + // Use base64 because Safari doesn't support saving blob URLs + const url = `data:application/json;base64,${btoa(text)}`; const link = document.createElement('a'); link.download = suggestedName; link.href = url; From 51b3bab359c8da55c9aa1a2b937670cf529ad209 Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 21:07:57 +0800 Subject: [PATCH 17/18] Comments --- file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.ts b/file.ts index f232698..d1e0e27 100644 --- a/file.ts +++ b/file.ts @@ -1,4 +1,3 @@ - const filePickerOptions: FilePickerOptions = { types: [ { @@ -31,7 +30,8 @@ async function loadFileOld(): Promise { } async function saveFileOld(text: string, suggestedName: string): Promise { - // Use base64 because Safari doesn't support saving blob URLs + // Use data URL because Safari doesn't support saving blob URLs + // Use base64 or else linebreaks are lost const url = `data:application/json;base64,${btoa(text)}`; const link = document.createElement('a'); link.download = suggestedName; From 0908e550bf1a1214437df12df040fecda7fa960a Mon Sep 17 00:00:00 2001 From: Federico Brigante Date: Fri, 14 Apr 2023 21:08:30 +0800 Subject: [PATCH 18/18] 4.1.0-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6704f81..1c98276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webext-options-sync", - "version": "4.1.0-4", + "version": "4.1.0-5", "description": "Helps you manage and autosave your extension's options.", "keywords": [ "browser",