aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/bg/background.html1
-rw-r--r--ext/bg/js/backend.js40
-rw-r--r--ext/bg/js/profile-conditions.js256
-rw-r--r--ext/bg/js/settings/conditions-ui.js6
-rw-r--r--ext/bg/js/settings/main.js19
-rw-r--r--ext/bg/js/settings/profiles.js4
-rw-r--r--ext/bg/settings.html9
-rw-r--r--ext/mixed/js/environment.js114
8 files changed, 286 insertions, 163 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 7cb76ec3..ca35a3c6 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -21,6 +21,7 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/environment.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki.js"></script>
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 9936baf8..557ceb29 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -24,6 +24,7 @@
* ClipboardMonitor
* Database
* DictionaryImporter
+ * Environment
* JsonSchema
* Mecab
* ObjectPropertyAccessor
@@ -35,6 +36,7 @@
* optionsLoad
* optionsSave
* profileConditionsDescriptor
+ * profileConditionsDescriptorPromise
* requestJson
* requestText
* utilIsolate
@@ -42,6 +44,7 @@
class Backend {
constructor() {
+ this.environment = new Environment();
this.database = new Database();
this.dictionaryImporter = new DictionaryImporter();
this.translator = new Translator(this.database);
@@ -100,7 +103,7 @@ class Backend {
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}],
- ['getEnvironmentInfo', {async: true, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
+ ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}],
['getQueryParserTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetQueryParserTemplatesHtml.bind(this)}],
@@ -140,9 +143,12 @@ class Backend {
}, 1000);
this._updateBadge();
+ await this.environment.prepare();
await this.database.prepare();
await this.translator.prepare();
+ await profileConditionsDescriptorPromise;
+
this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET');
this.options = await optionsLoad();
@@ -635,15 +641,8 @@ class Backend {
});
}
- async _onApiGetEnvironmentInfo() {
- const browser = await Backend._getBrowser();
- const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
- return {
- browser,
- platform: {
- os: platform.os
- }
- };
+ _onApiGetEnvironmentInfo() {
+ return this.environment.getInfo();
}
async _onApiClipboardGet() {
@@ -659,7 +658,7 @@ class Backend {
being an extension with clipboard permissions. It effectively asks for the
non-extension permission for clipboard access.
*/
- const browser = await Backend._getBrowser();
+ const {browser} = this.environment.getInfo();
if (browser === 'firefox' || browser === 'firefox-mobile') {
return await navigator.clipboard.readText();
} else {
@@ -1211,23 +1210,4 @@ class Backend {
// Edge throws exception for no reason here.
}
}
-
- static async _getBrowser() {
- if (EXTENSION_IS_BROWSER_EDGE) {
- return 'edge';
- }
- if (typeof browser !== 'undefined') {
- try {
- const info = await browser.runtime.getBrowserInfo();
- if (info.name === 'Fennec') {
- return 'firefox-mobile';
- }
- } catch (e) {
- // NOP
- }
- return 'firefox';
- } else {
- return 'chrome';
- }
- }
}
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index 32309c64..97e09f1c 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -15,6 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/* global
+ * Environment
+ */
function _profileConditionTestDomain(urlDomain, domain) {
return (
@@ -36,135 +39,140 @@ function _profileConditionTestDomainList(url, domainList) {
return false;
}
-const _profileModifierKeys = [
- {optionValue: 'alt', name: 'Alt'},
- {optionValue: 'ctrl', name: 'Ctrl'},
- {optionValue: 'shift', name: 'Shift'}
-];
+let profileConditionsDescriptor = null;
-if (!hasOwn(window, 'netscape')) {
- _profileModifierKeys.push({optionValue: 'meta', name: 'Meta'});
-}
+const profileConditionsDescriptorPromise = (async () => {
+ const environment = new Environment();
+ await environment.prepare();
-const _profileModifierValueToName = new Map(
- _profileModifierKeys.map(({optionValue, name}) => [optionValue, name])
-);
+ const modifiers = environment.getInfo().modifiers;
+ const modifierSeparator = modifiers.separator;
+ const modifierKeyValues = modifiers.keys.map(
+ ({value, name}) => ({optionValue: value, name})
+ );
-const _profileModifierNameToValue = new Map(
- _profileModifierKeys.map(({optionValue, name}) => [name, optionValue])
-);
+ const modifierValueToName = new Map(
+ modifierKeyValues.map(({optionValue, name}) => [optionValue, name])
+ );
-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)
+ const modifierNameToValue = new Map(
+ modifierKeyValues.map(({optionValue, name}) => [name, optionValue])
+ );
+
+ 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) => _profileConditionTestDomainList(url, transformedOptionValue)
- },
- 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))
+ },
+ 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) => _profileConditionTestDomainList(url, transformedOptionValue)
+ },
+ 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))
+ }
}
- }
- },
- modifierKeys: {
- name: 'Modifier Keys',
- description: 'Use profile depending on the active modifier keys.',
- values: _profileModifierKeys,
- defaultOperator: 'are',
- operators: {
- are: {
- name: 'are',
- placeholder: 'Press one or more modifier keys here',
- defaultValue: [],
- type: 'keyMulti',
- transformInput: (optionValue) => optionValue
- .split(' + ')
- .filter((v) => v.length > 0)
- .map((v) => _profileModifierNameToValue.get(v)),
- transformReverse: (transformedOptionValue) => transformedOptionValue
- .map((v) => _profileModifierValueToName.get(v))
- .join(' + '),
- test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
- },
- areNot: {
- name: 'are not',
- placeholder: 'Press one or more modifier keys here',
- defaultValue: [],
- type: 'keyMulti',
- transformInput: (optionValue) => optionValue
- .split(' + ')
- .filter((v) => v.length > 0)
- .map((v) => _profileModifierNameToValue.get(v)),
- transformReverse: (transformedOptionValue) => transformedOptionValue
- .map((v) => _profileModifierValueToName.get(v))
- .join(' + '),
- test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
- },
- include: {
- name: 'include',
- type: 'select',
- defaultValue: 'alt',
- test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
- },
- notInclude: {
- name: 'don\'t include',
- type: 'select',
- defaultValue: 'alt',
- test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
+ },
+ modifierKeys: {
+ name: 'Modifier Keys',
+ description: 'Use profile depending on the active modifier keys.',
+ values: modifierKeyValues,
+ defaultOperator: 'are',
+ operators: {
+ are: {
+ name: 'are',
+ placeholder: 'Press one or more modifier keys here',
+ defaultValue: [],
+ type: 'keyMulti',
+ keySeparator: modifierSeparator,
+ transformInput: (optionValue) => optionValue
+ .split(modifierSeparator)
+ .filter((v) => v.length > 0)
+ .map((v) => modifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => modifierValueToName.get(v))
+ .join(modifierSeparator),
+ test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
+ },
+ areNot: {
+ name: 'are not',
+ placeholder: 'Press one or more modifier keys here',
+ defaultValue: [],
+ type: 'keyMulti',
+ keySeparator: modifierSeparator,
+ transformInput: (optionValue) => optionValue
+ .split(modifierSeparator)
+ .filter((v) => v.length > 0)
+ .map((v) => modifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => modifierValueToName.get(v))
+ .join(modifierSeparator),
+ test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
+ },
+ include: {
+ name: 'include',
+ type: 'select',
+ defaultValue: 'alt',
+ test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
+ },
+ notInclude: {
+ name: 'don\'t include',
+ type: 'select',
+ defaultValue: 'alt',
+ test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
+ }
}
}
- }
-};
+ };
+})();
diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index 0670de5a..031689a7 100644
--- a/ext/bg/js/settings/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -310,10 +310,14 @@ ConditionsUI.Condition = class Condition {
inputInner.prop('readonly', true);
let values = [];
+ let keySeparator = ' + ';
for (const object of objects) {
if (hasOwn(object, 'values')) {
values = object.values;
}
+ if (hasOwn(object, 'keySeparator')) {
+ keySeparator = object.keySeparator;
+ }
}
const pressedKeyIndices = new Set();
@@ -347,7 +351,7 @@ ConditionsUI.Condition = class Condition {
}
}
- const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + ');
+ const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(keySeparator);
inputInner.val(inputValue);
inputInner.change();
};
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index cf75d629..61395b1c 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -22,6 +22,7 @@
* ankiTemplatesInitialize
* ankiTemplatesUpdateValue
* apiForwardLogsToBackend
+ * apiGetEnvironmentInfo
* apiOptionsSave
* appearanceInitialize
* audioSettingsInitialize
@@ -285,6 +286,23 @@ function showExtensionInformation() {
node.textContent = `${manifest.name} v${manifest.version}`;
}
+async function settingsPopulateModifierKeys() {
+ const scanModifierKeySelect = document.querySelector('#scan-modifier-key');
+ scanModifierKeySelect.textContent = '';
+
+ const environment = await apiGetEnvironmentInfo();
+ const modifierKeys = [
+ {value: 'none', name: 'None'},
+ ...environment.modifiers.keys
+ ];
+ for (const {value, name} of modifierKeys) {
+ const option = document.createElement('option');
+ option.value = value;
+ option.textContent = name;
+ scanModifierKeySelect.appendChild(option);
+ }
+}
+
async function onReady() {
apiForwardLogsToBackend();
@@ -292,6 +310,7 @@ async function onReady() {
showExtensionInformation();
+ await settingsPopulateModifierKeys();
formSetupEventListeners();
appearanceInitialize();
await audioSettingsInitialize();
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index 3f4b1da7..bdf5a13d 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -23,6 +23,7 @@
* getOptionsFullMutable
* getOptionsMutable
* profileConditionsDescriptor
+ * profileConditionsDescriptorPromise
* settingsSaveOptions
* utilBackgroundIsolate
*/
@@ -98,6 +99,7 @@ async function profileFormWrite(optionsFull) {
profileConditionsContainer.cleanup();
}
+ await profileConditionsDescriptorPromise;
profileConditionsContainer = new ConditionsUI.Container(
profileConditionsDescriptor,
'popupLevel',
@@ -128,7 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi
}
async function profileOptionsUpdateTarget(optionsFull) {
- profileFormWrite(optionsFull);
+ await profileFormWrite(optionsFull);
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index a0981687..3ce91f12 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -412,13 +412,7 @@
<div class="form-group">
<label for="scan-modifier-key">Scan modifier key</label>
- <select class="form-control" id="scan-modifier-key">
- <option value="none">None</option>
- <option value="alt">Alt</option>
- <option value="ctrl">Ctrl</option>
- <option value="shift">Shift</option>
- <option data-hide-for-browser="firefox firefox-mobile" value="meta">Meta</option>
- </select>
+ <select class="form-control" id="scan-modifier-key"></select>
</div>
</div>
@@ -1131,6 +1125,7 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/environment.js"></script>
<script src="/mixed/js/api.js"></script>
<script src="/mixed/js/japanese.js"></script>
diff --git a/ext/mixed/js/environment.js b/ext/mixed/js/environment.js
new file mode 100644
index 00000000..e5bc20a7
--- /dev/null
+++ b/ext/mixed/js/environment.js
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 Yomichan Authors
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+
+class Environment {
+ constructor() {
+ this._cachedEnvironmentInfo = null;
+ }
+
+ async prepare() {
+ this._cachedEnvironmentInfo = await this._loadEnvironmentInfo();
+ }
+
+ getInfo() {
+ if (this._cachedEnvironmentInfo === null) { throw new Error('Not prepared'); }
+ return this._cachedEnvironmentInfo;
+ }
+
+ async _loadEnvironmentInfo() {
+ const browser = await this._getBrowser();
+ const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
+ const modifierInfo = this._getModifierInfo(browser, platform.os);
+ return {
+ browser,
+ platform: {
+ os: platform.os
+ },
+ modifiers: modifierInfo
+ };
+ }
+
+ async _getBrowser() {
+ if (EXTENSION_IS_BROWSER_EDGE) {
+ return 'edge';
+ }
+ if (typeof browser !== 'undefined') {
+ try {
+ const info = await browser.runtime.getBrowserInfo();
+ if (info.name === 'Fennec') {
+ return 'firefox-mobile';
+ }
+ } catch (e) {
+ // NOP
+ }
+ return 'firefox';
+ } else {
+ return 'chrome';
+ }
+ }
+
+ _getModifierInfo(browser, os) {
+ let osKeys;
+ let separator;
+ switch (os) {
+ case 'win':
+ separator = ' + ';
+ osKeys = [
+ ['alt', 'Alt'],
+ ['ctrl', 'Ctrl'],
+ ['shift', 'Shift'],
+ ['meta', 'Windows']
+ ];
+ break;
+ case 'mac':
+ separator = '';
+ osKeys = [
+ ['alt', '⌥'],
+ ['ctrl', '⌃'],
+ ['shift', '⇧'],
+ ['meta', '⌘']
+ ];
+ break;
+ case 'linux':
+ case 'openbsd':
+ case 'cros':
+ case 'android':
+ separator = ' + ';
+ osKeys = [
+ ['alt', 'Alt'],
+ ['ctrl', 'Ctrl'],
+ ['shift', 'Shift'],
+ ['meta', 'Super']
+ ];
+ break;
+ default:
+ throw new Error(`Invalid OS: ${os}`);
+ }
+
+ const isFirefox = (browser === 'firefox' || browser === 'firefox-mobile');
+ const keys = [];
+
+ for (const [value, name] of osKeys) {
+ // Firefox doesn't support event.metaKey on platforms other than macOS
+ if (value === 'meta' && isFirefox && os !== 'mac') { continue; }
+ keys.push({value, name});
+ }
+
+ return {keys, separator};
+ }
+}