aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/api.js44
-rw-r--r--ext/bg/js/backend.js139
-rw-r--r--ext/bg/js/conditions-ui.js326
-rw-r--r--ext/bg/js/conditions.js117
-rw-r--r--ext/bg/js/context.js6
-rw-r--r--ext/bg/js/options.js226
-rw-r--r--ext/bg/js/profile-conditions.js85
-rw-r--r--ext/bg/js/search-frontend.js6
-rw-r--r--ext/bg/js/search.js9
-rw-r--r--ext/bg/js/settings-profiles.js281
-rw-r--r--ext/bg/js/settings.js338
-rw-r--r--ext/bg/js/translator.js6
-rw-r--r--ext/bg/js/util.js9
13 files changed, 1331 insertions, 261 deletions
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index b8ef4362..474fe604 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -17,16 +17,23 @@
*/
-async function apiOptionsSet(options) {
- utilBackend().onOptionsUpdated(options);
+function apiOptionsGet(optionsContext) {
+ return utilBackend().getOptions(optionsContext);
}
-async function apiOptionsGet() {
- return utilBackend().options;
+function apiOptionsGetFull() {
+ return utilBackend().getFullOptions();
}
-async function apiTermsFind(text) {
- const options = utilBackend().options;
+async function apiOptionsSave(source) {
+ const backend = utilBackend();
+ const options = await apiOptionsGetFull();
+ await optionsSave(options);
+ backend.onOptionsUpdated(source);
+}
+
+async function apiTermsFind(text, optionsContext) {
+ const options = await apiOptionsGet(optionsContext);
const translator = utilBackend().translator;
const searcher = {
@@ -38,7 +45,8 @@ async function apiTermsFind(text) {
const {definitions, length} = await searcher(
text,
dictEnabledSet(options),
- options.scanning.alphanumeric
+ options.scanning.alphanumeric,
+ options
);
return {
@@ -47,14 +55,14 @@ async function apiTermsFind(text) {
};
}
-async function apiKanjiFind(text) {
- const options = utilBackend().options;
+async function apiKanjiFind(text, optionsContext) {
+ const options = await apiOptionsGet(optionsContext);
const definitions = await utilBackend().translator.findKanji(text, dictEnabledSet(options));
return definitions.slice(0, options.general.maxResults);
}
-async function apiDefinitionAdd(definition, mode, context) {
- const options = utilBackend().options;
+async function apiDefinitionAdd(definition, mode, context, optionsContext) {
+ const options = await apiOptionsGet(optionsContext);
if (mode !== 'kanji') {
await audioInject(
@@ -76,14 +84,15 @@ async function apiDefinitionAdd(definition, mode, context) {
return utilBackend().anki.addNote(note);
}
-async function apiDefinitionsAddable(definitions, modes) {
+async function apiDefinitionsAddable(definitions, modes, optionsContext) {
+ const options = await apiOptionsGet(optionsContext);
const states = [];
try {
const notes = [];
for (const definition of definitions) {
for (const mode of modes) {
- const note = await dictNoteFormat(definition, mode, utilBackend().options);
+ const note = await dictNoteFormat(definition, mode, options);
notes.push(note);
}
}
@@ -131,10 +140,13 @@ async function apiCommandExec(command) {
},
toggle: async () => {
- const options = utilBackend().options;
+ const optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+ const options = await apiOptionsGet(optionsContext);
options.general.enable = !options.general.enable;
- await optionsSave(options);
- await apiOptionsSet(options);
+ await apiOptionsSave('popup');
}
};
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 39fd4288..4068b760 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -22,47 +22,44 @@ class Backend {
this.translator = new Translator();
this.anki = new AnkiNull();
this.options = null;
+ this.optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+
+ this.isPreparedResolve = null;
+ this.isPreparedPromise = new Promise((resolve) => (this.isPreparedResolve = resolve));
this.apiForwarder = new BackendApiForwarder();
}
async prepare() {
await this.translator.prepare();
- await apiOptionsSet(await optionsLoad());
+ this.options = await optionsLoad();
+ this.onOptionsUpdated('background');
if (chrome.commands !== null && typeof chrome.commands === 'object') {
chrome.commands.onCommand.addListener(this.onCommand.bind(this));
}
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
- if (this.options.general.showGuide) {
+ const options = this.getOptionsSync(this.optionsContext);
+ if (options.general.showGuide) {
chrome.tabs.create({url: chrome.extension.getURL('/bg/guide.html')});
}
+
+ this.isPreparedResolve();
+ this.isPreparedResolve = null;
+ this.isPreparedPromise = null;
}
- onOptionsUpdated(options) {
- this.options = utilIsolate(options);
-
- if (!options.general.enable) {
- this.setExtensionBadgeBackgroundColor('#555555');
- this.setExtensionBadgeText('off');
- } else if (!dictConfigured(options)) {
- this.setExtensionBadgeBackgroundColor('#f0ad4e');
- this.setExtensionBadgeText('!');
- } else {
- this.setExtensionBadgeText('');
- }
-
- if (options.anki.enable) {
- this.anki = new AnkiConnect(options.anki.server);
- } else {
- this.anki = new AnkiNull();
- }
+ onOptionsUpdated(source) {
+ this.applyOptions();
const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.tabs.query({}, tabs => {
for (const tab of tabs) {
- chrome.tabs.sendMessage(tab.id, {action: 'optionsSet', params: options}, callback);
+ chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback);
}
});
}
@@ -81,28 +78,24 @@ class Backend {
};
const handlers = {
- optionsGet: ({callback}) => {
- forward(apiOptionsGet(), callback);
- },
-
- optionsSet: ({options, callback}) => {
- forward(apiOptionsSet(options), callback);
+ optionsGet: ({optionsContext, callback}) => {
+ forward(apiOptionsGet(optionsContext), callback);
},
- kanjiFind: ({text, callback}) => {
- forward(apiKanjiFind(text), callback);
+ kanjiFind: ({text, optionsContext, callback}) => {
+ forward(apiKanjiFind(text, optionsContext), callback);
},
- termsFind: ({text, callback}) => {
- forward(apiTermsFind(text), callback);
+ termsFind: ({text, optionsContext, callback}) => {
+ forward(apiTermsFind(text, optionsContext), callback);
},
- definitionAdd: ({definition, mode, context, callback}) => {
- forward(apiDefinitionAdd(definition, mode, context), callback);
+ definitionAdd: ({definition, mode, context, optionsContext, callback}) => {
+ forward(apiDefinitionAdd(definition, mode, context, optionsContext), callback);
},
- definitionsAddable: ({definitions, modes, callback}) => {
- forward(apiDefinitionsAddable(definitions, modes), callback);
+ definitionsAddable: ({definitions, modes, optionsContext, callback}) => {
+ forward(apiDefinitionsAddable(definitions, modes, optionsContext), callback);
},
noteView: ({noteId}) => {
@@ -143,12 +136,86 @@ class Backend {
return true;
}
+ applyOptions() {
+ const options = this.getOptionsSync(this.optionsContext);
+ if (!options.general.enable) {
+ this.setExtensionBadgeBackgroundColor('#555555');
+ this.setExtensionBadgeText('off');
+ } else if (!dictConfigured(options)) {
+ this.setExtensionBadgeBackgroundColor('#f0ad4e');
+ this.setExtensionBadgeText('!');
+ } else {
+ this.setExtensionBadgeText('');
+ }
+
+ this.anki = options.anki.enable ? new AnkiConnect(options.anki.server) : new AnkiNull();
+ }
+
+ async getFullOptions() {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ return this.options;
+ }
+
+ async getOptions(optionsContext) {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ return this.getOptionsSync(optionsContext);
+ }
+
+ getOptionsSync(optionsContext) {
+ return this.getProfileSync(optionsContext).options;
+ }
+
+ getProfileSync(optionsContext) {
+ const profiles = this.options.profiles;
+ if (typeof optionsContext.index === 'number') {
+ return profiles[optionsContext.index];
+ }
+ const profile = this.getProfileFromContext(optionsContext);
+ return profile !== null ? profile : this.options.profiles[this.options.profileCurrent];
+ }
+
+ getProfileFromContext(optionsContext) {
+ for (const profile of this.options.profiles) {
+ const conditionGroups = profile.conditionGroups;
+ if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) {
+ return profile;
+ }
+ }
+ return null;
+ }
+
+ static testConditionGroups(conditionGroups, data) {
+ if (conditionGroups.length === 0) { return false; }
+
+ for (const conditionGroup of conditionGroups) {
+ const conditions = conditionGroup.conditions;
+ if (conditions.length > 0 && Backend.testConditions(conditions, data)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static testConditions(conditions, data) {
+ for (const condition of conditions) {
+ if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
setExtensionBadgeBackgroundColor(color) {
if (typeof chrome.browserAction.setBadgeBackgroundColor === 'function') {
chrome.browserAction.setBadgeBackgroundColor({color});
}
}
-
+
setExtensionBadgeText(text) {
if (typeof chrome.browserAction.setBadgeText === 'function') {
chrome.browserAction.setBadgeText({text});
diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js
new file mode 100644
index 00000000..a6f54a1c
--- /dev/null
+++ b/ext/bg/js/conditions-ui.js
@@ -0,0 +1,326 @@
+/*
+ * 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/>.
+ */
+
+
+class ConditionsUI {
+ static instantiateTemplate(templateSelector) {
+ const template = document.querySelector(templateSelector);
+ const content = document.importNode(template.content, true);
+ return $(content.firstChild);
+ }
+}
+
+ConditionsUI.Container = class Container {
+ constructor(conditionDescriptors, conditionNameDefault, conditionGroups, container, addButton) {
+ this.children = [];
+ this.conditionDescriptors = conditionDescriptors;
+ this.conditionNameDefault = conditionNameDefault;
+ this.conditionGroups = conditionGroups;
+ this.container = container;
+ this.addButton = addButton;
+
+ this.container.empty();
+
+ for (const conditionGroup of conditionGroups) {
+ this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));
+ }
+
+ this.addButton.on('click', () => this.onAddConditionGroup());
+ }
+
+ cleanup() {
+ for (const child of this.children) {
+ child.cleanup();
+ }
+
+ this.addButton.off('click');
+ this.container.empty();
+ }
+
+ save() {
+ // Override
+ }
+
+ isolate(object) {
+ // Override
+ return object;
+ }
+
+ remove(child) {
+ const index = this.children.indexOf(child);
+ if (index < 0) {
+ return;
+ }
+
+ child.cleanup();
+ this.children.splice(index, 1);
+ this.conditionGroups.splice(index, 1);
+ }
+
+ onAddConditionGroup() {
+ const conditionGroup = this.isolate({
+ conditions: [this.createDefaultCondition(this.conditionNameDefault)]
+ });
+ this.conditionGroups.push(conditionGroup);
+ this.save();
+ this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));
+ }
+
+ createDefaultCondition(type) {
+ let operator = '';
+ let value = '';
+ if (this.conditionDescriptors.hasOwnProperty(type)) {
+ const conditionDescriptor = this.conditionDescriptors[type];
+ operator = conditionDescriptor.defaultOperator;
+ ({value} = this.getOperatorDefaultValue(type, operator));
+ if (typeof value === 'undefined') {
+ value = '';
+ }
+ }
+ return {type, operator, value};
+ }
+
+ getOperatorDefaultValue(type, operator) {
+ if (this.conditionDescriptors.hasOwnProperty(type)) {
+ const conditionDescriptor = this.conditionDescriptors[type];
+ if (conditionDescriptor.operators.hasOwnProperty(operator)) {
+ const operatorDescriptor = conditionDescriptor.operators[operator];
+ if (operatorDescriptor.hasOwnProperty('defaultValue')) {
+ return {value: operatorDescriptor.defaultValue, fromOperator: true};
+ }
+ }
+ if (conditionDescriptor.hasOwnProperty('defaultValue')) {
+ return {value: conditionDescriptor.defaultValue, fromOperator: false};
+ }
+ }
+ return {fromOperator: false};
+ }
+};
+
+ConditionsUI.ConditionGroup = class ConditionGroup {
+ constructor(parent, conditionGroup) {
+ this.parent = parent;
+ this.children = [];
+ this.conditionGroup = conditionGroup;
+ this.container = $('<div>').addClass('condition-group').appendTo(parent.container);
+ this.options = ConditionsUI.instantiateTemplate('#condition-group-options-template').appendTo(parent.container);
+ this.separator = ConditionsUI.instantiateTemplate('#condition-group-separator-template').appendTo(parent.container);
+ this.addButton = this.options.find('.condition-add');
+
+ for (const condition of conditionGroup.conditions) {
+ this.children.push(new ConditionsUI.Condition(this, condition));
+ }
+
+ this.addButton.on('click', () => this.onAddCondition());
+ }
+
+ cleanup() {
+ for (const child of this.children) {
+ child.cleanup();
+ }
+
+ this.addButton.off('click');
+ this.container.remove();
+ this.options.remove();
+ this.separator.remove();
+ }
+
+ save() {
+ this.parent.save();
+ }
+
+ isolate(object) {
+ return this.parent.isolate(object);
+ }
+
+ remove(child) {
+ const index = this.children.indexOf(child);
+ if (index < 0) {
+ return;
+ }
+
+ child.cleanup();
+ this.children.splice(index, 1);
+ this.conditionGroup.conditions.splice(index, 1);
+
+ if (this.children.length === 0) {
+ this.parent.remove(this, false);
+ }
+ }
+
+ onAddCondition() {
+ const condition = this.isolate(this.parent.createDefaultCondition(this.parent.conditionNameDefault));
+ this.conditionGroup.conditions.push(condition);
+ this.children.push(new ConditionsUI.Condition(this, condition));
+ }
+};
+
+ConditionsUI.Condition = class Condition {
+ constructor(parent, condition) {
+ this.parent = parent;
+ this.condition = condition;
+ this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container);
+ this.input = this.container.find('input');
+ this.typeSelect = this.container.find('.condition-type');
+ this.operatorSelect = this.container.find('.condition-operator');
+ this.removeButton = this.container.find('.condition-remove');
+
+ this.updateTypes();
+ this.updateOperators();
+ this.updateInput();
+
+ this.input.on('change', () => this.onInputChanged());
+ this.typeSelect.on('change', () => this.onConditionTypeChanged());
+ this.operatorSelect.on('change', () => this.onConditionOperatorChanged());
+ this.removeButton.on('click', () => this.onRemoveClicked());
+ }
+
+ cleanup() {
+ this.input.off('change');
+ this.typeSelect.off('change');
+ this.operatorSelect.off('change');
+ this.removeButton.off('click');
+ this.container.remove();
+ }
+
+ save() {
+ this.parent.save();
+ }
+
+ updateTypes() {
+ const conditionDescriptors = this.parent.parent.conditionDescriptors;
+ const optionGroup = this.typeSelect.find('optgroup');
+ optionGroup.empty();
+ for (const type of Object.keys(conditionDescriptors)) {
+ const conditionDescriptor = conditionDescriptors[type];
+ $('<option>').val(type).text(conditionDescriptor.name).appendTo(optionGroup);
+ }
+ this.typeSelect.val(this.condition.type);
+ }
+
+ updateOperators() {
+ const conditionDescriptors = this.parent.parent.conditionDescriptors;
+ const optionGroup = this.operatorSelect.find('optgroup');
+ optionGroup.empty();
+
+ const type = this.condition.type;
+ if (conditionDescriptors.hasOwnProperty(type)) {
+ const conditionDescriptor = conditionDescriptors[type];
+ const operators = conditionDescriptor.operators;
+ for (const operatorName of Object.keys(operators)) {
+ const operatorDescriptor = operators[operatorName];
+ $('<option>').val(operatorName).text(operatorDescriptor.name).appendTo(optionGroup);
+ }
+ }
+
+ this.operatorSelect.val(this.condition.operator);
+ }
+
+ updateInput() {
+ const conditionDescriptors = this.parent.parent.conditionDescriptors;
+ const {type, operator} = this.condition;
+ const props = {
+ placeholder: '',
+ type: 'text'
+ };
+
+ const objects = [];
+ if (conditionDescriptors.hasOwnProperty(type)) {
+ const conditionDescriptor = conditionDescriptors[type];
+ objects.push(conditionDescriptor);
+ if (conditionDescriptor.operators.hasOwnProperty(operator)) {
+ const operatorDescriptor = conditionDescriptor.operators[operator];
+ objects.push(operatorDescriptor);
+ }
+ }
+
+ for (const object of objects) {
+ if (object.hasOwnProperty('placeholder')) {
+ props.placeholder = object.placeholder;
+ }
+ if (object.type === 'number') {
+ props.type = 'number';
+ for (const prop of ['step', 'min', 'max']) {
+ if (object.hasOwnProperty(prop)) {
+ props[prop] = object[prop];
+ }
+ }
+ }
+ }
+
+ for (const prop in props) {
+ this.input.prop(prop, props[prop]);
+ }
+
+ const {valid} = this.validateValue(this.condition.value);
+ this.input.toggleClass('is-invalid', !valid);
+ this.input.val(this.condition.value);
+ }
+
+ validateValue(value) {
+ const conditionDescriptors = this.parent.parent.conditionDescriptors;
+ let valid = true;
+ try {
+ value = conditionsNormalizeOptionValue(
+ conditionDescriptors,
+ this.condition.type,
+ this.condition.operator,
+ value
+ );
+ } catch (e) {
+ valid = false;
+ }
+ return {valid, value};
+ }
+
+ onInputChanged() {
+ const {valid, value} = this.validateValue(this.input.val());
+ this.input.toggleClass('is-invalid', !valid);
+ this.input.val(value);
+ this.condition.value = value;
+ this.save();
+ }
+
+ onConditionTypeChanged() {
+ const type = this.typeSelect.val();
+ const {operator, value} = this.parent.parent.createDefaultCondition(type);
+ this.condition.type = type;
+ this.condition.operator = operator;
+ this.condition.value = value;
+ this.save();
+ this.updateOperators();
+ this.updateInput();
+ }
+
+ onConditionOperatorChanged() {
+ const type = this.condition.type;
+ const operator = this.operatorSelect.val();
+ const {value, fromOperator} = this.parent.parent.getOperatorDefaultValue(type, operator);
+ this.condition.operator = operator;
+ if (fromOperator) {
+ this.condition.value = value;
+ }
+ this.save();
+ this.updateInput();
+ }
+
+ onRemoveClicked() {
+ this.parent.remove(this);
+ this.save();
+ }
+};
diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js
new file mode 100644
index 00000000..ed4b14f5
--- /dev/null
+++ b/ext/bg/js/conditions.js
@@ -0,0 +1,117 @@
+/*
+ * 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/>.
+ */
+
+
+function conditionsValidateOptionValue(object, value) {
+ if (object.hasOwnProperty('validate') && !object.validate(value)) {
+ throw new Error('Invalid value for condition');
+ }
+
+ if (object.hasOwnProperty('transform')) {
+ value = object.transform(value);
+
+ if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) {
+ throw new Error('Invalid value for condition');
+ }
+ }
+
+ return value;
+}
+
+function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) {
+ if (!descriptors.hasOwnProperty(type)) {
+ throw new Error('Invalid type');
+ }
+
+ const conditionDescriptor = descriptors[type];
+ if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
+ throw new Error('Invalid operator');
+ }
+
+ const operatorDescriptor = conditionDescriptor.operators[operator];
+
+ let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue);
+ transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue);
+
+ if (operatorDescriptor.hasOwnProperty('transformReverse')) {
+ transformedValue = operatorDescriptor.transformReverse(transformedValue);
+ }
+ return transformedValue;
+}
+
+function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) {
+ if (!descriptors.hasOwnProperty(type)) {
+ throw new Error('Invalid type');
+ }
+
+ const conditionDescriptor = descriptors[type];
+ if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
+ throw new Error('Invalid operator');
+ }
+
+ const operatorDescriptor = conditionDescriptor.operators[operator];
+ if (operatorDescriptor.hasOwnProperty('transform')) {
+ if (operatorDescriptor.hasOwnProperty('transformCache')) {
+ const key = `${optionValue}`;
+ const transformCache = operatorDescriptor.transformCache;
+ if (transformCache.hasOwnProperty(key)) {
+ optionValue = transformCache[key];
+ } else {
+ optionValue = operatorDescriptor.transform(optionValue);
+ transformCache[key] = optionValue;
+ }
+ } else {
+ optionValue = operatorDescriptor.transform(optionValue);
+ }
+ }
+
+ return operatorDescriptor.test(value, optionValue);
+}
+
+function conditionsTestValue(descriptors, type, operator, optionValue, value) {
+ try {
+ return conditionsTestValueThrowing(descriptors, type, operator, optionValue, value);
+ } catch (e) {
+ return false;
+ }
+}
+
+function conditionsClearCaches(descriptors) {
+ for (const type in descriptors) {
+ if (!descriptors.hasOwnProperty(type)) {
+ continue;
+ }
+
+ const conditionDescriptor = descriptors[type];
+ if (conditionDescriptor.hasOwnProperty('transformCache')) {
+ conditionDescriptor.transformCache = {};
+ }
+
+ const operatorDescriptors = conditionDescriptor.operators;
+ for (const operator in operatorDescriptors) {
+ if (!operatorDescriptors.hasOwnProperty(operator)) {
+ continue;
+ }
+
+ const operatorDescriptor = operatorDescriptors[operator];
+ if (operatorDescriptor.hasOwnProperty('transformCache')) {
+ operatorDescriptor.transformCache = {};
+ }
+ }
+ }
+}
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 689d6863..0f88e9c0 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -22,7 +22,11 @@ $(document).ready(utilAsync(() => {
$('#open-options').click(() => apiCommandExec('options'));
$('#open-help').click(() => apiCommandExec('help'));
- optionsLoad().then(options => {
+ const optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+ apiOptionsGet(optionsContext).then(options => {
const toggle = $('#enable-search');
toggle.prop('checked', options.general.enable).change();
toggle.bootstrapToggle();
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index df95aae9..e9e321df 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -17,7 +17,67 @@
*/
-function optionsFieldTemplates() {
+/*
+ * Generic options functions
+ */
+
+function optionsGenericApplyUpdates(options, updates) {
+ const targetVersion = updates.length;
+ const currentVersion = options.version;
+ if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) {
+ for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) {
+ const update = updates[i];
+ if (update !== null) {
+ update(options);
+ }
+ }
+ }
+
+ options.version = targetVersion;
+ return options;
+}
+
+
+/*
+ * Per-profile options
+ */
+
+const profileOptionsVersionUpdates = [
+ null,
+ null,
+ null,
+ null,
+ (options) => {
+ options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled';
+ },
+ (options) => {
+ options.general.showGuide = false;
+ },
+ (options) => {
+ options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none';
+ },
+ (options) => {
+ const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();
+ options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split';
+ options.anki.fieldTemplates = (
+ (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) ?
+ `{{#if merge}}${fieldTemplatesDefault}{{else}}${options.anki.fieldTemplates}{{/if}}` :
+ fieldTemplatesDefault
+ );
+ },
+ (options) => {
+ if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) {
+ options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();
+ }
+ },
+ (options) => {
+ if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {
+ options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();
+ }
+ }
+];
+
+function profileOptionsGetDefaultFieldTemplates() {
return `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
@@ -183,8 +243,8 @@ function optionsFieldTemplates() {
`.trim();
}
-function optionsSetDefaults(options) {
- const defaults = {
+function profileOptionsCreateDefaults() {
+ return {
general: {
enable: true,
audioSource: 'jpod101',
@@ -235,9 +295,13 @@ function optionsSetDefaults(options) {
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}},
- fieldTemplates: optionsFieldTemplates()
+ fieldTemplates: profileOptionsGetDefaultFieldTemplates()
}
};
+}
+
+function profileOptionsSetDefaults(options) {
+ const defaults = profileOptionsCreateDefaults();
const combine = (target, source) => {
for (const key in source) {
@@ -257,81 +321,119 @@ function optionsSetDefaults(options) {
return options;
}
-function optionsVersion(options) {
- const fixups = [
- () => {},
- () => {},
- () => {},
- () => {},
- () => {
- if (options.general.audioPlayback) {
- options.general.audioSource = 'jpod101';
- } else {
- options.general.audioSource = 'disabled';
- }
- },
- () => {
- options.general.showGuide = false;
- },
- () => {
- if (options.scanning.requireShift) {
- options.scanning.modifier = 'shift';
- } else {
- options.scanning.modifier = 'none';
- }
- },
- () => {
- if (options.general.groupResults) {
- options.general.resultOutputMode = 'group';
- } else {
- options.general.resultOutputMode = 'split';
- }
- if (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) {
- options.anki.fieldTemplates = `{{#if merge}}${optionsFieldTemplates()}{{else}}${options.anki.fieldTemplates}{{/if}}`;
- } else {
- options.anki.fieldTemplates = optionsFieldTemplates();
- }
- },
- () => {
- if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) {
- options.anki.fieldTemplates = optionsFieldTemplates();
- }
- },
- () => {
- if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {
- options.anki.fieldTemplates = optionsFieldTemplates();
- }
+function profileOptionsUpdateVersion(options) {
+ profileOptionsSetDefaults(options);
+ return optionsGenericApplyUpdates(options, profileOptionsVersionUpdates);
+}
+
+
+/*
+ * Global options
+ *
+ * Each profile has an array named "conditionGroups", which is an array of condition groups
+ * which enable the contextual selection of profiles. The structure of the array is as follows:
+ * [
+ * {
+ * conditions: [
+ * {
+ * type: "string",
+ * operator: "string",
+ * value: "string"
+ * },
+ * // ...
+ * ]
+ * },
+ * // ...
+ * ]
+ */
+
+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);
}
- ];
+ }
- optionsSetDefaults(options);
- if (!options.hasOwnProperty('version')) {
- options.version = fixups.length;
+ // Require at least one profile
+ if (profiles.length === 0) {
+ profiles.push({
+ name: 'Default',
+ options: defaultProfileOptions,
+ conditionGroups: []
+ });
}
- while (options.version < fixups.length) {
- fixups[options.version++]();
+ // 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;
}
- return options;
+ // Update profile options
+ for (const profile of profiles) {
+ if (!Array.isArray(profile.conditionGroups)) {
+ profile.conditionGroups = [];
+ }
+ profile.options = profileOptionsUpdateVersion(profile.options);
+ }
+
+ // Generic updates
+ return optionsGenericApplyUpdates(options, optionsVersionUpdates);
}
function optionsLoad() {
return new Promise((resolve, reject) => {
- chrome.storage.local.get(null, store => resolve(store.options));
+ chrome.storage.local.get(['options'], store => {
+ const error = chrome.runtime.lastError;
+ if (error) {
+ reject(error);
+ } else {
+ resolve(store.options);
+ }
+ });
}).then(optionsStr => {
- return optionsStr ? JSON.parse(optionsStr) : {};
- }).catch(error => {
+ if (typeof optionsStr === 'string') {
+ const options = JSON.parse(optionsStr);
+ if (utilIsObject(options)) {
+ return options;
+ }
+ }
+ return {};
+ }).catch(() => {
return {};
}).then(options => {
- return optionsVersion(options);
+ return (
+ Array.isArray(options.profiles) ?
+ optionsUpdateVersion(options, {}) :
+ optionsUpdateVersion({}, options)
+ );
});
}
function optionsSave(options) {
return new Promise((resolve, reject) => {
- chrome.storage.local.set({options: JSON.stringify(options)}, resolve);
- }).then(() => {
- apiOptionsSet(options);
+ chrome.storage.local.set({options: JSON.stringify(options)}, () => {
+ const error = chrome.runtime.lastError;
+ if (error) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ });
});
}
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
new file mode 100644
index 00000000..5daa904e
--- /dev/null
+++ b/ext/bg/js/profile-conditions.js
@@ -0,0 +1,85 @@
+/*
+ * 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/>.
+ */
+
+
+const profileConditionsDescriptor = {
+ popupLevel: {
+ name: 'Popup Level',
+ description: 'Use profile depending on the level of the popup.',
+ placeholder: 'Number',
+ type: 'number',
+ step: 1,
+ defaultValue: 0,
+ defaultOperator: 'equal',
+ transform: (optionValue) => parseInt(optionValue, 10),
+ transformReverse: (transformedOptionValue) => `${transformedOptionValue}`,
+ validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue),
+ operators: {
+ equal: {
+ name: '=',
+ test: ({depth}, optionValue) => (depth === optionValue)
+ },
+ notEqual: {
+ name: '\u2260',
+ test: ({depth}, optionValue) => (depth !== optionValue)
+ },
+ lessThan: {
+ name: '<',
+ test: ({depth}, optionValue) => (depth < optionValue)
+ },
+ greaterThan: {
+ name: '>',
+ test: ({depth}, optionValue) => (depth > optionValue)
+ },
+ lessThanOrEqual: {
+ name: '\u2264',
+ test: ({depth}, optionValue) => (depth <= optionValue)
+ },
+ greaterThanOrEqual: {
+ name: '\u2265',
+ test: ({depth}, optionValue) => (depth >= optionValue)
+ }
+ }
+ },
+ url: {
+ name: 'URL',
+ description: 'Use profile depending on the URL of the current website.',
+ defaultOperator: 'matchDomain',
+ operators: {
+ matchDomain: {
+ name: 'Matches Domain',
+ placeholder: 'Comma separated list of domains',
+ defaultValue: 'example.com',
+ transformCache: {},
+ transform: (optionValue) => optionValue.split(/[,;\s]+/).map(v => v.trim().toLowerCase()).filter(v => v.length > 0),
+ transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),
+ validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),
+ test: ({url}, transformedOptionValue) => (transformedOptionValue.indexOf(new URL(url).hostname.toLowerCase()) >= 0)
+ },
+ matchRegExp: {
+ name: 'Matches RegExp',
+ placeholder: 'Regular expression',
+ defaultValue: 'example\\.com',
+ transformCache: {},
+ transform: (optionValue) => new RegExp(optionValue, 'i'),
+ transformReverse: (transformedOptionValue) => transformedOptionValue.source,
+ test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
+ }
+ }
+ }
+};
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index 840a1ea8..faec29ef 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -18,7 +18,11 @@
async function searchFrontendSetup() {
- const options = await apiOptionsGet();
+ const optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+ const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage) { return; }
const scriptSrcs = [
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index a3382398..6ff710f0 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -21,6 +21,11 @@ class DisplaySearch extends Display {
constructor() {
super($('#spinner'), $('#content'));
+ this.optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+
this.search = $('#search').click(this.onSearch.bind(this));
this.query = $('#query').on('input', this.onSearchInput.bind(this));
this.intro = $('#intro');
@@ -46,8 +51,8 @@ class DisplaySearch extends Display {
try {
e.preventDefault();
this.intro.slideUp();
- const {length, definitions} = await apiTermsFind(this.query.val());
- super.termsShow(definitions, await apiOptionsGet());
+ const {length, definitions} = await apiTermsFind(this.query.val(), this.optionsContext);
+ super.termsShow(definitions, await apiOptionsGet(this.optionsContext));
} catch (e) {
this.onError(e);
}
diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings-profiles.js
new file mode 100644
index 00000000..ededc998
--- /dev/null
+++ b/ext/bg/js/settings-profiles.js
@@ -0,0 +1,281 @@
+/*
+ * 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;
+let profileConditionsContainer = null;
+
+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);
+
+ if (profileConditionsContainer !== null) {
+ profileConditionsContainer.cleanup();
+ }
+
+ profileConditionsContainer = new ConditionsUI.Container(
+ profileConditionsDescriptor,
+ 'popupLevel',
+ profile.conditionGroups,
+ $('#profile-condition-groups'),
+ $('#profile-add-condition-group')
+ );
+ profileConditionsContainer.save = () => {
+ apiOptionsSave();
+ conditionsClearCaches(profileConditionsDescriptor);
+ };
+ profileConditionsContainer.isolate = utilBackgroundIsolate;
+}
+
+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 83f4528c..cb3ddd4e 100644
--- a/ext/bg/js/settings.js
+++ b/ext/bg/js/settings.js
@@ -16,73 +16,143 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+async function getOptionsArray() {
+ const optionsFull = await apiOptionsGetFull();
+ return optionsFull.profiles.map(profile => profile.options);
+}
-async function formRead() {
- const optionsOld = await optionsLoad();
- const optionsNew = $.extend(true, {}, optionsOld);
-
- optionsNew.general.showGuide = $('#show-usage-guide').prop('checked');
- optionsNew.general.compactTags = $('#compact-tags').prop('checked');
- optionsNew.general.compactGlossaries = $('#compact-glossaries').prop('checked');
- optionsNew.general.autoPlayAudio = $('#auto-play-audio').prop('checked');
- optionsNew.general.resultOutputMode = $('#result-output-mode').val();
- optionsNew.general.audioSource = $('#audio-playback-source').val();
- optionsNew.general.audioVolume = parseFloat($('#audio-playback-volume').val());
- optionsNew.general.debugInfo = $('#show-debug-info').prop('checked');
- optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked');
- optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
- optionsNew.general.popupDisplayMode = $('#popup-display-mode').val();
- optionsNew.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val();
- optionsNew.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();
- optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10);
- optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10);
- optionsNew.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
- optionsNew.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
- optionsNew.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
- optionsNew.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
- optionsNew.general.customPopupCss = $('#custom-popup-css').val();
-
- optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
- optionsNew.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
- optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');
- optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
- optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
- optionsNew.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
- optionsNew.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
- optionsNew.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
- optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10);
- optionsNew.scanning.length = parseInt($('#scan-length').val(), 10);
- optionsNew.scanning.modifier = $('#scan-modifier-key').val();
- optionsNew.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10);
-
- optionsNew.anki.enable = $('#anki-enable').prop('checked');
- optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/);
- optionsNew.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
- optionsNew.anki.server = $('#interface-server').val();
- optionsNew.anki.screenshot.format = $('#screenshot-format').val();
- optionsNew.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
- optionsNew.anki.fieldTemplates = $('#field-templates').val();
-
- if (optionsOld.anki.enable && !ankiErrorShown()) {
- optionsNew.anki.terms.deck = $('#anki-terms-deck').val();
- optionsNew.anki.terms.model = $('#anki-terms-model').val();
- optionsNew.anki.terms.fields = ankiFieldsToDict($('#terms .anki-field-value'));
- optionsNew.anki.kanji.deck = $('#anki-kanji-deck').val();
- optionsNew.anki.kanji.model = $('#anki-kanji-model').val();
- optionsNew.anki.kanji.fields = ankiFieldsToDict($('#kanji .anki-field-value'));
+async function formRead(options) {
+ options.general.enable = $('#enable').prop('checked');
+ options.general.showGuide = $('#show-usage-guide').prop('checked');
+ options.general.compactTags = $('#compact-tags').prop('checked');
+ options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
+ options.general.autoPlayAudio = $('#auto-play-audio').prop('checked');
+ options.general.resultOutputMode = $('#result-output-mode').val();
+ options.general.audioSource = $('#audio-playback-source').val();
+ options.general.audioVolume = parseFloat($('#audio-playback-volume').val());
+ options.general.debugInfo = $('#show-debug-info').prop('checked');
+ options.general.showAdvanced = $('#show-advanced-options').prop('checked');
+ options.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
+ options.general.popupDisplayMode = $('#popup-display-mode').val();
+ options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val();
+ options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();
+ options.general.popupWidth = parseInt($('#popup-width').val(), 10);
+ options.general.popupHeight = parseInt($('#popup-height').val(), 10);
+ options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
+ options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
+ options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
+ options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
+ options.general.customPopupCss = $('#custom-popup-css').val();
+
+ options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
+ options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
+ options.scanning.selectText = $('#select-matched-text').prop('checked');
+ options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
+ options.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
+ options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
+ options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
+ options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
+ options.scanning.delay = parseInt($('#scan-delay').val(), 10);
+ options.scanning.length = parseInt($('#scan-length').val(), 10);
+ options.scanning.modifier = $('#scan-modifier-key').val();
+ options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10);
+
+ const optionsAnkiEnableOld = options.anki.enable;
+ options.anki.enable = $('#anki-enable').prop('checked');
+ options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/));
+ options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
+ options.anki.server = $('#interface-server').val();
+ options.anki.screenshot.format = $('#screenshot-format').val();
+ options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
+ options.anki.fieldTemplates = $('#field-templates').val();
+
+ if (optionsAnkiEnableOld && !ankiErrorShown()) {
+ options.anki.terms.deck = $('#anki-terms-deck').val();
+ options.anki.terms.model = $('#anki-terms-model').val();
+ options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict($('#terms .anki-field-value')));
+ options.anki.kanji.deck = $('#anki-kanji-deck').val();
+ options.anki.kanji.model = $('#anki-kanji-model').val();
+ options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict($('#kanji .anki-field-value')));
}
- optionsNew.general.mainDictionary = $('#dict-main').val();
+ options.general.mainDictionary = $('#dict-main').val();
$('.dict-group').each((index, element) => {
const dictionary = $(element);
- optionsNew.dictionaries[dictionary.data('title')] = {
+ options.dictionaries[dictionary.data('title')] = utilBackgroundIsolate({
priority: parseInt(dictionary.find('.dict-priority').val(), 10),
enabled: dictionary.find('.dict-enabled').prop('checked'),
allowSecondarySearches: dictionary.find('.dict-allow-secondary-searches').prop('checked')
- };
+ });
});
+}
+
+async function formWrite(options) {
+ $('#enable').prop('checked', options.general.enable);
+ $('#show-usage-guide').prop('checked', options.general.showGuide);
+ $('#compact-tags').prop('checked', options.general.compactTags);
+ $('#compact-glossaries').prop('checked', options.general.compactGlossaries);
+ $('#auto-play-audio').prop('checked', options.general.autoPlayAudio);
+ $('#result-output-mode').val(options.general.resultOutputMode);
+ $('#audio-playback-source').val(options.general.audioSource);
+ $('#audio-playback-volume').val(options.general.audioVolume);
+ $('#show-debug-info').prop('checked', options.general.debugInfo);
+ $('#show-advanced-options').prop('checked', options.general.showAdvanced);
+ $('#max-displayed-results').val(options.general.maxResults);
+ $('#popup-display-mode').val(options.general.popupDisplayMode);
+ $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition);
+ $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);
+ $('#popup-width').val(options.general.popupWidth);
+ $('#popup-height').val(options.general.popupHeight);
+ $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
+ $('#popup-vertical-offset').val(options.general.popupVerticalOffset);
+ $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
+ $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
+ $('#custom-popup-css').val(options.general.customPopupCss);
+
+ $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
+ $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
+ $('#select-matched-text').prop('checked', options.scanning.selectText);
+ $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
+ $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
+ $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
+ $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
+ $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
+ $('#scan-delay').val(options.scanning.delay);
+ $('#scan-length').val(options.scanning.length);
+ $('#scan-modifier-key').val(options.scanning.modifier);
+ $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth);
+
+ $('#anki-enable').prop('checked', options.anki.enable);
+ $('#card-tags').val(options.anki.tags.join(' '));
+ $('#sentence-detection-extent').val(options.anki.sentenceExt);
+ $('#interface-server').val(options.anki.server);
+ $('#screenshot-format').val(options.anki.screenshot.format);
+ $('#screenshot-quality').val(options.anki.screenshot.quality);
+ $('#field-templates').val(options.anki.fieldTemplates);
+
+ try {
+ await dictionaryGroupsPopulate(options);
+ await formMainDictionaryOptionsPopulate(options);
+ } catch (e) {
+ dictionaryErrorsShow([e]);
+ }
- return {optionsNew, optionsOld};
+ try {
+ await ankiDeckAndModelPopulate(options);
+ } catch (e) {
+ ankiErrorShow(e);
+ }
+
+ formUpdateVisibility(options);
+}
+
+function formSetupEventListeners() {
+ $('#dict-purge-link').click(utilAsync(onDictionaryPurge));
+ $('#dict-file').change(utilAsync(onDictionaryImport));
+
+ $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
+ $('input, select, textarea').not('.anki-model').not('.profile-form *').change(utilAsync(onFormOptionsChanged));
+ $('.anki-model').change(utilAsync(onAnkiModelChanged));
}
function formUpdateVisibility(options) {
@@ -141,18 +211,23 @@ async function onFormOptionsChanged(e) {
return;
}
- const {optionsNew, optionsOld} = await formRead();
- await optionsSave(optionsNew);
- formUpdateVisibility(optionsNew);
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ const optionsAnkiEnableOld = options.anki.enable;
+ const optionsAnkiServerOld = options.anki.server;
+
+ await formRead(options);
+ await settingsSaveOptions();
+ formUpdateVisibility(options);
try {
const ankiUpdated =
- optionsNew.anki.enable !== optionsOld.anki.enable ||
- optionsNew.anki.server !== optionsOld.anki.server;
+ options.anki.enable !== optionsAnkiEnableOld ||
+ options.anki.server !== optionsAnkiServerOld;
if (ankiUpdated) {
ankiSpinnerShow(true);
- await ankiDeckAndModelPopulate(optionsNew);
+ await ankiDeckAndModelPopulate(options);
ankiErrorShow();
}
} catch (e) {
@@ -163,75 +238,46 @@ async function onFormOptionsChanged(e) {
}
async function onReady() {
- const options = await optionsLoad();
+ formSetupEventListeners();
+ await profileOptionsSetup();
- $('#show-usage-guide').prop('checked', options.general.showGuide);
- $('#compact-tags').prop('checked', options.general.compactTags);
- $('#compact-glossaries').prop('checked', options.general.compactGlossaries);
- $('#auto-play-audio').prop('checked', options.general.autoPlayAudio);
- $('#result-output-mode').val(options.general.resultOutputMode);
- $('#audio-playback-source').val(options.general.audioSource);
- $('#audio-playback-volume').val(options.general.audioVolume);
- $('#show-debug-info').prop('checked', options.general.debugInfo);
- $('#show-advanced-options').prop('checked', options.general.showAdvanced);
- $('#max-displayed-results').val(options.general.maxResults);
- $('#popup-display-mode').val(options.general.popupDisplayMode);
- $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition);
- $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);
- $('#popup-width').val(options.general.popupWidth);
- $('#popup-height').val(options.general.popupHeight);
- $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
- $('#popup-vertical-offset').val(options.general.popupVerticalOffset);
- $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
- $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
- $('#custom-popup-css').val(options.general.customPopupCss);
+ storageInfoInitialize();
- $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
- $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
- $('#select-matched-text').prop('checked', options.scanning.selectText);
- $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
- $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
- $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
- $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
- $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
- $('#scan-delay').val(options.scanning.delay);
- $('#scan-length').val(options.scanning.length);
- $('#scan-modifier-key').val(options.scanning.modifier);
- $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth);
+ chrome.runtime.onMessage.addListener(onMessage);
+}
- $('#dict-purge-link').click(utilAsync(onDictionaryPurge));
- $('#dict-file').change(utilAsync(onDictionaryImport));
+$(document).ready(utilAsync(onReady));
- $('#anki-enable').prop('checked', options.anki.enable);
- $('#card-tags').val(options.anki.tags.join(' '));
- $('#sentence-detection-extent').val(options.anki.sentenceExt);
- $('#interface-server').val(options.anki.server);
- $('#screenshot-format').val(options.anki.screenshot.format);
- $('#screenshot-quality').val(options.anki.screenshot.quality);
- $('#field-templates').val(options.anki.fieldTemplates);
- $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
- $('input, select, textarea').not('.anki-model').change(utilAsync(onFormOptionsChanged));
- $('.anki-model').change(utilAsync(onAnkiModelChanged));
- try {
- await dictionaryGroupsPopulate(options);
- await formMainDictionaryOptionsPopulate(options);
- } catch (e) {
- dictionaryErrorsShow([e]);
- }
+/*
+ * Remote options updates
+ */
- try {
- await ankiDeckAndModelPopulate(options);
- } catch (e) {
- ankiErrorShow(e);
- }
+function settingsGetSource() {
+ return new Promise((resolve) => {
+ chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`));
+ });
+}
- formUpdateVisibility(options);
+async function settingsSaveOptions() {
+ const source = await settingsGetSource();
+ await apiOptionsSave(source);
+}
- storageInfoInitialize();
+async function onOptionsUpdate({source}) {
+ const thisSource = await settingsGetSource();
+ if (source === thisSource) { return; }
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ await formWrite(options);
}
-$(document).ready(utilAsync(onReady));
+function onMessage({action, params}) {
+ if (action === 'optionsUpdate') {
+ onOptionsUpdate(params);
+ }
+}
/*
@@ -374,11 +420,14 @@ async function onDictionaryPurge(e) {
dictionarySpinnerShow(true);
await utilDatabasePurge();
- const options = await optionsLoad();
- options.dictionaries = {};
- options.general.mainDictionary = '';
- await optionsSave(options);
+ 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) {
@@ -414,20 +463,26 @@ async function onDictionaryImport(e) {
setProgress(0.0);
const exceptions = [];
- const options = await optionsLoad();
const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions);
- options.dictionaries[summary.title] = {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 optionsSave(options);
-
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
await dictionaryGroupsPopulate(options);
await formMainDictionaryOptionsPopulate(options);
} catch (e) {
@@ -566,12 +621,14 @@ async function onAnkiModelChanged(e) {
const tab = element.closest('.tab-pane');
const tabId = tab.attr('id');
- const {optionsNew, optionsOld} = await formRead();
- optionsNew.anki[tabId].fields = {};
- await optionsSave(optionsNew);
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ await formRead(options);
+ options.anki[tabId].fields = utilBackgroundIsolate({});
+ await settingsSaveOptions();
ankiSpinnerShow(true);
- await ankiFieldsPopulate(element, optionsNew);
+ await ankiFieldsPopulate(element, options);
ankiErrorShow();
} catch (e) {
ankiErrorShow(e);
@@ -583,9 +640,12 @@ async function onAnkiModelChanged(e) {
async function onAnkiFieldTemplatesReset(e) {
try {
e.preventDefault();
- const options = await optionsLoad();
- $('#field-templates').val(options.anki.fieldTemplates = optionsFieldTemplates());
- await optionsSave(options);
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ const fieldTemplates = profileOptionsGetDefaultFieldTemplates();
+ options.anki.fieldTemplates = fieldTemplates;
+ $('#field-templates').val(fieldTemplates);
+ await settingsSaveOptions();
} catch (e) {
ankiErrorShow(e);
}
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index c89b43ff..7b952622 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -36,8 +36,7 @@ class Translator {
}
}
- async findTermsGrouped(text, dictionaries, alphanumeric) {
- const options = await apiOptionsGet();
+ async findTermsGrouped(text, dictionaries, alphanumeric, options) {
const titles = Object.keys(dictionaries);
const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
@@ -55,8 +54,7 @@ class Translator {
return {length, definitions: definitionsGrouped};
}
- async findTermsMerged(text, dictionaries, alphanumeric) {
- const options = await apiOptionsGet();
+ async findTermsMerged(text, dictionaries, alphanumeric, options) {
const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches);
const titles = Object.keys(dictionaries);
const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 3dc7c900..73a8396f 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -26,6 +26,11 @@ function utilIsolate(data) {
return JSON.parse(JSON.stringify(data));
}
+function utilBackgroundIsolate(data) {
+ const backgroundPage = chrome.extension.getBackgroundPage();
+ return backgroundPage.utilIsolate(data);
+}
+
function utilSetEqual(setA, setB) {
if (setA.size !== setB.size) {
return false;
@@ -104,3 +109,7 @@ function utilReadFile(file) {
reader.readAsBinaryString(file);
});
}
+
+function utilIsObject(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}