From 899cfe45bb1d6a47b979174b38adcf93a269aaba Mon Sep 17 00:00:00 2001 From: Richard Frost Date: Wed, 8 May 2024 18:43:03 -0600 Subject: [PATCH 1/5] :art: Split helper import statement in test --- test/spec/lib/helper.spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/spec/lib/helper.spec.ts b/test/spec/lib/helper.spec.ts index 8d34681a..752e0c02 100644 --- a/test/spec/lib/helper.spec.ts +++ b/test/spec/lib/helper.spec.ts @@ -1,6 +1,16 @@ import { expect } from 'chai'; import Constants from '@APF/lib/constants'; -import { booleanToNumber, formatNumber, getParent, getVersion, hmsToSeconds, isVersionOlder, numberToBoolean, removeFromArray, secondsToHMS } from '@APF/lib/helper'; +import { + booleanToNumber, + formatNumber, + getParent, + getVersion, + hmsToSeconds, + isVersionOlder, + numberToBoolean, + removeFromArray, + secondsToHMS, +} from '@APF/lib/helper'; const array = ['a', 'needle', 'in', 'a', 'large', 'haystack']; From 73c649a0419a8ff8b67182a3256de3a0b1778eb0 Mon Sep 17 00:00:00 2001 From: Richard Frost Date: Wed, 8 May 2024 18:43:05 -0600 Subject: [PATCH 2/5] :recycle: Add timeForFileName helper function --- src/script/lib/helper.ts | 8 ++++++++ test/spec/lib/helper.spec.ts | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/src/script/lib/helper.ts b/src/script/lib/helper.ts index f6242279..9ecd1153 100644 --- a/src/script/lib/helper.ts +++ b/src/script/lib/helper.ts @@ -214,6 +214,14 @@ export function stringArray(data: string | string[]): string[] { return data; } +export function timeForFileName() { + const padded = (num: number) => { return ('0' + num).slice(-2); }; + const date = new Date; + const today = `${date.getFullYear()}-${padded(date.getMonth()+1)}-${padded(date.getDate())}`; + const time = `${padded(date.getHours())}${padded(date.getMinutes())}${padded(date.getSeconds())}`; + return `${today}_${time}`; +} + export function upperCaseFirst(str: string, lowerCaseRest: boolean = true): string { let value = str.charAt(0).toUpperCase(); value += lowerCaseRest ? str.toLowerCase().slice(1) : str.slice(1); diff --git a/test/spec/lib/helper.spec.ts b/test/spec/lib/helper.spec.ts index 752e0c02..f2ab771a 100644 --- a/test/spec/lib/helper.spec.ts +++ b/test/spec/lib/helper.spec.ts @@ -10,6 +10,7 @@ import { numberToBoolean, removeFromArray, secondsToHMS, + timeForFileName, } from '@APF/lib/helper'; const array = ['a', 'needle', 'in', 'a', 'large', 'haystack']; @@ -189,4 +190,10 @@ describe('Helper', function() { expect(secondsToHMS(10818.5)).to.eql('03:00:18.500'); }); }); + + describe('timeForFileName()', function() { + it('Returns time string', function() { + expect(timeForFileName()).to.match(/\d{4}-\d{2}-\d{2}_\d{6}/); + }); + }); }); From 5fddd78f6aadf5f85f281817d479e63438f84323 Mon Sep 17 00:00:00 2001 From: Richard Frost Date: Wed, 8 May 2024 18:43:06 -0600 Subject: [PATCH 3/5] :recycle: Use timeForFileName helper in backupConfig() --- src/script/optionPage.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/script/optionPage.ts b/src/script/optionPage.ts index cd06b7cc..2e1e4be6 100644 --- a/src/script/optionPage.ts +++ b/src/script/optionPage.ts @@ -17,6 +17,7 @@ import { removeChildren, removeFromArray, stringArray, + timeForFileName, upperCaseFirst, } from '@APF/lib/helper'; @@ -253,11 +254,7 @@ export default class OptionPage { } backupConfig(config = this.cfg.ordered(), filePrefix = 'apf-backup') { - const padded = (num: number) => { return ('0' + num).slice(-2); }; - const date = new Date; - const today = `${date.getFullYear()}-${padded(date.getMonth()+1)}-${padded(date.getDate())}`; - const time = `${padded(date.getHours())}${padded(date.getMinutes())}${padded(date.getSeconds())}`; - exportToFile(JSON.stringify(config, null, 2), `${filePrefix}-${today}_${time}.json`); + exportToFile(JSON.stringify(config, null, 2), `${filePrefix}-${timeForFileName()}.json`); } backupConfigInline(config = this.cfg.ordered()) { From 3d7804b48dd7c65c926b5fd368b1300e6fb4c74a Mon Sep 17 00:00:00 2001 From: Richard Frost Date: Wed, 8 May 2024 18:43:07 -0600 Subject: [PATCH 4/5] :art: Use event.target in importConfigFile() --- src/script/mainOptionPage.ts | 2 +- src/script/optionPage.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/script/mainOptionPage.ts b/src/script/mainOptionPage.ts index 206939c7..974ad23c 100644 --- a/src/script/mainOptionPage.ts +++ b/src/script/mainOptionPage.ts @@ -65,7 +65,7 @@ document.querySelectorAll('#bookmarkletConfigInputs input').forEach((el) => { el // Config document.getElementById('configSyncLargeKeys').addEventListener('click', (evt) => { option.confirm('convertStorageLocation'); }); document.getElementById('configInlineInput').addEventListener('click', (evt) => { option.configInlineToggle(); }); -document.getElementById('importFileInput').addEventListener('change', (evt) => { option.importConfigFile((evt.target as HTMLInputElement).files); }); +document.getElementById('importFileInput').addEventListener('change', (evt) => { option.importConfigFile(evt.target as HTMLInputElement, (evt.target as HTMLInputElement).files); }); document.getElementById('configReset').addEventListener('click', (evt) => { option.confirm('restoreDefaults'); }); document.getElementById('configExport').addEventListener('click', (evt) => { option.exportConfig(); }); document.getElementById('configImport').addEventListener('click', (evt) => { option.confirm('importConfig'); }); diff --git a/src/script/optionPage.ts b/src/script/optionPage.ts index 2e1e4be6..58f35216 100644 --- a/src/script/optionPage.ts +++ b/src/script/optionPage.ts @@ -747,12 +747,11 @@ export default class OptionPage { } } - async importConfigFile(files: FileList) { + async importConfigFile(input: HTMLInputElement, files: FileList) { const file = files[0]; - const importFileInput = document.getElementById('importFileInput') as HTMLInputElement; const fileText = await readFile(file) as string; this.importConfigText(fileText); - importFileInput.value = ''; + input.value = ''; } async importConfigRetry() { From f53e47b7df4d9cf2da05a8bafc97c827cc7d5096 Mon Sep 17 00:00:00 2001 From: Richard Frost Date: Wed, 8 May 2024 18:43:09 -0600 Subject: [PATCH 5/5] :sparkles: Add export/import feature for local stats --- src/script/mainOptionPage.ts | 3 +++ src/script/optionPage.ts | 36 ++++++++++++++++++++++++++++++++++++ src/static/optionPage.html | 7 ++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/script/mainOptionPage.ts b/src/script/mainOptionPage.ts index 974ad23c..de775c4b 100644 --- a/src/script/mainOptionPage.ts +++ b/src/script/mainOptionPage.ts @@ -76,6 +76,9 @@ document.getElementById('setPasswordBtn').addEventListener('click', (evt) => { o document.getElementById('testText').addEventListener('input', (evt) => { option.populateTest(); }); // Stats document.getElementById('collectStats').addEventListener('click', (evt) => { option.saveOptions(); }); +document.getElementById('statsExport').addEventListener('click', (evt) => { option.exportStats(); }); +document.getElementById('statsImport').addEventListener('click', (evt) => { option.confirm('statsImport'); }); +document.getElementById('statsImportInput').addEventListener('change', (evt) => { option.importStatsFile(evt.target as HTMLInputElement, (evt.target as HTMLInputElement).files); }); document.getElementById('statsReset').addEventListener('click', (evt) => { option.confirm('statsReset'); }); document.getElementById('lessUsedWordsNumber').addEventListener('input', (evt) => { OptionPage.hideInputError(evt.target as HTMLInputElement); }); document.getElementById('removeLessUsedWords').addEventListener('click', (evt) => { option.confirm('removeLessUsedWords'); }); diff --git a/src/script/optionPage.ts b/src/script/optionPage.ts index 58f35216..1b6ce84f 100644 --- a/src/script/optionPage.ts +++ b/src/script/optionPage.ts @@ -610,6 +610,11 @@ export default class OptionPage { } } break; + case 'statsImport': + this.Class.configureConfirmModal({ content: 'Are you sure you want to overwrite your statistics?' }); + this._confirmEventListeners.push(this.importStats.bind(this)); + ok.addEventListener('click', lastElement(this._confirmEventListeners)); + break; case 'statsReset': this.Class.configureConfirmModal({ content: 'Are you sure you want to reset filter statistics?' }); this._confirmEventListeners.push(this.statsReset.bind(this)); @@ -731,6 +736,11 @@ export default class OptionPage { } } + async exportStats(filePrefix = 'apf-stats') { + const stats = await this.getStatsFromStorage(); + exportToFile(JSON.stringify(stats, null, 2), `${filePrefix}-${timeForFileName()}.json`); + } + async getStatsFromStorage() { const { stats }: { stats: Statistics } = await this.Class.Config.getLocalStorage({ stats: { words: {} } }) as any; return stats; @@ -792,6 +802,28 @@ export default class OptionPage { } } + importStats() { + const fileImportInput = document.getElementById('statsImportInput') as HTMLInputElement; + fileImportInput.click(); + } + + async importStatsFile(input: HTMLInputElement, files: FileList) { + const backupStats = await this.getStatsFromStorage(); + + try { + const file = files[0]; + const fileText = await readFile(file) as string; + const stats = JSON.parse(fileText); + if (!this.validStatsForImport(stats)) throw new Error('Invalid stats file.'); + await this.Class.Config.saveLocalStorage({ stats: stats }); + input.value = ''; + await this.populateStats(); + } catch (err) { + await this.Class.Config.saveLocalStorage({ stats: backupStats }); + this.Class.handleError('Failed to import stats.', err); + } + } + async init(refreshTheme = false) { await this.initializeCfg(); logger.setLevel(this.cfg.loggingLevel); @@ -1829,6 +1861,10 @@ export default class OptionPage { return valid; } + validStatsForImport(stats) { + return stats && stats?.startedAt > 1 && stats.words[Object.keys(stats.words)[0]].text >= 0; + } + wordlistTypeFromElement(element: HTMLSelectElement) { if (element.id === 'textWordlistSelect') return 'wordlistId'; } diff --git a/src/static/optionPage.html b/src/static/optionPage.html index 43f1238c..860aed20 100644 --- a/src/static/optionPage.html +++ b/src/static/optionPage.html @@ -443,7 +443,12 @@

Stats

(Only stored locally) - +
+ + + + +

Summary