aboutsummaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html2
-rw-r--r--ext/bg/guide.html2
-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
-rw-r--r--ext/bg/settings.html285
16 files changed, 1577 insertions, 304 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 90a56024..3b37db87 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -16,11 +16,13 @@
<script src="/bg/js/api.js"></script>
<script src="/bg/js/audio.js"></script>
<script src="/bg/js/backend-api-forwarder.js"></script>
+ <script src="/bg/js/conditions.js"></script>
<script src="/bg/js/database.js"></script>
<script src="/bg/js/deinflector.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/options.js"></script>
+ <script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/translator.js"></script>
diff --git a/ext/bg/guide.html b/ext/bg/guide.html
index 6f98d264..7ec1d8d9 100644
--- a/ext/bg/guide.html
+++ b/ext/bg/guide.html
@@ -15,7 +15,7 @@
<p>
Read the steps below to get up and running with Yomichan. For complete documentation,
- visit the <a href="https://foosoft.net/projects/yomichan/" target="_blank">official homepage</a>.
+ visit the <a href="https://foosoft.net/projects/yomichan/" target="_blank" rel="noopener">official homepage</a>.
</p>
<ol>
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);
+}
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 85b7ee5f..d38aa090 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -30,6 +30,59 @@
padding-bottom: 1em;
}
+ .label-light {
+ font-weight: normal;
+ }
+
+ .form-control.is-invalid {
+ border-color: #f00000;
+ }
+
+ .condition>.condition-prefix:after {
+ content: "IF";
+ }
+ .condition:nth-child(n+2)>.condition-prefix:after {
+ content: "AND";
+ }
+
+ .input-group .condition-prefix,
+ .input-group .condition-group-separator-label {
+ width: 60px;
+ text-align: center;
+ }
+ .input-group .condition-group-separator-label {
+ padding: 6px 12px;
+ font-weight: bold;
+ display: inline-block;
+ }
+ .input-group .condition-type,
+ .input-group .condition-operator {
+ width: auto;
+ text-align: center;
+ text-align-last: center;
+ }
+
+ .condition-group>.condition>div:first-child {
+ border-bottom-left-radius: 0;
+ }
+ .condition-group>.condition:nth-child(n+2)>div:first-child {
+ border-top-left-radius: 0;
+ }
+ .condition-group>.condition:nth-child(n+2)>div:last-child>button {
+ border-top-right-radius: 0;
+ }
+ .condition-group>.condition:nth-last-child(n+2)>div:last-child>button {
+ border-bottom-right-radius: 0;
+ }
+ .condition-group-options>.condition-add {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ .condition-groups>*:last-of-type {
+ display: none;
+ }
+
#custom-popup-css {
width: 100%;
min-height: 34px;
@@ -49,14 +102,134 @@
[data-browser=firefox-mobile] [data-show-for-browser~=firefox-mobile] {
display: initial;
}
+
+ @media screen and (max-width: 740px) {
+ .col-xs-6 {
+ float: none;
+ width: 100%;
+ }
+ .col-xs-6+.col-xs-6 {
+ margin-top: 15px;
+ }
+ }
</style>
</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 or use them in different contexts.
+ </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="form-group">
+ <label>Usage conditions</label>
+
+ <p class="help-block">
+ Usage conditions can be assigned such that certain profiles are automatically used in different contexts.
+ For example, when <a href="#popup-content-scanning">Popup Content Scanning</a> is enabled, different profiles can be used
+ depending on the level of the popup.
+ </p>
+
+ <p class="help-block">
+ Conditions are organized into groups which represent how the conditions are checked.
+ If all of the conditions in any group are met, then the profile will automatically be used for that context.
+ If no conditions are specified, the profile will only be used if it is selected as the <strong>Active profile</strong>.
+ </p>
+
+ <div class="condition-groups" id="profile-condition-groups"></div>
+ </div>
+ <div class="form-group">
+ <button class="btn btn-default" id="profile-add-condition-group">Add Condition Group</button>
+ </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>
+
+ <template id="condition-template"><div class="input-group condition">
+ <div class="input-group-addon condition-prefix"></div>
+ <div class="input-group-btn"><select class="form-control btn btn-default condition-type"><optgroup label="Type"></optgroup></select></div>
+ <div class="input-group-btn"><select class="form-control btn btn-default condition-operator"><optgroup label="Operator"></optgroup></select></div>
+ <input type="text" class="form-control" />
+ <div class="input-group-btn"><button class="btn btn-danger condition-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div>
+ </div></template>
+ <template id="condition-group-separator-template"><div class="input-group">
+ <div class="condition-group-separator-label">OR</div>
+ </div></template>
+ <template id="condition-group-options-template"><div class="condition-group-options">
+ <button class="btn btn-default condition-add"><span class="glyphicon glyphicon-plus"></span></button>
+ </div></template>
+ </div>
+
<div>
<h3>General Options</h3>
<div class="checkbox">
+ <label><input type="checkbox" id="enable"> Enable content scanning</label>
+ </div>
+
+ <div class="checkbox">
<label><input type="checkbox" id="show-usage-guide"> Show usage guide on startup</label>
</div>
@@ -107,6 +280,16 @@
</select>
</div>
+ <div class="form-group options-advanced">
+ <label for="audio-playback-volume">Audio playback volume <span class="label-light">(percent)</span></label>
+ <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
+ </div>
+
+ <div class="form-group options-advanced">
+ <label for="max-displayed-results">Maximum displayed results</label>
+ <input type="number" min="1" id="max-displayed-results" class="form-control">
+ </div>
+
<div class="form-group">
<div class="row">
<div class="col-xs-6">
@@ -130,36 +313,41 @@
</div>
<div class="form-group options-advanced">
- <label for="audio-playback-volume">Audio playback volume (percent)</label>
- <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
- </div>
-
- <div class="form-group options-advanced">
- <label for="max-displayed-results">Maximum displayed results</label>
- <input type="number" min="1" id="max-displayed-results" class="form-control">
- </div>
-
- <div class="form-group options-advanced">
- <label>Popup size (width &times; height, in pixels)</label>
<div class="row">
- <div class="col-xs-6"><input type="number" min="1" id="popup-width" class="form-control"></div>
- <div class="col-xs-6"><input type="number" min="1" id="popup-height" class="form-control"></div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Popup width <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="1" id="popup-width" class="form-control">
+ </div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Popup height <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="1" id="popup-height" class="form-control">
+ </div>
</div>
</div>
<div class="form-group options-advanced">
- <label>Popup offset (horizontal, vertical; in pixels)</label>
<div class="row">
- <div class="col-xs-6"><input type="number" min="0" id="popup-horizontal-offset" class="form-control"></div>
- <div class="col-xs-6"><input type="number" min="0" id="popup-vertical-offset" class="form-control"></div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Horizontal popup offset <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="0" id="popup-horizontal-offset" class="form-control">
+ </div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Vertical popup offset <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="0" id="popup-vertical-offset" class="form-control">
+ </div>
</div>
</div>
<div class="form-group options-advanced">
- <label>Popup offset for vertical text (horizontal, vertical; in pixels)</label>
<div class="row">
- <div class="col-xs-6"><input type="number" min="0" id="popup-horizontal-offset2" class="form-control"></div>
- <div class="col-xs-6"><input type="number" min="0" id="popup-vertical-offset2" class="form-control"></div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Horizontal popup offset for vertical text <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="0" id="popup-horizontal-offset2" class="form-control">
+ </div>
+ <div class="col-xs-6">
+ <label for="popup-display-mode">Vertical popup offset for vertical text <span class="label-light">(in pixels)</span></label>
+ <input type="number" min="0" id="popup-vertical-offset2" class="form-control">
+ </div>
</div>
</div>
@@ -193,24 +381,16 @@
</div>
<div class="checkbox options-advanced">
- <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of popup expressions</label>
- </div>
-
- <div class="checkbox">
- <label><input type="checkbox" id="enable-scanning-on-search-page"> Enable scanning on search page</label>
- </div>
-
- <div class="checkbox options-advanced">
<label><input type="checkbox" id="deep-dom-scan"> Deep DOM scan</label>
</div>
<div class="form-group options-advanced">
- <label for="scan-delay">Scan delay (in milliseconds)</label>
+ <label for="scan-delay">Scan delay <span class="label-light">(in milliseconds)</span></label>
<input type="number" min="1" id="scan-delay" class="form-control">
</div>
<div class="form-group options-advanced">
- <label for="scan-length">Scan length (in characters)</label>
+ <label for="scan-length">Scan length <span class="label-light">(in characters)</span></label>
<input type="number" min="1" id="scan-length" class="form-control">
</div>
@@ -223,9 +403,26 @@
<option value="shift">Shift</option>
</select>
</div>
+ </div>
- <div class="form-group options-advanced">
- <label for="popup-nesting-max-depth">Maximum nested popup depth</label>
+ <div id="popup-content-scanning">
+ <h3>Popup Content Scanning Options</h3>
+
+ <p class="help-block">
+ Yomichan is able to create additional popups in order to scan the content of other popups.
+ This feature can be enabled if the <strong>Maximum number of additional popups</strong> option is set to a value greater than 0.
+ </p>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="enable-scanning-on-search-page"> Enable scanning on search page</label>
+ </div>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of popup expressions</label>
+ </div>
+
+ <div class="form-group">
+ <label for="popup-nesting-max-depth">Maximum number of additional popups</label>
<input type="number" min="0" id="popup-nesting-max-depth" class="form-control">
</div>
</div>
@@ -265,7 +462,7 @@
<div id="dict-importer">
<p class="help-block">
Select a dictionary to import for use below. Please visit the Yomichan homepage to
- <a href="https://foosoft.net/projects/yomichan" target="_blank">download free dictionaries</a>
+ <a href="https://foosoft.net/projects/yomichan" target="_blank" rel="noopener">download free dictionaries</a>
for use with this extension and to learn about importing proprietary EPWING dictionaries.
</p>
<input type="file" id="dict-file">
@@ -291,7 +488,7 @@
<div data-show-for-browser="firefox firefox-mobile"><div class="alert alert-danger options-advanced">
On Firefox and Firefox for Android, the storage information feature may be hidden behind a browser flag.
- If you would like to enable this flag, open <a href="about:config" target="_blank">about:config</a> and search for the
+ If you would like to enable this flag, open <a href="about:config" target="_blank" rel="noopener">about:config</a> and search for the
<strong>dom.storageManager.enabled</strong> option. If this option has a value of <strong>false</strong>, toggling it to
<strong>true</strong> may allow storage information to be calculated.
</div></div>
@@ -313,9 +510,9 @@
</div>
<p class="help-block">
- Yomichan supports automatic flashcard creation for <a href="http://ankisrs.net/" target="_blank">Anki</a>, a free application
+ Yomichan supports automatic flashcard creation for <a href="https://apps.ankiweb.net/" target="_blank" rel="noopener">Anki</a>, a free application
designed to help you remember. This feature requires installation of the
- <a href="https://foosoft.net/projects/anki-connect/" target="_blank">AnkiConnect</a> plugin.
+ <a href="https://foosoft.net/projects/anki-connect/" target="_blank" rel="noopener">AnkiConnect</a> plugin.
</p>
<div class="alert alert-danger" id="anki-error"></div>
@@ -326,12 +523,12 @@
<div id="anki-general">
<div class="form-group">
- <label for="card-tags">Card tags (comma or space separated)</label>
+ <label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label>
<input type="text" id="card-tags" class="form-control">
</div>
<div class="form-group options-advanced">
- <label for="sentence-detection-extent">Sentence detection extent (in characters)</label>
+ <label for="sentence-detection-extent">Sentence detection extent <span class="label-light">(in characters)</span></label>
<input type="number" min="1" id="sentence-detection-extent" class="form-control">
</div>
@@ -349,7 +546,7 @@
</div>
<div class="form-group options-advanced">
- <label for="screenshot-quality">Screenshot quality (JPEG only)</label>
+ <label for="screenshot-quality">Screenshot quality <span class="label-light">(JPEG only)</span></label>
<input type="number" min="0" max="100" step="1" id="screenshot-quality" class="form-control">
</div>
@@ -408,12 +605,12 @@
<div class="options-advanced">
<p class="help-block">
- Fields are formatted using the <a href="http://handlebarsjs.com/">Handlebars.js</a> template rendering
+ Fields are formatted using the <a href="https://handlebarsjs.com/" target="_blank" rel="noopener">Handlebars.js</a> template rendering
engine. Advanced users can modify these templates for ultimate control of what information gets included in
their Anki cards. If you encounter problems with your changes you can always <a href="#" id="field-templates-reset">reset to default</a>
template settings.
</p>
- <textarea class="form-control" rows="10" id="field-templates"></textarea>
+ <textarea autocomplete="off" spellcheck="false" wrap="soft" class="form-control" rows="10" id="field-templates"></textarea>
</div>
</div>
</div>
@@ -431,14 +628,14 @@
countless hours that I have devoted to this extension.
</p>
<p>
- <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4DBTN9A3CUAFN" target="_blank"><img src="/bg/img/paypal.gif" alt></a>
+ <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4DBTN9A3CUAFN" target="_blank" rel="noopener"><img src="/bg/img/paypal.gif" alt></a>
</p>
</div>
<pre id="debug"></pre>
<div class="pull-right bottom-links">
- <small><a href="search.html">Search</a> &bull; <a href="https://foosoft.net/projects/yomichan/" target="_blank">Homepage</a> &bull; <a href="legal.html">Legal</a></small>
+ <small><a href="search.html">Search</a> &bull; <a href="https://foosoft.net/projects/yomichan/" target="_blank" rel="noopener">Homepage</a> &bull; <a href="legal.html">Legal</a></small>
</div>
</div>
@@ -450,12 +647,16 @@
<script src="/bg/js/anki.js"></script>
<script src="/bg/js/api.js"></script>
+ <script src="/bg/js/conditions.js"></script>
+ <script src="/bg/js/conditions-ui.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/options.js"></script>
+ <script src="/bg/js/profile-conditions.js"></script>
<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>