summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Yatskov <FooSoft@users.noreply.github.com>2019-09-16 18:32:01 -0700
committerGitHub <noreply@github.com>2019-09-16 18:32:01 -0700
commite3fb9603e22c5cbdc7cdf00b60e8ccbf1c7e2116 (patch)
tree912db53d6c496d8b2f26178db091aaaaaba729b4
parentae696c32eb572b3824cadcf3ab8852725c793191 (diff)
parent9fb89d8f7d77dd70b2493f730d8b224c994a6e98 (diff)
Merge pull request #209 from toasted-nutbread/settings-profiles
Settings profiles
-rw-r--r--ext/bg/js/api.js6
-rw-r--r--ext/bg/js/backend.js10
-rw-r--r--ext/bg/js/options.js91
-rw-r--r--ext/bg/js/settings-profiles.js263
-rw-r--r--ext/bg/js/settings.js47
-rw-r--r--ext/bg/settings.html72
6 files changed, 449 insertions, 40 deletions
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 <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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($(`<option value="${i}">${profile.name}</option>`));
+ }
+
+ 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 <http://www.gnu.org/licenses/>.
*/
-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 @@
</head>
<body>
<div class="container-fluid">
+ <div class="profile-form">
+ <h3>Profiles</h3>
+
+ <p class="help-block">
+ Profiles allow you to create multiple configurations and quickly switch between them.
+ </p>
+
+ <div class="form-group">
+ <label for="profile-active">Active profile</label>
+ <select class="form-control" id="profile-active"></select>
+ </div>
+
+ <div class="form-group">
+ <label for="profile-target">Modifying profile</label>
+ <div class="input-group">
+ <div class="input-group-btn">
+ <button class="btn btn-default" id="profile-add" title="Add"><span class="glyphicon glyphicon-plus"></span></button>
+ <button class="btn btn-default" id="profile-move-up" title="Move up"><span class="glyphicon glyphicon-arrow-up"></span></button>
+ <button class="btn btn-default" id="profile-move-down" title="Move down"><span class="glyphicon glyphicon-arrow-down"></span></button>
+ <button class="btn btn-default" id="profile-copy" title="Copy"><span class="glyphicon glyphicon-copy"></span></button>
+ </div>
+ <select class="form-control profile-form-manual" id="profile-target"></select>
+ <div class="input-group-btn">
+ <button class="btn btn-danger" id="profile-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="profile-name">Profile name</label>
+ <input type="text" id="profile-name" class="form-control">
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="profile-copy-modal">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">Copy Profile</h4>
+ </div>
+ <div class="modal-body">
+ <p>Select which profile to copy options from:</p>
+ <select class="form-control" id="profile-copy-source"></select>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-primary" id="profile-copy-confirm">Copy Profile</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="profile-remove-modal">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">Confirm profile removal</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to delete the profile <em id="profile-remove-modal-profile-name"></em>?
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger" id="profile-remove-confirm">Remove Profile</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
<div>
<h3>General Options</h3>
@@ -498,6 +569,7 @@
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script>
+ <script src="/bg/js/settings-profiles.js"></script>
<script src="/bg/js/settings.js"></script>
</body>
</html>