Add apiModifySettings (#501)

* Update getProfile/getProfileFromContext to store this.options in a variable

* Add useSchema parameter to options getter functions

* Add apiModifySettings

* Use apiModifySettings instead of apiOptionsSet

* Remove apiOptionsSet

* Fix incorrect deleteCount check

* Require explicit scope for options

* Throw on invalid scope
This commit is contained in:
toasted-nutbread 2020-05-06 19:32:28 -04:00 committed by GitHub
parent 021ccb5ac3
commit bb2d9501af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 67 deletions

View File

@ -46,6 +46,7 @@
<script src="/bg/js/translator.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio-system.js"></script>
<script src="/mixed/js/object-property-accessor.js"></script>
<script src="/bg/js/background-main.js"></script>
</body>

View File

@ -26,6 +26,7 @@
* DictionaryImporter
* JsonSchema
* Mecab
* ObjectPropertyAccessor
* Translator
* conditionsTestValue
* dictTermsSort
@ -84,7 +85,6 @@ class Backend {
['optionsSchemaGet', {handler: this._onApiOptionsSchemaGet.bind(this), async: false}],
['optionsGet', {handler: this._onApiOptionsGet.bind(this), async: false}],
['optionsGetFull', {handler: this._onApiOptionsGetFull.bind(this), async: false}],
['optionsSet', {handler: this._onApiOptionsSet.bind(this), async: true}],
['optionsSave', {handler: this._onApiOptionsSave.bind(this), async: true}],
['kanjiFind', {handler: this._onApiKanjiFind.bind(this), async: true}],
['termsFind', {handler: this._onApiTermsFind.bind(this), async: true}],
@ -115,7 +115,8 @@ class Backend {
['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}],
['log', {handler: this._onApiLog.bind(this), async: false}],
['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}],
['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}]
['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}],
['modifySettings', {handler: this._onApiModifySettings.bind(this), async: true}]
]);
this._messageHandlersWithProgress = new Map([
['importDictionaryArchive', {handler: this._onApiImportDictionaryArchive.bind(this), async: true}],
@ -258,8 +259,9 @@ class Backend {
return this.optionsSchema;
}
getFullOptions() {
return this.options;
getFullOptions(useSchema=false) {
const options = this.options;
return useSchema ? JsonSchema.createProxy(options, this.optionsSchema) : options;
}
setFullOptions(options) {
@ -271,21 +273,22 @@ class Backend {
}
}
getOptions(optionsContext) {
return this.getProfile(optionsContext).options;
getOptions(optionsContext, useSchema=false) {
return this.getProfile(optionsContext, useSchema).options;
}
getProfile(optionsContext) {
const profiles = this.options.profiles;
getProfile(optionsContext, useSchema=false) {
const options = this.getFullOptions(useSchema);
const profiles = 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];
const profile = this.getProfileFromContext(options, optionsContext);
return profile !== null ? profile : options.profiles[options.profileCurrent];
}
getProfileFromContext(optionsContext) {
for (const profile of this.options.profiles) {
getProfileFromContext(options, optionsContext) {
for (const profile of options.profiles) {
const conditionGroups = profile.conditionGroups;
if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) {
return profile;
@ -413,46 +416,6 @@ class Backend {
return this.getFullOptions();
}
async _onApiOptionsSet({changedOptions, optionsContext, source}) {
const options = this.getOptions(optionsContext);
function getValuePaths(obj) {
const valuePaths = [];
const nodes = [{obj, path: []}];
while (nodes.length > 0) {
const node = nodes.pop();
for (const key of Object.keys(node.obj)) {
const path = node.path.concat(key);
const obj2 = node.obj[key];
if (obj2 !== null && typeof obj2 === 'object') {
nodes.unshift({obj: obj2, path});
} else {
valuePaths.push([obj2, path]);
}
}
}
return valuePaths;
}
function modifyOption(path, value) {
let pivot = options;
for (const key of path.slice(0, -1)) {
if (!hasOwn(pivot, key)) {
return false;
}
pivot = pivot[key];
}
pivot[path[path.length - 1]] = value;
return true;
}
for (const [value, path] of getValuePaths(changedOptions)) {
modifyOption(path, value);
}
await this._onApiOptionsSave({source});
}
async _onApiOptionsSave({source}) {
const options = this.getFullOptions();
await optionsSave(options);
@ -829,6 +792,20 @@ class Backend {
await this.database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress);
}
async _onApiModifySettings({targets, source}) {
const results = [];
for (const target of targets) {
try {
this._modifySetting(target);
results.push({result: true});
} catch (e) {
results.push({error: errorToJson(e)});
}
}
await this._onApiOptionsSave({source});
return results;
}
// Command handlers
_createActionListenerPort(port, sender, handlers) {
@ -988,6 +965,63 @@ class Backend {
// Utilities
_getModifySettingObject(target) {
const scope = target.scope;
switch (scope) {
case 'profile':
if (!isObject(target.optionsContext)) { throw new Error('Invalid optionsContext'); }
return this.getOptions(target.optionsContext, true);
case 'global':
return this.getFullOptions(true);
default:
throw new Error(`Invalid scope: ${scope}`);
}
}
async _modifySetting(target) {
const options = this._getModifySettingObject(target);
const accessor = new ObjectPropertyAccessor(options);
const action = target.action;
switch (action) {
case 'set':
{
const {path, value} = target;
if (typeof path !== 'string') { throw new Error('Invalid path'); }
accessor.set(ObjectPropertyAccessor.getPathArray(path), value);
}
break;
case 'delete':
{
const {path} = target;
if (typeof path !== 'string') { throw new Error('Invalid path'); }
accessor.delete(ObjectPropertyAccessor.getPathArray(path));
}
break;
case 'swap':
{
const {path1, path2} = target;
if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
}
break;
case 'splice':
{
const {path, start, deleteCount, items} = target;
if (typeof path !== 'string') { throw new Error('Invalid path'); }
if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
if (!Array.isArray(items)) { throw new Error('Invalid items'); }
const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
array.splice(start, deleteCount, ...items);
}
break;
default:
throw new Error(`Unknown action: ${action}`);
}
}
_validatePrivilegedMessageSender(sender) {
const url = sender.url;
if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) {

View File

@ -18,7 +18,7 @@
/* global
* QueryParserGenerator
* TextScanner
* apiOptionsSet
* apiModifySettings
* apiTermsFind
* apiTextParse
* docSentenceExtract
@ -72,8 +72,14 @@ class QueryParser extends TextScanner {
}
onParserChange(e) {
const selectedParser = e.target.value;
apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext());
const value = e.target.value;
apiModifySettings([{
action: 'set',
path: 'parsing.selectedParser',
value,
scope: 'profile',
optionsContext: this.getOptionsContext()
}], 'search');
}
getMouseEventListeners() {
@ -92,8 +98,14 @@ class QueryParser extends TextScanner {
refreshSelectedParser() {
if (this.parseResults.length > 0) {
if (!this.getParseResult()) {
const selectedParser = this.parseResults[0].id;
apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext());
const value = this.parseResults[0].id;
apiModifySettings([{
action: 'set',
path: 'parsing.selectedParser',
value,
scope: 'profile',
optionsContext: this.getOptionsContext()
}], 'search');
}
}
}

View File

@ -21,7 +21,7 @@
* Display
* QueryParser
* apiClipboardGet
* apiOptionsSet
* apiModifySettings
* apiTermsFind
* wanakana
*/
@ -252,13 +252,19 @@ class DisplaySearch extends Display {
}
onWanakanaEnableChange(e) {
const enableWanakana = e.target.checked;
if (enableWanakana) {
const value = e.target.checked;
if (value) {
wanakana.bind(this.query);
} else {
wanakana.unbind(this.query);
}
apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext());
apiModifySettings([{
action: 'set',
path: 'general.enableWanakana',
value,
scope: 'profile',
optionsContext: this.getOptionsContext()
}], 'search');
}
onClipboardMonitorEnableChange(e) {
@ -268,7 +274,13 @@ class DisplaySearch extends Display {
(granted) => {
if (granted) {
this.clipboardMonitor.start();
apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());
apiModifySettings([{
action: 'set',
path: 'general.enableClipboardMonitor',
value: true,
scope: 'profile',
optionsContext: this.getOptionsContext()
}], 'search');
} else {
e.target.checked = false;
}
@ -276,7 +288,13 @@ class DisplaySearch extends Display {
);
} else {
this.clipboardMonitor.stop();
apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());
apiModifySettings([{
action: 'set',
path: 'general.enableClipboardMonitor',
value: false,
scope: 'profile',
optionsContext: this.getOptionsContext()
}], 'search');
}
}

View File

@ -28,10 +28,6 @@ function apiOptionsGetFull() {
return _apiInvoke('optionsGetFull');
}
function apiOptionsSet(changedOptions, optionsContext, source) {
return _apiInvoke('optionsSet', {changedOptions, optionsContext, source});
}
function apiOptionsSave(source) {
return _apiInvoke('optionsSave', {source});
}
@ -160,6 +156,10 @@ function apiDeleteDictionary(dictionaryName, onProgress) {
return _apiInvokeWithProgress('deleteDictionary', {dictionaryName}, onProgress);
}
function apiModifySettings(targets, source) {
return _apiInvoke('modifySettings', {targets, source});
}
function _apiCreateActionPort(timeout=5000) {
return new Promise((resolve, reject) => {
let timer = null;