diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 81772d08..f32b984f 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -21,9 +21,13 @@ function apiOptionsGet(optionsContext) { return utilBackend().getOptions(optionsContext); } +function apiOptionsGetFull() { + return utilBackend().getFullOptions(); +} + async function apiOptionsSave(source) { const backend = utilBackend(); - const options = await backend.getFullOptions(); + const options = await apiOptionsGetFull(); await optionsSave(options); backend.onOptionsUpdated(source); } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9a300d62..3839da39 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -165,7 +165,15 @@ class Backend { } getOptionsSync(optionsContext) { - return this.options; + return this.getProfileSync(optionsContext).options; + } + + getProfileSync(optionsContext) { + const profiles = this.options.profiles; + if (typeof optionsContext.index === 'number') { + return profiles[optionsContext.index]; + } + return this.options.profiles[this.options.profileCurrent]; } setExtensionBadgeBackgroundColor(color) { diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 5f04ec31..3dce5221 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -17,7 +17,11 @@ */ -function optionsApplyUpdates(options, updates) { +/* + * Generic options functions + */ + +function optionsGenericApplyUpdates(options, updates) { const targetVersion = updates.length; const currentVersion = options.version; if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) { @@ -33,7 +37,12 @@ function optionsApplyUpdates(options, updates) { return options; } -const optionsVersionUpdates = [ + +/* + * Per-profile options + */ + +const profileOptionsVersionUpdates = [ null, null, null, @@ -48,7 +57,7 @@ const optionsVersionUpdates = [ options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none'; }, (options) => { - const fieldTemplatesDefault = optionsFieldTemplates(); + const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates(); options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split'; options.anki.fieldTemplates = ( (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) ? @@ -58,17 +67,17 @@ const optionsVersionUpdates = [ }, (options) => { if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) { - options.anki.fieldTemplates = optionsFieldTemplates(); + options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates(); } }, (options) => { if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) { - options.anki.fieldTemplates = optionsFieldTemplates(); + options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates(); } } ]; -function optionsFieldTemplates() { +function profileOptionsGetDefaultFieldTemplates() { return ` {{#*inline "glossary-single"}} {{~#unless brief~}} @@ -234,7 +243,7 @@ function optionsFieldTemplates() { `.trim(); } -function optionsCreateDefaults() { +function profileOptionsCreateDefaults() { return { general: { enable: true, @@ -286,13 +295,13 @@ function optionsCreateDefaults() { screenshot: {format: 'png', quality: 92}, terms: {deck: '', model: '', fields: {}}, kanji: {deck: '', model: '', fields: {}}, - fieldTemplates: optionsFieldTemplates() + fieldTemplates: profileOptionsGetDefaultFieldTemplates() } }; } -function optionsSetDefaults(options) { - const defaults = optionsCreateDefaults(); +function profileOptionsSetDefaults(options) { + const defaults = profileOptionsCreateDefaults(); const combine = (target, source) => { for (const key in source) { @@ -312,9 +321,59 @@ function optionsSetDefaults(options) { return options; } -function optionsVersion(options) { - optionsSetDefaults(options); - return optionsApplyUpdates(options, optionsVersionUpdates); +function profileOptionsUpdateVersion(options) { + profileOptionsSetDefaults(options); + return optionsGenericApplyUpdates(options, profileOptionsVersionUpdates); +} + + +/* + * Global options + */ + +const optionsVersionUpdates = []; + +function optionsUpdateVersion(options, defaultProfileOptions) { + // Ensure profiles is an array + if (!Array.isArray(options.profiles)) { + options.profiles = []; + } + + // Remove invalid + const profiles = options.profiles; + for (let i = profiles.length - 1; i >= 0; --i) { + if (!utilIsObject(profiles[i])) { + profiles.splice(i, 1); + } + } + + // Require at least one profile + if (profiles.length === 0) { + profiles.push({ + name: 'Default', + options: defaultProfileOptions + }); + } + + // Ensure profileCurrent is valid + const profileCurrent = options.profileCurrent; + if (!( + typeof profileCurrent === 'number' && + Number.isFinite(profileCurrent) && + Math.floor(profileCurrent) === profileCurrent && + profileCurrent >= 0 && + profileCurrent < profiles.length + )) { + options.profileCurrent = 0; + } + + // Update profile options + for (const profile of profiles) { + profile.options = profileOptionsUpdateVersion(profile.options); + } + + // Generic updates + return optionsGenericApplyUpdates(options, optionsVersionUpdates); } function optionsLoad() { @@ -338,7 +397,11 @@ function optionsLoad() { }).catch(() => { return {}; }).then(options => { - return optionsVersion(options); + return ( + Array.isArray(options.profiles) ? + optionsUpdateVersion(options, {}) : + optionsUpdateVersion({}, options) + ); }); } diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings-profiles.js new file mode 100644 index 00000000..624562c6 --- /dev/null +++ b/ext/bg/js/settings-profiles.js @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +let currentProfileIndex = 0; + +function getOptionsContext() { + return { + index: currentProfileIndex + }; +} + + +async function profileOptionsSetup() { + const optionsFull = await apiOptionsGetFull(); + currentProfileIndex = optionsFull.profileCurrent; + + profileOptionsSetupEventListeners(); + await profileOptionsUpdateTarget(optionsFull); +} + +function profileOptionsSetupEventListeners() { + $('#profile-target').change(utilAsync(onTargetProfileChanged)); + $('#profile-name').change(onProfileNameChanged); + $('#profile-add').click(utilAsync(onProfileAdd)); + $('#profile-remove').click(utilAsync(onProfileRemove)); + $('#profile-remove-confirm').click(utilAsync(onProfileRemoveConfirm)); + $('#profile-copy').click(utilAsync(onProfileCopy)); + $('#profile-copy-confirm').click(utilAsync(onProfileCopyConfirm)); + $('#profile-move-up').click(() => onProfileMove(-1)); + $('#profile-move-down').click(() => onProfileMove(1)); + $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(utilAsync(onProfileOptionsChanged)); +} + +function tryGetIntegerValue(selector, min, max) { + const value = parseInt($(selector).val(), 10); + return ( + typeof value === 'number' && + Number.isFinite(value) && + Math.floor(value) === value && + value >= min && + value < max + ) ? value : null; +} + +async function profileFormRead(optionsFull) { + const profile = optionsFull.profiles[currentProfileIndex]; + + // Current profile + const index = tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length); + if (index !== null) { + optionsFull.profileCurrent = index; + } + + // Profile name + profile.name = $('#profile-name').val(); +} + +async function profileFormWrite(optionsFull) { + const profile = optionsFull.profiles[currentProfileIndex]; + + profileOptionsPopulateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null); + profileOptionsPopulateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null); + $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1); + $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1); + $('#profile-move-up').prop('disabled', currentProfileIndex <= 0); + $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1); + + $('#profile-name').val(profile.name); +} + +function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) { + select.empty(); + + + for (let i = 0; i < profiles.length; ++i) { + if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) { + continue; + } + const profile = profiles[i]; + select.append($(``)); + } + + select.val(`${currentValue}`); +} + +async function profileOptionsUpdateTarget(optionsFull) { + profileFormWrite(optionsFull); + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + await formWrite(options); +} + +function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) { + let space, index, prefix, suffix; + const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name); + if (match === null) { + prefix = `${name} (Copy`; + space = ''; + index = ''; + suffix = ')'; + } else { + prefix = match[1]; + suffix = match[5]; + if (typeof match[2] === 'string') { + space = match[3]; + index = parseInt(match[4], 10) + 1; + } else { + space = ' '; + index = 2; + } + } + + let i = 0; + while (true) { + const newName = `${prefix}${space}${index}${suffix}`; + if (i++ >= maxUniqueAttempts || profiles.findIndex(profile => profile.name === newName) < 0) { + return newName; + } + if (typeof index !== 'number') { + index = 2; + space = ' '; + } else { + ++index; + } + } +} + +async function onProfileOptionsChanged(e) { + if (!e.originalEvent && !e.isTrigger) { + return; + } + + const optionsFull = await apiOptionsGetFull(); + await profileFormRead(optionsFull); + await apiOptionsSave(); +} + +async function onTargetProfileChanged() { + const optionsFull = await apiOptionsGetFull(); + const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length); + if (index === null || currentProfileIndex === index) { + return; + } + + currentProfileIndex = index; + + await profileOptionsUpdateTarget(optionsFull); +} + +async function onProfileAdd() { + const optionsFull = await apiOptionsGetFull(); + const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]); + profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100); + optionsFull.profiles.push(profile); + currentProfileIndex = optionsFull.profiles.length - 1; + await profileOptionsUpdateTarget(optionsFull); + await apiOptionsSave(); +} + +async function onProfileRemove(e) { + if (e.shiftKey) { + return await onProfileRemoveConfirm(); + } + + const optionsFull = await apiOptionsGetFull(); + if (optionsFull.profiles.length <= 1) { + return; + } + + const profile = optionsFull.profiles[currentProfileIndex]; + + $('#profile-remove-modal-profile-name').text(profile.name); + $('#profile-remove-modal').modal('show'); +} + +async function onProfileRemoveConfirm() { + $('#profile-remove-modal').modal('hide'); + + const optionsFull = await apiOptionsGetFull(); + if (optionsFull.profiles.length <= 1) { + return; + } + + optionsFull.profiles.splice(currentProfileIndex, 1); + + if (currentProfileIndex >= optionsFull.profiles.length) { + --currentProfileIndex; + } + + if (optionsFull.profileCurrent >= optionsFull.profiles.length) { + optionsFull.profileCurrent = optionsFull.profiles.length - 1; + } + + await profileOptionsUpdateTarget(optionsFull); + await apiOptionsSave(); +} + +function onProfileNameChanged() { + $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); +} + +async function onProfileMove(offset) { + const optionsFull = await apiOptionsGetFull(); + const index = currentProfileIndex + offset; + if (index < 0 || index >= optionsFull.profiles.length) { + return; + } + + const profile = optionsFull.profiles[currentProfileIndex]; + optionsFull.profiles.splice(currentProfileIndex, 1); + optionsFull.profiles.splice(index, 0, profile); + + if (optionsFull.profileCurrent === currentProfileIndex) { + optionsFull.profileCurrent = index; + } + + currentProfileIndex = index; + + await profileOptionsUpdateTarget(optionsFull); + await settingsSaveOptions(); +} + +async function onProfileCopy() { + const optionsFull = await apiOptionsGetFull(); + if (optionsFull.profiles.length <= 1) { + return; + } + + profileOptionsPopulateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]); + $('#profile-copy-modal').modal('show'); +} + +async function onProfileCopyConfirm() { + $('#profile-copy-modal').modal('hide'); + + const optionsFull = await apiOptionsGetFull(); + const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length); + if (index === null || index === currentProfileIndex) { + return; + } + + const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options); + optionsFull.profiles[currentProfileIndex].options = profileOptions; + + await profileOptionsUpdateTarget(optionsFull); + await settingsSaveOptions(); +} diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 3d581ba5..cb3ddd4e 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -16,10 +16,9 @@ * along with this program. If not, see . */ -function getOptionsContext() { - return { - depth: 0 - }; +async function getOptionsArray() { + const optionsFull = await apiOptionsGetFull(); + return optionsFull.profiles.map(profile => profile.options); } async function formRead(options) { @@ -239,11 +238,8 @@ async function onFormOptionsChanged(e) { } async function onReady() { - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - formSetupEventListeners(); - await formWrite(options); + await profileOptionsSetup(); storageInfoInitialize(); @@ -424,12 +420,14 @@ async function onDictionaryPurge(e) { dictionarySpinnerShow(true); await utilDatabasePurge(); - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - options.dictionaries = utilBackgroundIsolate({}); - options.general.mainDictionary = ''; + for (const options of await getOptionsArray()) { + options.dictionaries = utilBackgroundIsolate({}); + options.general.mainDictionary = ''; + } await settingsSaveOptions(); + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); await dictionaryGroupsPopulate(options); await formMainDictionaryOptionsPopulate(options); } catch (e) { @@ -466,24 +464,25 @@ async function onDictionaryImport(e) { const exceptions = []; const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions); - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - options.dictionaries[summary.title] = utilBackgroundIsolate({ - enabled: true, - priority: 0, - allowSecondarySearches: false - }); - if (summary.sequenced && options.general.mainDictionary === '') { - options.general.mainDictionary = summary.title; + for (const options of await getOptionsArray()) { + options.dictionaries[summary.title] = utilBackgroundIsolate({ + enabled: true, + priority: 0, + allowSecondarySearches: false + }); + if (summary.sequenced && options.general.mainDictionary === '') { + options.general.mainDictionary = summary.title; + } } + await settingsSaveOptions(); if (exceptions.length > 0) { exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`); dictionaryErrorsShow(exceptions); } - await settingsSaveOptions(); - + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); await dictionaryGroupsPopulate(options); await formMainDictionaryOptionsPopulate(options); } catch (e) { @@ -643,7 +642,7 @@ async function onAnkiFieldTemplatesReset(e) { e.preventDefault(); const optionsContext = getOptionsContext(); const options = await apiOptionsGet(optionsContext); - const fieldTemplates = optionsFieldTemplates(); + const fieldTemplates = profileOptionsGetDefaultFieldTemplates(); options.anki.fieldTemplates = fieldTemplates; $('#field-templates').val(fieldTemplates); await settingsSaveOptions(); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 7df47980..c0489894 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -67,6 +67,77 @@
+
+

Profiles

+ +

+ Profiles allow you to create multiple configurations and quickly switch between them. +

+ +
+ + +
+ +
+ +
+
+ + + + +
+ +
+ +
+
+
+ +
+ + +
+ + + + +
+

General Options

@@ -498,6 +569,7 @@ +