Add support creating profile usage conditions
This commit is contained in:
parent
e3fb9603e2
commit
8c4fb28a30
@ -16,11 +16,13 @@
|
|||||||
<script src="/bg/js/api.js"></script>
|
<script src="/bg/js/api.js"></script>
|
||||||
<script src="/bg/js/audio.js"></script>
|
<script src="/bg/js/audio.js"></script>
|
||||||
<script src="/bg/js/backend-api-forwarder.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/database.js"></script>
|
||||||
<script src="/bg/js/deinflector.js"></script>
|
<script src="/bg/js/deinflector.js"></script>
|
||||||
<script src="/bg/js/dictionary.js"></script>
|
<script src="/bg/js/dictionary.js"></script>
|
||||||
<script src="/bg/js/handlebars.js"></script>
|
<script src="/bg/js/handlebars.js"></script>
|
||||||
<script src="/bg/js/options.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/request.js"></script>
|
||||||
<script src="/bg/js/templates.js"></script>
|
<script src="/bg/js/templates.js"></script>
|
||||||
<script src="/bg/js/translator.js"></script>
|
<script src="/bg/js/translator.js"></script>
|
||||||
|
317
ext/bg/js/conditions-ui.js
Normal file
317
ext/bg/js/conditions-ui.js
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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();
|
||||||
|
}
|
||||||
|
};
|
117
ext/bg/js/conditions.js
Normal file
117
ext/bg/js/conditions.js
Normal file
@ -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 = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -329,6 +329,22 @@ function profileOptionsUpdateVersion(options) {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Global options
|
* 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 = [];
|
const optionsVersionUpdates = [];
|
||||||
@ -351,7 +367,8 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
|
|||||||
if (profiles.length === 0) {
|
if (profiles.length === 0) {
|
||||||
profiles.push({
|
profiles.push({
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
options: defaultProfileOptions
|
options: defaultProfileOptions,
|
||||||
|
conditionGroups: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,6 +386,9 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
|
|||||||
|
|
||||||
// Update profile options
|
// Update profile options
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
|
if (!Array.isArray(profile.conditionGroups)) {
|
||||||
|
profile.conditionGroups = [];
|
||||||
|
}
|
||||||
profile.options = profileOptionsUpdateVersion(profile.options);
|
profile.options = profileOptionsUpdateVersion(profile.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
85
ext/bg/js/profile-conditions.js
Normal file
85
ext/bg/js/profile-conditions.js
Normal file
@ -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: (value, optionValue) => (value === optionValue)
|
||||||
|
},
|
||||||
|
notEqual: {
|
||||||
|
name: '\u2260',
|
||||||
|
test: (value, optionValue) => (value !== optionValue)
|
||||||
|
},
|
||||||
|
lessThan: {
|
||||||
|
name: '<',
|
||||||
|
test: (value, optionValue) => (value < optionValue)
|
||||||
|
},
|
||||||
|
greaterThan: {
|
||||||
|
name: '>',
|
||||||
|
test: (value, optionValue) => (value > optionValue)
|
||||||
|
},
|
||||||
|
lessThanOrEqual: {
|
||||||
|
name: '\u2264',
|
||||||
|
test: (value, optionValue) => (value <= optionValue)
|
||||||
|
},
|
||||||
|
greaterThanOrEqual: {
|
||||||
|
name: '\u2265',
|
||||||
|
test: (value, optionValue) => (value >= 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: (value, transformedOptionValue) => (transformedOptionValue.indexOf(new URL(value).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: (value, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let currentProfileIndex = 0;
|
let currentProfileIndex = 0;
|
||||||
|
let profileConditionsContainer = null;
|
||||||
|
|
||||||
function getOptionsContext() {
|
function getOptionsContext() {
|
||||||
return {
|
return {
|
||||||
@ -81,6 +82,19 @@ async function profileFormWrite(optionsFull) {
|
|||||||
$('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
|
$('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
|
||||||
|
|
||||||
$('#profile-name').val(profile.name);
|
$('#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();
|
||||||
}
|
}
|
||||||
|
|
||||||
function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {
|
function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {
|
||||||
|
@ -34,6 +34,55 @@
|
|||||||
font-weight: normal;
|
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 {
|
#custom-popup-css {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
@ -71,7 +120,7 @@
|
|||||||
<h3>Profiles</h3>
|
<h3>Profiles</h3>
|
||||||
|
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
Profiles allow you to create multiple configurations and quickly switch between them.
|
Profiles allow you to create multiple configurations and quickly switch between them or use them in different contexts.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -100,6 +149,27 @@
|
|||||||
<input type="text" id="profile-name" class="form-control">
|
<input type="text" id="profile-name" class="form-control">
|
||||||
</div>
|
</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 fade" tabindex="-1" role="dialog" id="profile-copy-modal">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -136,6 +206,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -563,9 +647,12 @@
|
|||||||
|
|
||||||
<script src="/bg/js/anki.js"></script>
|
<script src="/bg/js/anki.js"></script>
|
||||||
<script src="/bg/js/api.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/dictionary.js"></script>
|
||||||
<script src="/bg/js/handlebars.js"></script>
|
<script src="/bg/js/handlebars.js"></script>
|
||||||
<script src="/bg/js/options.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/templates.js"></script>
|
||||||
<script src="/bg/js/util.js"></script>
|
<script src="/bg/js/util.js"></script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user