Skip to content

Commit

Permalink
🔀 Merge pull request #553 from FrostCo/export_stats
Browse files Browse the repository at this point in the history
Import/Export Stats
  • Loading branch information
richardfrost authored May 9, 2024
2 parents 96b8293 + f53e47b commit 7718245
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 11 deletions.
8 changes: 8 additions & 0 deletions src/script/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/script/mainOptionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'); });
Expand All @@ -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'); });
Expand Down
48 changes: 40 additions & 8 deletions src/script/optionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
removeChildren,
removeFromArray,
stringArray,
timeForFileName,
upperCaseFirst,
} from '@APF/lib/helper';

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -613,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));
Expand Down Expand Up @@ -734,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;
Expand All @@ -750,12 +757,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() {
Expand Down Expand Up @@ -796,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);
Expand Down Expand Up @@ -1833,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';
}
Expand Down
7 changes: 6 additions & 1 deletion src/static/optionPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,12 @@ <h4 class="sectionHeader">Stats</h4>
<span class="notes">(Only stored locally)</span>
</label>

<button id="statsReset" class="w3-btn w3-round-large w3-flat-pomegranate w3-right">RESET STATS</button>
<div class="w3-right">
<input type="file" id="statsImportInput" accept=".json" style="display:none"/>
<button id="statsExport" class="w3-btn w3-round-large w3-flat-peter-river">EXPORT</button>
<button id="statsImport" class="w3-btn w3-round-large w3-flat-peter-river">IMPORT</button>
<button id="statsReset" class="w3-btn w3-round-large w3-flat-pomegranate">RESET</button>
</div>

<h4 class="sectionHeader">Summary</h4>
<div id="statsSummaryContainer">
Expand Down
19 changes: 18 additions & 1 deletion test/spec/lib/helper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
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,
timeForFileName,
} from '@APF/lib/helper';

const array = ['a', 'needle', 'in', 'a', 'large', 'haystack'];

Expand Down Expand Up @@ -179,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}/);
});
});
});

0 comments on commit 7718245

Please sign in to comment.