aboutsummaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-01-04 11:54:54 -0800
committerAlex Yatskov <alex@foosoft.net>2020-01-04 11:54:54 -0800
commit2a12036ca305044291f1f4105d6a8d249848b210 (patch)
tree5cfd4a3d837bf99730233a805d72395c8c61fc07 /ext/bg
parent9105cb5618cfdd14c2bc37cd22db2b360fe8cd52 (diff)
parent174b92366577b0a638003b15e2d73fdc91cd62c3 (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html5
-rw-r--r--ext/bg/context.html2
-rw-r--r--ext/bg/css/settings.css4
-rw-r--r--ext/bg/data/options-schema.json533
-rw-r--r--ext/bg/js/anki.js4
-rw-r--r--ext/bg/js/api.js498
-rw-r--r--ext/bg/js/audio.js43
-rw-r--r--ext/bg/js/backend-api-forwarder.js4
-rw-r--r--ext/bg/js/backend.js614
-rw-r--r--ext/bg/js/conditions.js4
-rw-r--r--ext/bg/js/context.js4
-rw-r--r--ext/bg/js/database.js57
-rw-r--r--ext/bg/js/deinflector.js4
-rw-r--r--ext/bg/js/dictionary.js12
-rw-r--r--ext/bg/js/handlebars.js15
-rw-r--r--ext/bg/js/json-schema.js428
-rw-r--r--ext/bg/js/mecab.js4
-rw-r--r--ext/bg/js/options.js32
-rw-r--r--ext/bg/js/page-exit-prevention.js4
-rw-r--r--ext/bg/js/profile-conditions.js4
-rw-r--r--ext/bg/js/request.js4
-rw-r--r--ext/bg/js/search-frontend.js8
-rw-r--r--ext/bg/js/search-query-parser.js15
-rw-r--r--ext/bg/js/search.js144
-rw-r--r--ext/bg/js/settings/anki-templates.js40
-rw-r--r--ext/bg/js/settings/anki.js6
-rw-r--r--ext/bg/js/settings/audio-ui.js (renamed from ext/bg/js/audio-ui.js)44
-rw-r--r--ext/bg/js/settings/audio.js52
-rw-r--r--ext/bg/js/settings/backup.js370
-rw-r--r--ext/bg/js/settings/conditions-ui.js (renamed from ext/bg/js/conditions-ui.js)4
-rw-r--r--ext/bg/js/settings/dictionaries.js57
-rw-r--r--ext/bg/js/settings/main.js40
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js47
-rw-r--r--ext/bg/js/settings/popup-preview.js4
-rw-r--r--ext/bg/js/settings/profiles.js20
-rw-r--r--ext/bg/js/settings/storage.js4
-rw-r--r--ext/bg/js/templates.js8
-rw-r--r--ext/bg/js/translator.js12
-rw-r--r--ext/bg/js/util.js105
-rw-r--r--ext/bg/legal.html4
-rw-r--r--ext/bg/search.html3
-rw-r--r--ext/bg/settings-popup-preview.html4
-rw-r--r--ext/bg/settings.html132
43 files changed, 2573 insertions, 829 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 5a6970c3..af87eddb 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -12,7 +12,7 @@
<link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
</head>
<body>
- <div id="clipboard-paste-target" contenteditable="true"></div>
+ <textarea id="clipboard-paste-target"></textarea>
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/jszip.min.js"></script>
@@ -22,8 +22,8 @@
<script src="/mixed/js/dom.js"></script>
<script src="/bg/js/anki.js"></script>
- <script src="/bg/js/mecab.js"></script>
<script src="/bg/js/api.js"></script>
+ <script src="/bg/js/mecab.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>
@@ -31,6 +31,7 @@
<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/json-schema.js"></script>
<script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script>
diff --git a/ext/bg/context.html b/ext/bg/context.html
index eda09a68..0e50ed7c 100644
--- a/ext/bg/context.html
+++ b/ext/bg/context.html
@@ -180,8 +180,8 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/api.js"></script>
- <script src="/bg/js/api.js"></script>
<script src="/bg/js/options.js"></script>
<script src="/bg/js/util.js"></script>
diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index 8adae47c..63cead6b 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json
new file mode 100644
index 00000000..c086052b
--- /dev/null
+++ b/ext/bg/data/options-schema.json
@@ -0,0 +1,533 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": [
+ "version",
+ "profiles",
+ "profileCurrent",
+ "global"
+ ],
+ "properties": {
+ "version": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "profiles": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "conditionGroups",
+ "options"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "default": "Default"
+ },
+ "conditionGroups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "conditions"
+ ],
+ "properties": {
+ "conditions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "type",
+ "operator",
+ "value"
+ ],
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "operator": {
+ "type": "string"
+ },
+ "value": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "options": {
+ "type": "object",
+ "required": [
+ "version",
+ "general",
+ "audio",
+ "scanning",
+ "dictionaries",
+ "parsing",
+ "anki"
+ ],
+ "properties": {
+ "version": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "general": {
+ "type": "object",
+ "required": [
+ "enable",
+ "resultOutputMode",
+ "debugInfo",
+ "maxResults",
+ "showAdvanced",
+ "popupDisplayMode",
+ "popupWidth",
+ "popupHeight",
+ "popupHorizontalOffset",
+ "popupVerticalOffset",
+ "popupHorizontalOffset2",
+ "popupVerticalOffset2",
+ "popupHorizontalTextPosition",
+ "popupVerticalTextPosition",
+ "showGuide",
+ "compactTags",
+ "compactGlossaries",
+ "mainDictionary",
+ "popupTheme",
+ "popupOuterTheme",
+ "customPopupCss",
+ "customPopupOuterCss",
+ "enableWanakana",
+ "enableClipboardMonitor"
+ ],
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "default": true
+ },
+ "resultOutputMode": {
+ "type": "string",
+ "enum": ["group", "merge", "split"],
+ "default": "group"
+ },
+ "debugInfo": {
+ "type": "boolean",
+ "default": false
+ },
+ "maxResults": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 32
+ },
+ "showAdvanced": {
+ "type": "boolean",
+ "default": false
+ },
+ "popupDisplayMode": {
+ "type": "string",
+ "enum": ["default", "full-width"],
+ "default": "default"
+ },
+ "popupWidth": {
+ "type": "number",
+ "minimum": 0,
+ "default": 400
+ },
+ "popupHeight": {
+ "type": "number",
+ "minimum": 0,
+ "default": 250
+ },
+ "popupHorizontalOffset": {
+ "type": "number",
+ "default": 0
+ },
+ "popupVerticalOffset": {
+ "type": "number",
+ "default": 10
+ },
+ "popupHorizontalOffset2": {
+ "type": "number",
+ "default": 10
+ },
+ "popupVerticalOffset2": {
+ "type": "number",
+ "default": 0
+ },
+ "popupHorizontalTextPosition": {
+ "type": "string",
+ "enum": ["below", "above"],
+ "default": "below"
+ },
+ "popupVerticalTextPosition": {
+ "type": "string",
+ "enum": ["default", "before", "after", "left", "right"],
+ "default": "before"
+ },
+ "showGuide": {
+ "type": "boolean",
+ "default": true
+ },
+ "compactTags": {
+ "type": "boolean",
+ "default": false
+ },
+ "compactGlossaries": {
+ "type": "boolean",
+ "default": false
+ },
+ "mainDictionary": {
+ "type": "string"
+ },
+ "popupTheme": {
+ "type": "string",
+ "enum": ["default", "dark"],
+ "default": "default"
+ },
+ "popupOuterTheme": {
+ "type": "string",
+ "enum": ["default", "dark", "auto"],
+ "default": "default"
+ },
+ "customPopupCss": {
+ "type": "string",
+ "default": ""
+ },
+ "customPopupOuterCss": {
+ "type": "string",
+ "default": ""
+ },
+ "enableWanakana": {
+ "type": "boolean",
+ "default": true
+ },
+ "enableClipboardMonitor": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "audio": {
+ "type": "object",
+ "required": [
+ "enabled",
+ "sources",
+ "volume",
+ "autoPlay",
+ "customSourceUrl",
+ "textToSpeechVoice"
+ ],
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "default": true
+ },
+ "sources": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "jpod101",
+ "jpod101-alternate",
+ "jisho",
+ "text-to-speech",
+ "text-to-speech-reading",
+ "custom"
+ ],
+ "default": "jpod101"
+ }
+ },
+ "volume": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 100,
+ "default": 100
+ },
+ "autoPlay": {
+ "type": "boolean",
+ "default": false
+ },
+ "customSourceUrl": {
+ "type": "string",
+ "default": ""
+ },
+ "textToSpeechVoice": {
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "scanning": {
+ "type": "object",
+ "required": [
+ "middleMouse",
+ "touchInputEnabled",
+ "selectText",
+ "alphanumeric",
+ "autoHideResults",
+ "delay",
+ "length",
+ "modifier",
+ "deepDomScan",
+ "popupNestingMaxDepth",
+ "enablePopupSearch",
+ "enableOnPopupExpressions",
+ "enableOnSearchPage"
+ ],
+ "properties": {
+ "middleMouse": {
+ "type": "boolean",
+ "default": true
+ },
+ "touchInputEnabled": {
+ "type": "boolean",
+ "default": true
+ },
+ "selectText": {
+ "type": "boolean",
+ "default": true
+ },
+ "alphanumeric": {
+ "type": "boolean",
+ "default": true
+ },
+ "autoHideResults": {
+ "type": "boolean",
+ "default": false
+ },
+ "delay": {
+ "type": "number",
+ "minimum": 0,
+ "default": 20
+ },
+ "length": {
+ "type": "integer",
+ "minimum": 1,
+ "default": 10
+ },
+ "modifier": {
+ "type": "string",
+ "enum": ["none", "alt", "ctrl", "shift"],
+ "default": "shift"
+ },
+ "deepDomScan": {
+ "type": "boolean",
+ "default": false
+ },
+ "popupNestingMaxDepth": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "enablePopupSearch": {
+ "type": "boolean",
+ "default": false
+ },
+ "enableOnPopupExpressions": {
+ "type": "boolean",
+ "default": false
+ },
+ "enableOnSearchPage": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "dictionaries": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": [
+ "priority",
+ "enabled",
+ "allowSecondarySearches"
+ ],
+ "properties": {
+ "priority": {
+ "type": "number",
+ "default": 0
+ },
+ "enabled": {
+ "type": "boolean",
+ "default": true
+ },
+ "allowSecondarySearches": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ },
+ "parsing": {
+ "type": "object",
+ "required": [
+ "enableScanningParser",
+ "enableMecabParser",
+ "selectedParser",
+ "readingMode"
+ ],
+ "properties": {
+ "enableScanningParser": {
+ "type": "boolean",
+ "default": true
+ },
+ "enableMecabParser": {
+ "type": "boolean",
+ "default": false
+ },
+ "selectedParser": {
+ "type": ["string", "null"],
+ "default": null
+ },
+ "readingMode": {
+ "type": "string",
+ "enum": ["hiragana", "katakana", "romaji"],
+ "default": "hiragana"
+ }
+ }
+ },
+ "anki": {
+ "type": "object",
+ "required": [
+ "enable",
+ "server",
+ "tags",
+ "sentenceExt",
+ "screenshot",
+ "terms",
+ "kanji",
+ "fieldTemplates"
+ ],
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "default": false
+ },
+ "server": {
+ "type": "string",
+ "default": "http://127.0.0.1:8765"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "default": [
+ "yomichan"
+ ]
+ },
+ "sentenceExt": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 200
+ },
+ "screenshot": {
+ "type": "object",
+ "required": [
+ "format",
+ "quality"
+ ],
+ "properties": {
+ "format": {
+ "type": "string",
+ "enum": ["png", "jpeg"],
+ "default": "png"
+ },
+ "quality": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100,
+ "default": 92
+ }
+ }
+ },
+ "terms": {
+ "type": "object",
+ "required": [
+ "deck",
+ "model",
+ "fields"
+ ],
+ "properties": {
+ "deck": {
+ "type": "string",
+ "default": ""
+ },
+ "model": {
+ "type": "string",
+ "default": ""
+ },
+ "fields": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "default": ""
+ }
+ }
+ }
+ },
+ "kanji": {
+ "type": "object",
+ "required": [
+ "deck",
+ "model",
+ "fields"
+ ],
+ "properties": {
+ "deck": {
+ "type": "string",
+ "default": ""
+ },
+ "model": {
+ "type": "string",
+ "default": ""
+ },
+ "fields": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "default": ""
+ }
+ }
+ }
+ },
+ "fieldTemplates": {
+ "type": ["string", "null"],
+ "default": null
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "profileCurrent": {
+ "type": "integer",
+ "minimum": 0,
+ "default": 0
+ },
+ "global": {
+ "type": "object",
+ "required": [
+ "database"
+ ],
+ "properties": {
+ "database": {
+ "type": "object",
+ "required": [
+ "prefixWildcardsSupported"
+ ],
+ "properties": {
+ "prefixWildcardsSupported": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 17b93620..10a07061 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index b489b8d2..906aaa30 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,491 +13,39 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-function apiOptionsGet(optionsContext) {
- return utilBackend().getOptions(optionsContext);
+function apiTemplateRender(template, data, dynamic) {
+ return _apiInvoke('templateRender', {data, template, dynamic});
}
-async function apiOptionsSet(changedOptions, optionsContext, source) {
- const options = await apiOptionsGet(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 obj = node.obj[key];
- if (obj !== null && typeof obj === 'object') {
- nodes.unshift({obj, path});
- } else {
- valuePaths.push([obj, path]);
- }
- }
- }
- return valuePaths;
- }
-
- function modifyOption(path, value, options) {
- 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, options);
- }
-
- await apiOptionsSave(source);
-}
-
-function apiOptionsGetFull() {
- return utilBackend().getFullOptions();
-}
-
-async function apiOptionsSave(source) {
- const backend = utilBackend();
- const options = await apiOptionsGetFull();
- await optionsSave(options);
- backend.onOptionsUpdated(source);
-}
-
-async function apiTermsFind(text, details, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const [definitions, length] = await utilBackend().translator.findTerms(text, details, options);
- definitions.splice(options.general.maxResults);
- return {length, definitions};
+function apiAudioGetUrl(definition, source, optionsContext) {
+ return _apiInvoke('audioGetUrl', {definition, source, optionsContext});
}
-async function apiTextParse(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const translator = utilBackend().translator;
-
- const results = [];
- while (text.length > 0) {
- const term = [];
- const [definitions, sourceLength] = await translator.findTermsInternal(
- text.slice(0, options.scanning.length),
- dictEnabledSet(options),
- options.scanning.alphanumeric,
- {}
- );
- if (definitions.length > 0) {
- dictTermsSort(definitions);
- const {expression, reading} = definitions[0];
- const source = text.slice(0, sourceLength);
- for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
- }
- text = text.slice(source.length);
- } else {
- const reading = jpConvertReading(text[0], null, options.parsing.readingMode);
- term.push({text: text[0], reading});
- text = text.slice(1);
- }
- results.push(term);
- }
- return results;
-}
-
-async function apiTextParseMecab(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const mecab = utilBackend().mecab;
-
- const results = {};
- const rawResults = await mecab.parseText(text);
- for (const mecabName in rawResults) {
- const result = [];
- for (const parsedLine of rawResults[mecabName]) {
- for (const {expression, reading, source} of parsedLine) {
- const term = [];
- if (expression !== null && reading !== null) {
- for (const {text, furigana} of jpDistributeFuriganaInflected(
- expression,
- jpKatakanaToHiragana(reading),
- source
- )) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
+function _apiInvoke(action, params={}) {
+ const data = {action, params};
+ return new Promise((resolve, reject) => {
+ try {
+ const callback = (response) => {
+ if (response !== null && typeof response === 'object') {
+ if (typeof response.error !== 'undefined') {
+ reject(jsonToError(response.error));
+ } else {
+ resolve(response.result);
}
} else {
- const reading = jpConvertReading(source, null, options.parsing.readingMode);
- term.push({text: source, reading});
- }
- result.push(term);
- }
- result.push([{text: '\n'}]);
- }
- results[mecabName] = result;
- }
- return results;
-}
-
-async function apiKanjiFind(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const definitions = await utilBackend().translator.findKanji(text, options);
- definitions.splice(options.general.maxResults);
- return definitions;
-}
-
-async function apiDefinitionAdd(definition, mode, context, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
-
- if (mode !== 'kanji') {
- await audioInject(
- definition,
- options.anki.terms.fields,
- options.audio.sources,
- optionsContext
- );
- }
-
- if (context && context.screenshot) {
- await apiInjectScreenshot(
- definition,
- options.anki.terms.fields,
- context.screenshot
- );
- }
-
- const note = await dictNoteFormat(definition, mode, options);
- return utilBackend().anki.addNote(note);
-}
-
-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, options);
- notes.push(note);
- }
- }
-
- const cannotAdd = [];
- const anki = utilBackend().anki;
- const results = await anki.canAddNotes(notes);
- for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {
- const state = {};
- for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) {
- const index = resultBase + modeOffset;
- const result = results[index];
- const info = {canAdd: result};
- state[modes[modeOffset]] = info;
- if (!result) {
- cannotAdd.push([notes[index], info]);
- }
- }
-
- states.push(state);
- }
-
- if (cannotAdd.length > 0) {
- const noteIdsArray = await anki.findNoteIds(cannotAdd.map((e) => e[0]));
- for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
- const noteIds = noteIdsArray[i];
- if (noteIds.length > 0) {
- cannotAdd[i][1].noteId = noteIds[0];
+ const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`;
+ reject(new Error(`${message} (${JSON.stringify(data)})`));
}
- }
- }
- } catch (e) {
- // NOP
- }
-
- return states;
-}
-
-async function apiNoteView(noteId) {
- return utilBackend().anki.guiBrowse(`nid:${noteId}`);
-}
-
-async function apiTemplateRender(template, data, dynamic) {
- if (dynamic) {
- return handlebarsRenderDynamic(template, data);
- } else {
- return handlebarsRenderStatic(template, data);
- }
-}
-
-async function apiCommandExec(command, params) {
- const handlers = apiCommandExec.handlers;
- if (hasOwn(handlers, command)) {
- const handler = handlers[command];
- handler(params);
- }
-}
-apiCommandExec.handlers = {
- search: async (params) => {
- const url = chrome.runtime.getURL('/bg/search.html');
- if (!(params && params.newTab)) {
- try {
- const tab = await apiFindTab(1000, (url2) => (
- url2 !== null &&
- url2.startsWith(url) &&
- (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
- ));
- if (tab !== null) {
- await apiFocusTab(tab);
- return;
- }
- } catch (e) {
- // NOP
- }
- }
- chrome.tabs.create({url});
- },
-
- help: () => {
- chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
- },
-
- options: (params) => {
- if (!(params && params.newTab)) {
- chrome.runtime.openOptionsPage();
- } else {
- const manifest = chrome.runtime.getManifest();
- const url = chrome.runtime.getURL(manifest.options_ui.page);
- chrome.tabs.create({url});
- }
- },
-
- toggle: async () => {
- const optionsContext = {
- depth: 0,
- url: window.location.href
- };
- const options = await apiOptionsGet(optionsContext);
- options.general.enable = !options.general.enable;
- await apiOptionsSave('popup');
- }
-};
-
-async function apiAudioGetUrl(definition, source, optionsContext) {
- return audioGetUrl(definition, source, optionsContext);
-}
-
-async function apiInjectScreenshot(definition, fields, screenshot) {
- let usesScreenshot = false;
- for (const name in fields) {
- if (fields[name].includes('{screenshot}')) {
- usesScreenshot = true;
- break;
- }
- }
-
- if (!usesScreenshot) {
- return;
- }
-
- const dateToString = (date) => {
- const year = date.getUTCFullYear();
- const month = date.getUTCMonth().toString().padStart(2, '0');
- const day = date.getUTCDate().toString().padStart(2, '0');
- const hours = date.getUTCHours().toString().padStart(2, '0');
- const minutes = date.getUTCMinutes().toString().padStart(2, '0');
- const seconds = date.getUTCSeconds().toString().padStart(2, '0');
- return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
- };
-
- const now = new Date(Date.now());
- const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`;
- const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, '');
-
- try {
- await utilBackend().anki.storeMediaFile(filename, data);
- } catch (e) {
- return;
- }
-
- definition.screenshotFileName = filename;
-}
-
-function apiScreenshotGet(options, sender) {
- if (!(sender && sender.tab)) {
- return Promise.resolve();
- }
-
- const windowId = sender.tab.windowId;
- return new Promise((resolve) => {
- chrome.tabs.captureVisibleTab(windowId, options, (dataUrl) => resolve(dataUrl));
- });
-}
-
-function apiForward(action, params, sender) {
- if (!(sender && sender.tab)) {
- return Promise.resolve();
- }
-
- const tabId = sender.tab.id;
- return new Promise((resolve) => {
- chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response));
- });
-}
-
-function apiFrameInformationGet(sender) {
- const frameId = sender.frameId;
- return Promise.resolve({frameId});
-}
-
-function apiInjectStylesheet(css, sender) {
- if (!sender.tab) {
- return Promise.reject(new Error('Invalid tab'));
- }
-
- const tabId = sender.tab.id;
- const frameId = sender.frameId;
- const details = {
- code: css,
- runAt: 'document_start',
- cssOrigin: 'user',
- allFrames: false
- };
- if (typeof frameId === 'number') {
- details.frameId = frameId;
- }
-
- return new Promise((resolve, reject) => {
- chrome.tabs.insertCSS(tabId, details, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
-}
-
-async function apiGetEnvironmentInfo() {
- const browser = await apiGetBrowser();
- const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
- return {
- browser,
- platform: {
- os: platform.os
- }
- };
-}
-
-async function apiGetBrowser() {
- 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';
- }
+ };
+ const backend = window.yomichanBackend;
+ backend.onMessage({action, params}, null, callback);
} catch (e) {
- // NOP
+ reject(e);
+ yomichan.triggerOrphaned(e);
}
- return 'firefox';
- } else {
- return 'chrome';
- }
-}
-
-function apiGetTabUrl(tab) {
- return new Promise((resolve) => {
- chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
- let url = null;
- if (!chrome.runtime.lastError) {
- url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null);
- if (url !== null && typeof url !== 'string') {
- url = null;
- }
- }
- resolve({tab, url});
- });
});
}
-
-async function apiFindTab(timeout, checkUrl) {
- // This function works around the need to have the "tabs" permission to access tab.url.
- const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));
- let matchPromiseResolve = null;
- const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; });
-
- const checkTabUrl = ({tab, url}) => {
- if (checkUrl(url, tab)) {
- matchPromiseResolve(tab);
- }
- };
-
- const promises = [];
- for (const tab of tabs) {
- const promise = apiGetTabUrl(tab);
- promise.then(checkTabUrl);
- promises.push(promise);
- }
-
- const racePromises = [
- matchPromise,
- Promise.all(promises).then(() => null)
- ];
- if (typeof timeout === 'number') {
- racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout)));
- }
-
- return await Promise.race(racePromises);
-}
-
-async function apiFocusTab(tab) {
- await new Promise((resolve, reject) => {
- chrome.tabs.update(tab.id, {active: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
- });
- });
-
- if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) {
- // Windows not supported (e.g. on Firefox mobile)
- return;
- }
-
- try {
- const tabWindow = await new Promise((resolve) => {
- chrome.windows.get(tab.windowId, {}, (tabWindow) => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(tabWindow); }
- });
- });
- if (!tabWindow.focused) {
- await new Promise((resolve, reject) => {
- chrome.windows.update(tab.windowId, {focused: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
- });
- });
- }
- } catch (e) {
- // Edge throws exception for no reason here.
- }
-}
-
-async function apiClipboardGet() {
- const clipboardPasteTarget = utilBackend().clipboardPasteTarget;
- clipboardPasteTarget.innerText = '';
- clipboardPasteTarget.focus();
- document.execCommand('paste');
- return clipboardPasteTarget.innerText;
-}
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index dc0ba5eb..36ac413b 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,12 +13,12 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const audioUrlBuilders = {
- 'jpod101': async (definition) => {
+const audioUrlBuilders = new Map([
+ ['jpod101', async (definition) => {
let kana = definition.reading;
let kanji = definition.expression;
@@ -36,8 +36,8 @@ const audioUrlBuilders = {
}
return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- },
- 'jpod101-alternate': async (definition) => {
+ }],
+ ['jpod101-alternate', async (definition) => {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
@@ -61,8 +61,8 @@ const audioUrlBuilders = {
}
throw new Error('Failed to find audio URL');
- },
- 'jisho': async (definition) => {
+ }],
+ ['jisho', async (definition) => {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
@@ -85,37 +85,34 @@ const audioUrlBuilders = {
}
throw new Error('Failed to find audio URL');
- },
- 'text-to-speech': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['text-to-speech', async (definition, options) => {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- },
- 'text-to-speech-reading': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['text-to-speech-reading', async (definition, options) => {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- },
- 'custom': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['custom', async (definition, options) => {
const customSourceUrl = options.audio.customSourceUrl;
return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));
- }
-};
+ }]
+]);
-async function audioGetUrl(definition, mode, optionsContext, download) {
- if (hasOwn(audioUrlBuilders, mode)) {
- const handler = audioUrlBuilders[mode];
+async function audioGetUrl(definition, mode, options, download) {
+ const handler = audioUrlBuilders.get(mode);
+ if (typeof handler === 'function') {
try {
- return await handler(definition, optionsContext, download);
+ return await handler(definition, options, download);
} catch (e) {
// NOP
}
diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js
index db4d30b9..170a6b32 100644
--- a/ext/bg/js/backend-api-forwarder.js
+++ b/ext/bg/js/backend-api-forwarder.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index d9f9b586..28b0201e 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -23,6 +23,7 @@ class Backend {
this.anki = new AnkiNull();
this.mecab = new Mecab();
this.options = null;
+ this.optionsSchema = null;
this.optionsContext = {
depth: 0,
url: window.location.href
@@ -38,11 +39,20 @@ class Backend {
async prepare() {
await this.translator.prepare();
+
+ this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
this.options = await optionsLoad();
+ try {
+ this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, this.options);
+ } catch (e) {
+ // This shouldn't happen, but catch errors just in case of bugs
+ logError(e);
+ }
+
this.onOptionsUpdated('background');
if (chrome.commands !== null && typeof chrome.commands === 'object') {
- chrome.commands.onCommand.addListener(this.onCommand.bind(this));
+ chrome.commands.onCommand.addListener((command) => this._runCommand(command));
}
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
@@ -67,22 +77,21 @@ class Backend {
});
}
- onCommand(command) {
- apiCommandExec(command);
- }
-
onMessage({action, params}, sender, callback) {
- const handlers = Backend.messageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- const promise = handler(params, sender);
+ const handler = Backend._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return false; }
+
+ try {
+ const promise = handler(this, params, sender);
promise.then(
(result) => callback({result}),
(error) => callback({error: errorToJson(error)})
);
+ return true;
+ } catch (error) {
+ callback({error: errorToJson(error)});
+ return false;
}
-
- return true;
}
applyOptions() {
@@ -106,6 +115,13 @@ class Backend {
}
}
+ async getOptionsSchema() {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ return this.optionsSchema;
+ }
+
async getFullOptions() {
if (this.isPreparedPromise !== null) {
await this.isPreparedPromise;
@@ -113,6 +129,18 @@ class Backend {
return this.options;
}
+ async setFullOptions(options) {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ try {
+ this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options));
+ } catch (e) {
+ // This shouldn't happen, but catch errors just in case of bugs
+ logError(e);
+ }
+ }
+
async getOptions(optionsContext) {
if (this.isPreparedPromise !== null) {
await this.isPreparedPromise;
@@ -180,28 +208,542 @@ class Backend {
checkLastError() {
// NOP
}
+
+ _runCommand(command, params) {
+ const handler = Backend._commandHandlers.get(command);
+ if (typeof handler !== 'function') { return false; }
+
+ handler(this, params);
+ return true;
+ }
+
+ // Message handlers
+
+ _onApiOptionsSchemaGet() {
+ return this.getOptionsSchema();
+ }
+
+ _onApiOptionsGet({optionsContext}) {
+ return this.getOptions(optionsContext);
+ }
+
+ _onApiOptionsGetFull() {
+ return this.getFullOptions();
+ }
+
+ async _onApiOptionsSet({changedOptions, optionsContext, source}) {
+ const options = await 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 obj = node.obj[key];
+ if (obj !== null && typeof obj === 'object') {
+ nodes.unshift({obj, path});
+ } else {
+ valuePaths.push([obj, path]);
+ }
+ }
+ }
+ return valuePaths;
+ }
+
+ function modifyOption(path, value, options) {
+ 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, options);
+ }
+
+ await this._onApiOptionsSave({source});
+ }
+
+ async _onApiOptionsSave({source}) {
+ const options = await this.getFullOptions();
+ await optionsSave(options);
+ this.onOptionsUpdated(source);
+ }
+
+ async _onApiKanjiFind({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const definitions = await this.translator.findKanji(text, options);
+ definitions.splice(options.general.maxResults);
+ return definitions;
+ }
+
+ async _onApiTermsFind({text, details, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const [definitions, length] = await this.translator.findTerms(text, details, options);
+ definitions.splice(options.general.maxResults);
+ return {length, definitions};
+ }
+
+ async _onApiTextParse({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const results = [];
+ while (text.length > 0) {
+ const term = [];
+ const [definitions, sourceLength] = await this.translator.findTermsInternal(
+ text.substring(0, options.scanning.length),
+ dictEnabledSet(options),
+ options.scanning.alphanumeric,
+ {}
+ );
+ if (definitions.length > 0) {
+ dictTermsSort(definitions);
+ const {expression, reading} = definitions[0];
+ const source = text.substring(0, sourceLength);
+ for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
+ const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
+ term.push({text, reading});
+ }
+ text = text.substring(source.length);
+ } else {
+ const reading = jpConvertReading(text[0], null, options.parsing.readingMode);
+ term.push({text: text[0], reading});
+ text = text.substring(1);
+ }
+ results.push(term);
+ }
+ return results;
+ }
+
+ async _onApiTextParseMecab({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const results = {};
+ const rawResults = await this.mecab.parseText(text);
+ for (const mecabName in rawResults) {
+ const result = [];
+ for (const parsedLine of rawResults[mecabName]) {
+ for (const {expression, reading, source} of parsedLine) {
+ const term = [];
+ if (expression !== null && reading !== null) {
+ for (const {text, furigana} of jpDistributeFuriganaInflected(
+ expression,
+ jpKatakanaToHiragana(reading),
+ source
+ )) {
+ const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
+ term.push({text, reading});
+ }
+ } else {
+ const reading = jpConvertReading(source, null, options.parsing.readingMode);
+ term.push({text: source, reading});
+ }
+ result.push(term);
+ }
+ result.push([{text: '\n'}]);
+ }
+ results[mecabName] = result;
+ }
+ return results;
+ }
+
+ async _onApiDefinitionAdd({definition, mode, context, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const templates = Backend._getTemplates(options);
+
+ if (mode !== 'kanji') {
+ await audioInject(
+ definition,
+ options.anki.terms.fields,
+ options.audio.sources,
+ optionsContext
+ );
+ }
+
+ if (context && context.screenshot) {
+ await this._injectScreenshot(
+ definition,
+ options.anki.terms.fields,
+ context.screenshot
+ );
+ }
+
+ const note = await dictNoteFormat(definition, mode, options, templates);
+ return this.anki.addNote(note);
+ }
+
+ async _onApiDefinitionsAddable({definitions, modes, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const templates = Backend._getTemplates(options);
+ const states = [];
+
+ try {
+ const notes = [];
+ for (const definition of definitions) {
+ for (const mode of modes) {
+ const note = await dictNoteFormat(definition, mode, options, templates);
+ notes.push(note);
+ }
+ }
+
+ const cannotAdd = [];
+ const results = await this.anki.canAddNotes(notes);
+ for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {
+ const state = {};
+ for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) {
+ const index = resultBase + modeOffset;
+ const result = results[index];
+ const info = {canAdd: result};
+ state[modes[modeOffset]] = info;
+ if (!result) {
+ cannotAdd.push([notes[index], info]);
+ }
+ }
+
+ states.push(state);
+ }
+
+ if (cannotAdd.length > 0) {
+ const noteIdsArray = await this.anki.findNoteIds(cannotAdd.map((e) => e[0]));
+ for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
+ const noteIds = noteIdsArray[i];
+ if (noteIds.length > 0) {
+ cannotAdd[i][1].noteId = noteIds[0];
+ }
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+
+ return states;
+ }
+
+ async _onApiNoteView({noteId}) {
+ return this.anki.guiBrowse(`nid:${noteId}`);
+ }
+
+ async _onApiTemplateRender({template, data, dynamic}) {
+ return (
+ dynamic ?
+ handlebarsRenderDynamic(template, data) :
+ handlebarsRenderStatic(template, data)
+ );
+ }
+
+ async _onApiCommandExec({command, params}) {
+ return this._runCommand(command, params);
+ }
+
+ async _onApiAudioGetUrl({definition, source, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ return await audioGetUrl(definition, source, options);
+ }
+
+ _onApiScreenshotGet({options}, sender) {
+ if (!(sender && sender.tab)) {
+ return Promise.resolve();
+ }
+
+ const windowId = sender.tab.windowId;
+ return new Promise((resolve) => {
+ chrome.tabs.captureVisibleTab(windowId, options, (dataUrl) => resolve(dataUrl));
+ });
+ }
+
+ _onApiForward({action, params}, sender) {
+ if (!(sender && sender.tab)) {
+ return Promise.resolve();
+ }
+
+ const tabId = sender.tab.id;
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response));
+ });
+ }
+
+ _onApiFrameInformationGet(params, sender) {
+ const frameId = sender.frameId;
+ return Promise.resolve({frameId});
+ }
+
+ _onApiInjectStylesheet({css}, sender) {
+ if (!sender.tab) {
+ return Promise.reject(new Error('Invalid tab'));
+ }
+
+ const tabId = sender.tab.id;
+ const frameId = sender.frameId;
+ const details = {
+ code: css,
+ runAt: 'document_start',
+ cssOrigin: 'user',
+ allFrames: false
+ };
+ if (typeof frameId === 'number') {
+ details.frameId = frameId;
+ }
+
+ return new Promise((resolve, reject) => {
+ chrome.tabs.insertCSS(tabId, details, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+
+ async _onApiGetEnvironmentInfo() {
+ const browser = await Backend._getBrowser();
+ const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
+ return {
+ browser,
+ platform: {
+ os: platform.os
+ }
+ };
+ }
+
+ async _onApiClipboardGet() {
+ const clipboardPasteTarget = this.clipboardPasteTarget;
+ clipboardPasteTarget.value = '';
+ clipboardPasteTarget.focus();
+ document.execCommand('paste');
+ const result = clipboardPasteTarget.value;
+ clipboardPasteTarget.value = '';
+ return result;
+ }
+
+ // Command handlers
+
+ async _onCommandSearch(params) {
+ const url = chrome.runtime.getURL('/bg/search.html');
+ if (!(params && params.newTab)) {
+ try {
+ const tab = await Backend._findTab(1000, (url2) => (
+ url2 !== null &&
+ url2.startsWith(url) &&
+ (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
+ ));
+ if (tab !== null) {
+ await Backend._focusTab(tab);
+ return;
+ }
+ } catch (e) {
+ // NOP
+ }
+ }
+ chrome.tabs.create({url});
+ }
+
+ _onCommandHelp() {
+ chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
+ }
+
+ _onCommandOptions(params) {
+ if (!(params && params.newTab)) {
+ chrome.runtime.openOptionsPage();
+ } else {
+ const manifest = chrome.runtime.getManifest();
+ const url = chrome.runtime.getURL(manifest.options_ui.page);
+ chrome.tabs.create({url});
+ }
+ }
+
+ async _onCommandToggle() {
+ const optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+ const source = 'popup';
+
+ const options = await this.getOptions(optionsContext);
+ options.general.enable = !options.general.enable;
+ await this._onApiOptionsSave({source});
+ }
+
+ // Utilities
+
+ async _injectScreenshot(definition, fields, screenshot) {
+ let usesScreenshot = false;
+ for (const name in fields) {
+ if (fields[name].includes('{screenshot}')) {
+ usesScreenshot = true;
+ break;
+ }
+ }
+
+ if (!usesScreenshot) {
+ return;
+ }
+
+ const dateToString = (date) => {
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth().toString().padStart(2, '0');
+ const day = date.getUTCDate().toString().padStart(2, '0');
+ const hours = date.getUTCHours().toString().padStart(2, '0');
+ const minutes = date.getUTCMinutes().toString().padStart(2, '0');
+ const seconds = date.getUTCSeconds().toString().padStart(2, '0');
+ return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
+ };
+
+ const now = new Date(Date.now());
+ const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`;
+ const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, '');
+
+ try {
+ await this.anki.storeMediaFile(filename, data);
+ } catch (e) {
+ return;
+ }
+
+ definition.screenshotFileName = filename;
+ }
+
+ static _getTabUrl(tab) {
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
+ let url = null;
+ if (!chrome.runtime.lastError) {
+ url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null);
+ if (url !== null && typeof url !== 'string') {
+ url = null;
+ }
+ }
+ resolve({tab, url});
+ });
+ });
+ }
+
+ static async _findTab(timeout, checkUrl) {
+ // This function works around the need to have the "tabs" permission to access tab.url.
+ const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));
+ let matchPromiseResolve = null;
+ const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; });
+
+ const checkTabUrl = ({tab, url}) => {
+ if (checkUrl(url, tab)) {
+ matchPromiseResolve(tab);
+ }
+ };
+
+ const promises = [];
+ for (const tab of tabs) {
+ const promise = Backend._getTabUrl(tab);
+ promise.then(checkTabUrl);
+ promises.push(promise);
+ }
+
+ const racePromises = [
+ matchPromise,
+ Promise.all(promises).then(() => null)
+ ];
+ if (typeof timeout === 'number') {
+ racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout)));
+ }
+
+ return await Promise.race(racePromises);
+ }
+
+ static async _focusTab(tab) {
+ await new Promise((resolve, reject) => {
+ chrome.tabs.update(tab.id, {active: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+
+ if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) {
+ // Windows not supported (e.g. on Firefox mobile)
+ return;
+ }
+
+ try {
+ const tabWindow = await new Promise((resolve) => {
+ chrome.windows.get(tab.windowId, {}, (tabWindow) => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(tabWindow); }
+ });
+ });
+ if (!tabWindow.focused) {
+ await new Promise((resolve, reject) => {
+ chrome.windows.update(tab.windowId, {focused: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+ }
+ } catch (e) {
+ // 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';
+ }
+ }
+
+ static _getTemplates(options) {
+ const templates = options.anki.fieldTemplates;
+ return typeof templates === 'string' ? templates : profileOptionsGetDefaultFieldTemplates();
+ }
}
-Backend.messageHandlers = {
- optionsGet: ({optionsContext}) => apiOptionsGet(optionsContext),
- optionsSet: ({changedOptions, optionsContext, source}) => apiOptionsSet(changedOptions, optionsContext, source),
- kanjiFind: ({text, optionsContext}) => apiKanjiFind(text, optionsContext),
- termsFind: ({text, details, optionsContext}) => apiTermsFind(text, details, optionsContext),
- textParse: ({text, optionsContext}) => apiTextParse(text, optionsContext),
- textParseMecab: ({text, optionsContext}) => apiTextParseMecab(text, optionsContext),
- definitionAdd: ({definition, mode, context, optionsContext}) => apiDefinitionAdd(definition, mode, context, optionsContext),
- definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext),
- noteView: ({noteId}) => apiNoteView(noteId),
- templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic),
- commandExec: ({command, params}) => apiCommandExec(command, params),
- audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),
- screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),
- forward: ({action, params}, sender) => apiForward(action, params, sender),
- frameInformationGet: (params, sender) => apiFrameInformationGet(sender),
- injectStylesheet: ({css}, sender) => apiInjectStylesheet(css, sender),
- getEnvironmentInfo: () => apiGetEnvironmentInfo(),
- clipboardGet: () => apiClipboardGet()
-};
-
-window.yomichan_backend = new Backend();
-window.yomichan_backend.prepare();
+Backend._messageHandlers = new Map([
+ ['optionsSchemaGet', (self, ...args) => self._onApiOptionsSchemaGet(...args)],
+ ['optionsGet', (self, ...args) => self._onApiOptionsGet(...args)],
+ ['optionsGetFull', (self, ...args) => self._onApiOptionsGetFull(...args)],
+ ['optionsSet', (self, ...args) => self._onApiOptionsSet(...args)],
+ ['optionsSave', (self, ...args) => self._onApiOptionsSave(...args)],
+ ['kanjiFind', (self, ...args) => self._onApiKanjiFind(...args)],
+ ['termsFind', (self, ...args) => self._onApiTermsFind(...args)],
+ ['textParse', (self, ...args) => self._onApiTextParse(...args)],
+ ['textParseMecab', (self, ...args) => self._onApiTextParseMecab(...args)],
+ ['definitionAdd', (self, ...args) => self._onApiDefinitionAdd(...args)],
+ ['definitionsAddable', (self, ...args) => self._onApiDefinitionsAddable(...args)],
+ ['noteView', (self, ...args) => self._onApiNoteView(...args)],
+ ['templateRender', (self, ...args) => self._onApiTemplateRender(...args)],
+ ['commandExec', (self, ...args) => self._onApiCommandExec(...args)],
+ ['audioGetUrl', (self, ...args) => self._onApiAudioGetUrl(...args)],
+ ['screenshotGet', (self, ...args) => self._onApiScreenshotGet(...args)],
+ ['forward', (self, ...args) => self._onApiForward(...args)],
+ ['frameInformationGet', (self, ...args) => self._onApiFrameInformationGet(...args)],
+ ['injectStylesheet', (self, ...args) => self._onApiInjectStylesheet(...args)],
+ ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],
+ ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)]
+]);
+
+Backend._commandHandlers = new Map([
+ ['search', (self, ...args) => self._onCommandSearch(...args)],
+ ['help', (self, ...args) => self._onCommandHelp(...args)],
+ ['options', (self, ...args) => self._onCommandOptions(...args)],
+ ['toggle', (self, ...args) => self._onCommandToggle(...args)]
+]);
+
+window.yomichanBackend = new Backend();
+window.yomichanBackend.prepare();
diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js
index c0f0f301..d4d1c0e0 100644
--- a/ext/bg/js/conditions.js
+++ b/ext/bg/js/conditions.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 0b21f662..834174bf 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index a20d5f15..42a143f3 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -28,7 +28,7 @@ class Database {
}
try {
- this.db = await Database.open('dict', 4, (db, transaction, oldVersion) => {
+ this.db = await Database.open('dict', 5, (db, transaction, oldVersion) => {
Database.upgrade(db, transaction, oldVersion, [
{
version: 2,
@@ -76,6 +76,15 @@ class Database {
indices: ['dictionary', 'expression', 'reading', 'sequence']
}
}
+ },
+ {
+ version: 5,
+ stores: {
+ terms: {
+ primaryKey: {keyPath: 'id', autoIncrement: true},
+ indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
+ }
+ }
}
]);
});
@@ -143,14 +152,17 @@ class Database {
}
};
+ const useWildcard = !!wildcard;
+ const prefixWildcard = wildcard === 'prefix';
+
const dbTransaction = this.db.transaction(['terms'], 'readonly');
const dbTerms = dbTransaction.objectStore('terms');
- const dbIndex1 = dbTerms.index('expression');
- const dbIndex2 = dbTerms.index('reading');
+ const dbIndex1 = dbTerms.index(prefixWildcard ? 'expressionReverse' : 'expression');
+ const dbIndex2 = dbTerms.index(prefixWildcard ? 'readingReverse' : 'reading');
for (let i = 0; i < termList.length; ++i) {
- const term = termList[i];
- const query = wildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
+ const term = prefixWildcard ? stringReverse(termList[i]) : termList[i];
+ const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
promises.push(
Database.getAll(dbIndex1, query, i, processRow),
Database.getAll(dbIndex2, query, i, processRow)
@@ -320,9 +332,12 @@ class Database {
return result;
}
- async importDictionary(archive, progressCallback, exceptions) {
+ async importDictionary(archive, progressCallback, details) {
this.validate();
+ const errors = [];
+ const prefixWildcardsSupported = details.prefixWildcardsSupported;
+
const maxTransactionLength = 1000;
const bulkAdd = async (objectStoreName, items, total, current) => {
const db = this.db;
@@ -337,11 +352,7 @@ class Database {
const objectStore = transaction.objectStore(objectStoreName);
await Database.bulkAdd(objectStore, items, i, count);
} catch (e) {
- if (exceptions) {
- exceptions.push(e);
- } else {
- throw e;
- }
+ errors.push(e);
}
}
};
@@ -396,6 +407,13 @@ class Database {
}
}
+ if (prefixWildcardsSupported) {
+ for (const row of rows) {
+ row.expressionReverse = stringReverse(row.expression);
+ row.readingReverse = stringReverse(row.reading);
+ }
+ }
+
await bulkAdd('terms', rows, total, current);
};
@@ -475,15 +493,18 @@ class Database {
await bulkAdd('tagMeta', rows, total, current);
};
- return await Database.importDictionaryZip(
+ const result = await Database.importDictionaryZip(
archive,
indexDataLoaded,
termDataLoaded,
termMetaDataLoaded,
kanjiDataLoaded,
kanjiMetaDataLoaded,
- tagDataLoaded
+ tagDataLoaded,
+ details
);
+
+ return {result, errors};
}
validate() {
@@ -499,7 +520,8 @@ class Database {
termMetaDataLoaded,
kanjiDataLoaded,
kanjiMetaDataLoaded,
- tagDataLoaded
+ tagDataLoaded,
+ details
) {
const zip = await JSZip.loadAsync(archive);
@@ -517,7 +539,8 @@ class Database {
title: index.title,
revision: index.revision,
sequenced: index.sequenced,
- version: index.format || index.version
+ version: index.format || index.version,
+ prefixWildcardsSupported: !!details.prefixWildcardsSupported
};
await indexDataLoaded(summary);
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index 51f4723c..33b2a8b3 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 0b35e32e..92adc532 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -310,7 +310,7 @@ function dictFieldSplit(field) {
return field.length === 0 ? [] : field.split(' ');
}
-async function dictFieldFormat(field, definition, mode, options, exceptions) {
+async function dictFieldFormat(field, definition, mode, options, templates, exceptions) {
const data = {
marker: null,
definition,
@@ -329,7 +329,7 @@ async function dictFieldFormat(field, definition, mode, options, exceptions) {
}
data.marker = marker;
try {
- return await apiTemplateRender(options.anki.fieldTemplates, data, true);
+ return await apiTemplateRender(templates, data, true);
} catch (e) {
if (exceptions) { exceptions.push(e); }
return `{${marker}-render-error}`;
@@ -357,7 +357,7 @@ dictFieldFormat.markers = new Set([
'url'
]);
-async function dictNoteFormat(definition, mode, options) {
+async function dictNoteFormat(definition, mode, options, templates) {
const note = {fields: {}, tags: options.anki.tags};
let fields = [];
@@ -391,7 +391,7 @@ async function dictNoteFormat(definition, mode, options) {
}
for (const name in fields) {
- note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options);
+ note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options, templates);
}
return note;
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index 8f43cf9a..6d1581be 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -141,12 +141,13 @@ function handlebarsRenderStatic(name, data) {
function handlebarsRenderDynamic(template, data) {
handlebarsRegisterHelpers();
-
- Handlebars.yomichan_cache = Handlebars.yomichan_cache || {};
- let instance = Handlebars.yomichan_cache[template];
- if (!instance) {
- instance = Handlebars.yomichan_cache[template] = Handlebars.compile(template);
+ const cache = handlebarsRenderDynamic._cache;
+ let instance = cache.get(template);
+ if (typeof instance === 'undefined') {
+ instance = Handlebars.compile(template);
+ cache.set(template, instance);
}
return instance(data).trim();
}
+handlebarsRenderDynamic._cache = new Map();
diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js
new file mode 100644
index 00000000..5d596a8b
--- /dev/null
+++ b/ext/bg/js/json-schema.js
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2019-2020 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 <https://www.gnu.org/licenses/>.
+ */
+
+
+class JsonSchemaProxyHandler {
+ constructor(schema) {
+ this._schema = schema;
+ }
+
+ getPrototypeOf(target) {
+ return Object.getPrototypeOf(target);
+ }
+
+ setPrototypeOf() {
+ throw new Error('setPrototypeOf not supported');
+ }
+
+ isExtensible(target) {
+ return Object.isExtensible(target);
+ }
+
+ preventExtensions(target) {
+ Object.preventExtensions(target);
+ return true;
+ }
+
+ getOwnPropertyDescriptor(target, property) {
+ return Object.getOwnPropertyDescriptor(target, property);
+ }
+
+ defineProperty() {
+ throw new Error('defineProperty not supported');
+ }
+
+ has(target, property) {
+ return property in target;
+ }
+
+ get(target, property) {
+ if (typeof property === 'symbol') {
+ return target[property];
+ }
+
+ if (Array.isArray(target)) {
+ if (typeof property === 'string' && /^\d+$/.test(property)) {
+ property = parseInt(property, 10);
+ } else if (typeof property === 'string') {
+ return target[property];
+ }
+ }
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ if (propertySchema === null) {
+ return;
+ }
+
+ const value = target[property];
+ return value !== null && typeof value === 'object' ? JsonSchema.createProxy(value, propertySchema) : value;
+ }
+
+ set(target, property, value) {
+ if (Array.isArray(target)) {
+ if (typeof property === 'string' && /^\d+$/.test(property)) {
+ property = parseInt(property, 10);
+ if (property > target.length) {
+ throw new Error('Array index out of range');
+ }
+ } else if (typeof property === 'string') {
+ target[property] = value;
+ return true;
+ }
+ }
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ if (propertySchema === null) {
+ throw new Error(`Property ${property} not supported`);
+ }
+
+ value = JsonSchema.isolate(value);
+
+ const error = JsonSchemaProxyHandler.validate(value, propertySchema);
+ if (error !== null) {
+ throw new Error(`Invalid value: ${error}`);
+ }
+
+ target[property] = value;
+ return true;
+ }
+
+ deleteProperty(target, property) {
+ const required = this._schema.required;
+ if (Array.isArray(required) && required.includes(property)) {
+ throw new Error(`${property} cannot be deleted`);
+ }
+ return Reflect.deleteProperty(target, property);
+ }
+
+ ownKeys(target) {
+ return Reflect.ownKeys(target);
+ }
+
+ apply() {
+ throw new Error('apply not supported');
+ }
+
+ construct() {
+ throw new Error('construct not supported');
+ }
+
+ static getPropertySchema(schema, property) {
+ const type = schema.type;
+ if (Array.isArray(type)) {
+ throw new Error(`Ambiguous property type for ${property}`);
+ }
+ switch (type) {
+ case 'object':
+ {
+ const properties = schema.properties;
+ if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
+ if (Object.prototype.hasOwnProperty.call(properties, property)) {
+ return properties[property];
+ }
+ }
+
+ const additionalProperties = schema.additionalProperties;
+ return (additionalProperties !== null && typeof additionalProperties === 'object' && !Array.isArray(additionalProperties)) ? additionalProperties : null;
+ }
+ case 'array':
+ {
+ const items = schema.items;
+ return (items !== null && typeof items === 'object' && !Array.isArray(items)) ? items : null;
+ }
+ default:
+ return null;
+ }
+ }
+
+ static validate(value, schema) {
+ const type = JsonSchemaProxyHandler.getValueType(value);
+ const schemaType = schema.type;
+ if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
+ return `Value type ${type} does not match schema type ${schemaType}`;
+ }
+
+ const schemaEnum = schema.enum;
+ if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) {
+ return 'Invalid enum value';
+ }
+
+ switch (type) {
+ case 'number':
+ return JsonSchemaProxyHandler.validateNumber(value, schema);
+ case 'string':
+ return JsonSchemaProxyHandler.validateString(value, schema);
+ case 'array':
+ return JsonSchemaProxyHandler.validateArray(value, schema);
+ case 'object':
+ return JsonSchemaProxyHandler.validateObject(value, schema);
+ default:
+ return null;
+ }
+ }
+
+ static validateNumber(value, schema) {
+ const multipleOf = schema.multipleOf;
+ if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
+ return `Number is not a multiple of ${multipleOf}`;
+ }
+
+ const minimum = schema.minimum;
+ if (typeof minimum === 'number' && value < minimum) {
+ return `Number is less than ${minimum}`;
+ }
+
+ const exclusiveMinimum = schema.exclusiveMinimum;
+ if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
+ return `Number is less than or equal to ${exclusiveMinimum}`;
+ }
+
+ const maximum = schema.maximum;
+ if (typeof maximum === 'number' && value > maximum) {
+ return `Number is greater than ${maximum}`;
+ }
+
+ const exclusiveMaximum = schema.exclusiveMaximum;
+ if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
+ return `Number is greater than or equal to ${exclusiveMaximum}`;
+ }
+
+ return null;
+ }
+
+ static validateString(value, schema) {
+ const minLength = schema.minLength;
+ if (typeof minLength === 'number' && value.length < minLength) {
+ return 'String length too short';
+ }
+
+ const maxLength = schema.minLength;
+ if (typeof maxLength === 'number' && value.length > maxLength) {
+ return 'String length too long';
+ }
+
+ return null;
+ }
+
+ static validateArray(value, schema) {
+ const minItems = schema.minItems;
+ if (typeof minItems === 'number' && value.length < minItems) {
+ return 'Array length too short';
+ }
+
+ const maxItems = schema.maxItems;
+ if (typeof maxItems === 'number' && value.length > maxItems) {
+ return 'Array length too long';
+ }
+
+ return null;
+ }
+
+ static validateObject(value, schema) {
+ const properties = new Set(Object.getOwnPropertyNames(value));
+
+ const required = schema.required;
+ if (Array.isArray(required)) {
+ for (const property of required) {
+ if (!properties.has(property)) {
+ return `Missing property ${property}`;
+ }
+ }
+ }
+
+ const minProperties = schema.minProperties;
+ if (typeof minProperties === 'number' && properties.length < minProperties) {
+ return 'Not enough object properties';
+ }
+
+ const maxProperties = schema.maxProperties;
+ if (typeof maxProperties === 'number' && properties.length > maxProperties) {
+ return 'Too many object properties';
+ }
+
+ for (const property of properties) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) {
+ return `No schema found for ${property}`;
+ }
+ const error = JsonSchemaProxyHandler.validate(value[property], propertySchema);
+ if (error !== null) {
+ return error;
+ }
+ }
+
+ return null;
+ }
+
+ static isValueTypeAny(value, type, schemaTypes) {
+ if (typeof schemaTypes === 'string') {
+ return JsonSchemaProxyHandler.isValueType(value, type, schemaTypes);
+ } else if (Array.isArray(schemaTypes)) {
+ for (const schemaType of schemaTypes) {
+ if (JsonSchemaProxyHandler.isValueType(value, type, schemaType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ static isValueType(value, type, schemaType) {
+ return (
+ type === schemaType ||
+ (schemaType === 'integer' && Math.floor(value) === value)
+ );
+ }
+
+ static getValueType(value) {
+ const type = typeof value;
+ if (type === 'object') {
+ if (value === null) { return 'null'; }
+ if (Array.isArray(value)) { return 'array'; }
+ }
+ return type;
+ }
+
+ static valuesAreEqualAny(value1, valueList) {
+ for (const value2 of valueList) {
+ if (JsonSchemaProxyHandler.valuesAreEqual(value1, value2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static valuesAreEqual(value1, value2) {
+ return value1 === value2;
+ }
+
+ static getDefaultTypeValue(type) {
+ if (typeof type === 'string') {
+ switch (type) {
+ case 'null':
+ return null;
+ case 'boolean':
+ return false;
+ case 'number':
+ case 'integer':
+ return 0;
+ case 'string':
+ return '';
+ case 'array':
+ return [];
+ case 'object':
+ return {};
+ }
+ }
+ return null;
+ }
+
+ static getValidValueOrDefault(schema, value) {
+ let type = JsonSchemaProxyHandler.getValueType(value);
+ const schemaType = schema.type;
+ if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
+ let assignDefault = true;
+
+ const schemaDefault = schema.default;
+ if (typeof schemaDefault !== 'undefined') {
+ value = JsonSchema.isolate(schemaDefault);
+ type = JsonSchemaProxyHandler.getValueType(value);
+ assignDefault = !JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType);
+ }
+
+ if (assignDefault) {
+ value = JsonSchemaProxyHandler.getDefaultTypeValue(schemaType);
+ type = JsonSchemaProxyHandler.getValueType(value);
+ }
+ }
+
+ switch (type) {
+ case 'object':
+ value = JsonSchemaProxyHandler.populateObjectDefaults(value, schema);
+ break;
+ case 'array':
+ value = JsonSchemaProxyHandler.populateArrayDefaults(value, schema);
+ break;
+ }
+
+ return value;
+ }
+
+ static populateObjectDefaults(value, schema) {
+ const properties = new Set(Object.getOwnPropertyNames(value));
+
+ const required = schema.required;
+ if (Array.isArray(required)) {
+ for (const property of required) {
+ properties.delete(property);
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) { continue; }
+ value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]);
+ }
+ }
+
+ for (const property of properties) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) {
+ Reflect.deleteProperty(value, property);
+ } else {
+ value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]);
+ }
+ }
+
+ return value;
+ }
+
+ static populateArrayDefaults(value, schema) {
+ for (let i = 0, ii = value.length; i < ii; ++i) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i);
+ if (propertySchema === null) { continue; }
+ value[i] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[i]);
+ }
+
+ return value;
+ }
+}
+
+class JsonSchema {
+ static createProxy(target, schema) {
+ return new Proxy(target, new JsonSchemaProxyHandler(schema));
+ }
+
+ static getValidValueOrDefault(schema, value) {
+ return JsonSchemaProxyHandler.getValidValueOrDefault(schema, value);
+ }
+
+ static isolate(value) {
+ if (value === null) { return null; }
+
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ return value;
+ }
+
+ const stringValue = JSON.stringify(value);
+ return typeof stringValue === 'string' ? JSON.parse(stringValue) : null;
+ }
+}
diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js
index 62111f73..8bcbb91c 100644
--- a/ext/bg/js/mecab.js
+++ b/ext/bg/js/mecab.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index e53a8a13..8021672b 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -86,6 +86,13 @@ const profileOptionsVersionUpdates = [
delete options.general.audioSource;
delete options.general.audioVolume;
delete options.general.autoPlayAudio;
+ },
+ (options) => {
+ // Version 12 changes:
+ // The preferred default value of options.anki.fieldTemplates has been changed to null.
+ if (utilStringHashCode(options.anki.fieldTemplates) === 1444379824) {
+ options.anki.fieldTemplates = null;
+ }
}
];
@@ -326,7 +333,7 @@ function profileOptionsCreateDefaults() {
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}},
- fieldTemplates: profileOptionsGetDefaultFieldTemplates()
+ fieldTemplates: null
}
};
}
@@ -378,7 +385,15 @@ function profileOptionsUpdateVersion(options) {
* ]
*/
-const optionsVersionUpdates = [];
+const optionsVersionUpdates = [
+ (options) => {
+ options.global = {
+ database: {
+ prefixWildcardsSupported: false
+ }
+ };
+ }
+];
function optionsUpdateVersion(options, defaultProfileOptions) {
// Ensure profiles is an array
@@ -423,6 +438,11 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
profile.options = profileOptionsUpdateVersion(profile.options);
}
+ // Version
+ if (typeof options.version !== 'number') {
+ options.version = 0;
+ }
+
// Generic updates
return optionsGenericApplyUpdates(options, optionsVersionUpdates);
}
@@ -468,3 +488,7 @@ function optionsSave(options) {
});
});
}
+
+function optionsGetDefault() {
+ return optionsUpdateVersion({}, {});
+}
diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js
index aee4e3c2..3a320db3 100644
--- a/ext/bg/js/page-exit-prevention.js
+++ b/ext/bg/js/page-exit-prevention.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index ebc6680a..1fd78e5d 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js
index 7d73d49b..b584c9a9 100644
--- a/ext/bg/js/request.js
+++ b/ext/bg/js/request.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index 6ba8467e..2fe50a13 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -33,6 +33,7 @@ async function searchFrontendSetup() {
window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false};
const scriptSrcs = [
+ '/mixed/js/text-scanner.js',
'/fg/js/frontend-api-receiver.js',
'/fg/js/popup.js',
'/fg/js/popup-proxy-host.js',
@@ -40,6 +41,9 @@ async function searchFrontendSetup() {
'/fg/js/frontend-initialize.js'
];
for (const src of scriptSrcs) {
+ const node = document.querySelector(`script[src='${src}']`);
+ if (node !== null) { continue; }
+
const script = document.createElement('script');
script.async = false;
script.src = src;
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 8dc2e30a..0b3eccbd 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -62,7 +62,7 @@ class QueryParser {
const scanningOptions = this.search.options.scanning;
const scanningModifier = scanningOptions.modifier;
if (!(
- Frontend.isScanningModifierPressed(scanningModifier, e) ||
+ TextScanner.isScanningModifierPressed(scanningModifier, e) ||
(scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))
)) {
return;
@@ -148,10 +148,9 @@ class QueryParser {
async setPreview(text) {
const previewTerms = [];
- while (text.length > 0) {
- const tempText = text.slice(0, 2);
- previewTerms.push([{text: Array.from(tempText)}]);
- text = text.slice(2);
+ for (let i = 0, ii = text.length; i < ii; i += 2) {
+ const tempText = text.substring(i, i + 2);
+ previewTerms.push([{text: tempText.split('')}]);
}
this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', {
terms: previewTerms,
@@ -218,7 +217,7 @@ class QueryParser {
return result.map((term) => {
return term.filter((part) => part.text.trim()).map((part) => {
return {
- text: Array.from(part.text),
+ text: part.text.split(''),
reading: part.reading,
raw: !part.reading || !part.reading.trim()
};
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index fe48773f..a4103ef2 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,16 +13,9 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-
-let IS_FIREFOX = null;
-(async () => {
- const {browser} = await apiGetEnvironmentInfo();
- IS_FIREFOX = ['firefox', 'firefox-mobile'].includes(browser);
-})();
-
class DisplaySearch extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#content'));
@@ -43,8 +36,12 @@ class DisplaySearch extends Display {
this.introVisible = true;
this.introAnimationTimer = null;
- this.clipboardMonitorIntervalId = null;
- this.clipboardPrevText = null;
+ this.isFirefox = false;
+
+ this.clipboardMonitorTimerId = null;
+ this.clipboardMonitorTimerToken = null;
+ this.clipboardInterval = 250;
+ this.clipboardPreviousText = null;
}
static create() {
@@ -56,6 +53,7 @@ class DisplaySearch extends Display {
async prepare() {
try {
await this.initialize();
+ this.isFirefox = await DisplaySearch._isFirefox();
if (this.search !== null) {
this.search.addEventListener('click', (e) => this.onSearch(e), false);
@@ -207,10 +205,14 @@ class DisplaySearch extends Display {
async onSearchQueryUpdated(query, animate) {
try {
const details = {};
- const match = /[*\uff0a]+$/.exec(query);
+ const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(query);
if (match !== null) {
- details.wildcard = true;
- query = query.substring(0, query.length - match[0].length);
+ if (match[1]) {
+ details.wildcard = 'prefix';
+ } else if (match[3]) {
+ details.wildcard = 'suffix';
+ }
+ query = match[2];
}
const valid = (query.length > 0);
@@ -224,63 +226,81 @@ class DisplaySearch extends Display {
sentence: {text: query, offset: 0},
url: window.location.href
});
- this.setTitleText(query);
} else {
this.container.textContent = '';
}
+ this.setTitleText(query);
window.parent.postMessage('popupClose', '*');
} catch (e) {
this.onError(e);
}
}
- onRuntimeMessage({action, params}, sender, callback) {
- const handlers = DisplaySearch.runtimeMessageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- const result = handler(this, params);
- callback(result);
- } else {
- return super.onRuntimeMessage({action, params}, sender, callback);
- }
- }
-
initClipboardMonitor() {
// ignore copy from search page
window.addEventListener('copy', () => {
- this.clipboardPrevText = document.getSelection().toString().trim();
+ this.clipboardPreviousText = document.getSelection().toString().trim();
});
}
startClipboardMonitor() {
- this.clipboardMonitorIntervalId = setInterval(async () => {
- let curText = null;
- // TODO get rid of this and figure out why apiClipboardGet doesn't work on Firefox
- if (IS_FIREFOX) {
- curText = (await navigator.clipboard.readText()).trim();
- } else if (IS_FIREFOX === false) {
- curText = (await apiClipboardGet()).trim();
- }
- if (curText && (curText !== this.clipboardPrevText) && jpIsJapaneseText(curText)) {
- if (this.isWanakanaEnabled()) {
- this.setQuery(window.wanakana.toKana(curText));
- } else {
- this.setQuery(curText);
+ // The token below is used as a unique identifier to ensure that a new clipboard monitor
+ // hasn't been started during the await call. The check below the await this.getClipboardText()
+ // call will exit early if the reference has changed.
+ const token = {};
+ const intervalCallback = async () => {
+ this.clipboardMonitorTimerId = null;
+
+ let text = await this.getClipboardText();
+ if (this.clipboardMonitorTimerToken !== token) { return; }
+
+ if (
+ typeof text === 'string' &&
+ (text = text.trim()).length > 0 &&
+ text !== this.clipboardPreviousText
+ ) {
+ this.clipboardPreviousText = text;
+ if (jpIsJapaneseText(text)) {
+ this.setQuery(this.isWanakanaEnabled() ? window.wanakana.toKana(text) : text);
+ window.history.pushState(null, '', `${window.location.pathname}?query=${encodeURIComponent(text)}`);
+ this.onSearchQueryUpdated(this.query.value, true);
}
+ }
- const queryString = curText.length > 0 ? `?query=${encodeURIComponent(curText)}` : '';
- window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
- this.onSearchQueryUpdated(this.query.value, true);
+ this.clipboardMonitorTimerId = setTimeout(intervalCallback, this.clipboardInterval);
+ };
- this.clipboardPrevText = curText;
- }
- }, 100);
+ this.clipboardMonitorTimerToken = token;
+
+ intervalCallback();
}
stopClipboardMonitor() {
- if (this.clipboardMonitorIntervalId) {
- clearInterval(this.clipboardMonitorIntervalId);
- this.clipboardMonitorIntervalId = null;
+ this.clipboardMonitorTimerToken = null;
+ if (this.clipboardMonitorTimerId !== null) {
+ clearTimeout(this.clipboardMonitorTimerId);
+ this.clipboardMonitorTimerId = null;
+ }
+ }
+
+ async getClipboardText() {
+ /*
+ Notes:
+ apiClipboardGet doesn't work on Firefox because document.execCommand('paste')
+ results in an empty string on the web extension background page.
+ This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
+ Therefore, navigator.clipboard.readText() is used on Firefox.
+
+ navigator.clipboard.readText() can't be used in Chrome for two reasons:
+ * Requires page to be focused, else it rejects with an exception.
+ * When the page is focused, Chrome will request clipboard permission, despite already
+ being an extension with clipboard permissions. It effectively asks for the
+ non-extension permission for clipboard access.
+ */
+ try {
+ return this.isFirefox ? await navigator.clipboard.readText() : await apiClipboardGet();
+ } catch (e) {
+ return null;
}
}
@@ -360,22 +380,32 @@ class DisplaySearch extends Display {
setTitleText(text) {
// Chrome limits title to 1024 characters
if (text.length > 1000) {
- text = text.slice(0, 1000) + '...';
+ text = text.substring(0, 1000) + '...';
+ }
+
+ if (text.length === 0) {
+ document.title = 'Yomichan Search';
+ } else {
+ document.title = `${text} - Yomichan Search`;
}
- document.title = `${text} - Yomichan Search`;
}
static getSearchQueryFromLocation(url) {
const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
return match !== null ? decodeURIComponent(match[1]) : null;
}
-}
-DisplaySearch.runtimeMessageHandlers = {
- getUrl: () => {
- return {url: window.location.href};
+ static async _isFirefox() {
+ const {browser} = await apiGetEnvironmentInfo();
+ switch (browser) {
+ case 'firefox':
+ case 'firefox-mobile':
+ return true;
+ default:
+ return false;
+ }
}
-};
+}
DisplaySearch.onKeyDownIgnoreKeys = {
'ANY_MOD': [
@@ -392,4 +422,4 @@ DisplaySearch.onKeyDownIgnoreKeys = {
'Shift': []
};
-window.yomichan_search = DisplaySearch.create();
+DisplaySearch.instance = DisplaySearch.create();
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
index 9cdfc134..5e74358f 100644
--- a/ext/bg/js/settings/anki-templates.js
+++ b/ext/bg/js/settings/anki-templates.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -42,10 +42,22 @@ function ankiTemplatesInitialize() {
node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
}
- $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(e));
+ $('#field-templates').on('change', (e) => onAnkiFieldTemplatesChanged(e));
$('#field-template-render').on('click', (e) => onAnkiTemplateRender(e));
$('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e));
$('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e));
+
+ ankiTemplatesUpdateValue();
+}
+
+async function ankiTemplatesUpdateValue() {
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
+ $('#field-templates').val(templates);
+
+ onAnkiTemplatesValidateCompile();
}
const ankiTemplatesValidateGetDefinition = (() => {
@@ -73,7 +85,9 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
if (definition !== null) {
const options = await apiOptionsGet(optionsContext);
- result = await dictFieldFormat(field, definition, mode, options, exceptions);
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
+ result = await dictFieldFormat(field, definition, mode, options, templates, exceptions);
}
} catch (e) {
exceptions.push(e);
@@ -89,6 +103,24 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
}
}
+async function onAnkiFieldTemplatesChanged(e) {
+ // Get value
+ let templates = e.currentTarget.value;
+ if (templates === profileOptionsGetDefaultFieldTemplates()) {
+ // Default
+ templates = null;
+ }
+
+ // Overwrite
+ const optionsContext = getOptionsContext();
+ const options = await getOptionsMutable(optionsContext);
+ options.anki.fieldTemplates = templates;
+ await settingsSaveOptions();
+
+ // Compile
+ onAnkiTemplatesValidateCompile();
+}
+
function onAnkiTemplatesValidateCompile() {
const infoNode = document.querySelector('#field-template-compile-result');
ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
index e1aabbaf..5f7989b8 100644
--- a/ext/bg/js/settings/anki.js
+++ b/ext/bg/js/settings/anki.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -154,7 +154,7 @@ async function _onAnkiModelChanged(e) {
}
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
options.anki[tabId].fields = utilBackgroundIsolate(fields);
await settingsSaveOptions();
diff --git a/ext/bg/js/audio-ui.js b/ext/bg/js/settings/audio-ui.js
index 381129ac..711c2291 100644
--- a/ext/bg/js/audio-ui.js
+++ b/ext/bg/js/settings/audio-ui.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,7 +21,7 @@ class AudioSourceUI {
static instantiateTemplate(templateSelector) {
const template = document.querySelector(templateSelector);
const content = document.importNode(template.content, true);
- return $(content.firstChild);
+ return content.firstChild;
}
}
@@ -32,13 +32,14 @@ AudioSourceUI.Container = class Container {
this.addButton = addButton;
this.children = [];
- this.container.empty();
+ this.container.textContent = '';
for (const audioSource of toIterable(audioSources)) {
this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
}
- this.addButton.on('click', () => this.onAddAudioSource());
+ this._clickListener = () => this.onAddAudioSource();
+ this.addButton.addEventListener('click', this._clickListener, false);
}
cleanup() {
@@ -46,8 +47,9 @@ AudioSourceUI.Container = class Container {
child.cleanup();
}
- this.addButton.off('click');
- this.container.empty();
+ this.addButton.removeEventListener('click', this._clickListener, false);
+ this.container.textContent = '';
+ this._clickListener = null;
}
save() {
@@ -98,20 +100,28 @@ AudioSourceUI.AudioSource = class AudioSource {
this.audioSource = audioSource;
this.index = index;
- this.container = AudioSourceUI.instantiateTemplate('#audio-source-template').appendTo(parent.container);
- this.select = this.container.find('.audio-source-select');
- this.removeButton = this.container.find('.audio-source-remove');
+ this.container = AudioSourceUI.instantiateTemplate('#audio-source-template');
+ this.select = this.container.querySelector('.audio-source-select');
+ this.removeButton = this.container.querySelector('.audio-source-remove');
- this.select.val(audioSource);
+ this.select.value = audioSource;
- this.select.on('change', () => this.onSelectChanged());
- this.removeButton.on('click', () => this.onRemoveClicked());
+ this._selectChangeListener = () => this.onSelectChanged();
+ this._removeClickListener = () => this.onRemoveClicked();
+
+ this.select.addEventListener('change', this._selectChangeListener, false);
+ this.removeButton.addEventListener('click', this._removeClickListener, false);
+
+ parent.container.appendChild(this.container);
}
cleanup() {
- this.select.off('change');
- this.removeButton.off('click');
- this.container.remove();
+ this.select.removeEventListener('change', this._selectChangeListener, false);
+ this.removeButton.removeEventListener('click', this._removeClickListener, false);
+
+ if (this.container.parentNode !== null) {
+ this.container.parentNode.removeChild(this.container);
+ }
}
save() {
@@ -119,7 +129,7 @@ AudioSourceUI.AudioSource = class AudioSource {
}
onSelectChanged() {
- this.audioSource = this.select.val();
+ this.audioSource = this.select.value;
this.parent.audioSources[this.index] = this.audioSource;
this.save();
}
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
index f63551ed..cff3f521 100644
--- a/ext/bg/js/settings/audio.js
+++ b/ext/bg/js/settings/audio.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,8 +21,12 @@ let audioSourceUI = null;
async function audioSettingsInitialize() {
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add'));
+ const options = await getOptionsMutable(optionsContext);
+ audioSourceUI = new AudioSourceUI.Container(
+ options.audio.sources,
+ document.querySelector('.audio-source-list'),
+ document.querySelector('.audio-source-add')
+ );
audioSourceUI.save = () => settingsSaveOptions();
textToSpeechInitialize();
@@ -34,24 +38,34 @@ function textToSpeechInitialize() {
speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
updateTextToSpeechVoices();
- $('#text-to-speech-voice-test').on('click', () => textToSpeechTest());
+ document.querySelector('#text-to-speech-voice').addEventListener('change', (e) => onTextToSpeechVoiceChange(e), false);
+ document.querySelector('#text-to-speech-voice-test').addEventListener('click', () => textToSpeechTest(), false);
}
function updateTextToSpeechVoices() {
const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
voices.sort(textToSpeechVoiceCompare);
- if (voices.length > 0) {
- $('#text-to-speech-voice-container').css('display', '');
- }
- const select = $('#text-to-speech-voice');
- select.empty();
- select.append($('<option>').val('').text('None'));
+ document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
+
+ const fragment = document.createDocumentFragment();
+
+ let option = document.createElement('option');
+ option.value = '';
+ option.textContent = 'None';
+ fragment.appendChild(option);
+
for (const {voice} of voices) {
- select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`));
+ option = document.createElement('option');
+ option.value = voice.voiceURI;
+ option.textContent = `${voice.name} (${voice.lang})`;
+ fragment.appendChild(option);
}
- select.val(select.attr('data-value'));
+ const select = document.querySelector('#text-to-speech-voice');
+ select.textContent = '';
+ select.appendChild(fragment);
+ select.value = select.dataset.value;
}
function languageTagIsJapanese(languageTag) {
@@ -78,15 +92,13 @@ function textToSpeechVoiceCompare(a, b) {
if (bIsDefault) { return 1; }
}
- if (a.index < b.index) { return -1; }
- if (a.index > b.index) { return 1; }
- return 0;
+ return a.index - b.index;
}
function textToSpeechTest() {
try {
- const text = $('#text-to-speech-voice-test').attr('data-speech-text') || '';
- const voiceURI = $('#text-to-speech-voice').val();
+ const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
+ const voiceURI = document.querySelector('#text-to-speech-voice').value;
const voice = audioGetTextToSpeechVoice(voiceURI);
if (voice === null) { return; }
@@ -100,3 +112,7 @@ function textToSpeechTest() {
// NOP
}
}
+
+function onTextToSpeechVoiceChange(e) {
+ e.currentTarget.dataset.value = e.currentTarget.value;
+}
diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
new file mode 100644
index 00000000..becdc568
--- /dev/null
+++ b/ext/bg/js/settings/backup.js
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2019-2020 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. 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/>.
+ */
+
+
+// Exporting
+
+let _settingsExportToken = null;
+let _settingsExportRevoke = null;
+const SETTINGS_EXPORT_CURRENT_VERSION = 0;
+
+function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) {
+ const values = [
+ date.getUTCFullYear().toString(),
+ dateSeparator,
+ (date.getUTCMonth() + 1).toString().padStart(2, '0'),
+ dateSeparator,
+ date.getUTCDate().toString().padStart(2, '0'),
+ dateTimeSeparator,
+ date.getUTCHours().toString().padStart(2, '0'),
+ timeSeparator,
+ date.getUTCMinutes().toString().padStart(2, '0'),
+ timeSeparator,
+ date.getUTCSeconds().toString().padStart(2, '0')
+ ];
+ return values.slice(0, resolution * 2 - 1).join('');
+}
+
+async function _getSettingsExportData(date) {
+ const optionsFull = await apiOptionsGetFull();
+ const environment = await apiGetEnvironmentInfo();
+
+ const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();
+
+ // Format options
+ for (const {options} of optionsFull.profiles) {
+ if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
+ delete options.anki.fieldTemplates; // Default
+ }
+ }
+
+ const data = {
+ version: SETTINGS_EXPORT_CURRENT_VERSION,
+ date: _getSettingsExportDateString(date, '-', ' ', ':', 6),
+ url: chrome.runtime.getURL('/'),
+ manifest: chrome.runtime.getManifest(),
+ environment,
+ userAgent: navigator.userAgent,
+ options: optionsFull
+ };
+
+ return data;
+}
+
+function _saveBlob(blob, fileName) {
+ if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
+ if (navigator.msSaveBlob(blob)) {
+ return;
+ }
+ }
+
+ const blobUrl = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = blobUrl;
+ a.download = fileName;
+ a.rel = 'noopener';
+ a.target = '_blank';
+
+ const revoke = () => {
+ URL.revokeObjectURL(blobUrl);
+ a.href = '';
+ _settingsExportRevoke = null;
+ };
+ _settingsExportRevoke = revoke;
+
+ a.dispatchEvent(new MouseEvent('click'));
+ setTimeout(revoke, 60000);
+}
+
+async function _onSettingsExportClick() {
+ if (_settingsExportRevoke !== null) {
+ _settingsExportRevoke();
+ _settingsExportRevoke = null;
+ }
+
+ const date = new Date(Date.now());
+
+ const token = {};
+ _settingsExportToken = token;
+ const data = await _getSettingsExportData(date);
+ if (_settingsExportToken !== token) {
+ // A new export has been started
+ return;
+ }
+ _settingsExportToken = null;
+
+ const fileName = `yomichan-settings-${_getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
+ const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
+ _saveBlob(blob, fileName);
+}
+
+
+// Importing
+
+async function _settingsImportSetOptionsFull(optionsFull) {
+ return utilIsolate(await utilBackend().setFullOptions(
+ utilBackgroundIsolate(optionsFull)
+ ));
+}
+
+function _showSettingsImportError(error) {
+ logError(error);
+ document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
+ $('#settings-import-error-modal').modal('show');
+}
+
+async function _showSettingsImportWarnings(warnings) {
+ const modalNode = $('#settings-import-warning-modal');
+ const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
+ const messageContainer = document.querySelector('#settings-import-warning-modal-message');
+ if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
+ return {result: false};
+ }
+
+ // Set message
+ const fragment = document.createDocumentFragment();
+ for (const warning of warnings) {
+ const node = document.createElement('li');
+ node.textContent = `${warning}`;
+ fragment.appendChild(node);
+ }
+ messageContainer.textContent = '';
+ messageContainer.appendChild(fragment);
+
+ // Show modal
+ modalNode.modal('show');
+
+ // Wait for modal to close
+ return new Promise((resolve) => {
+ const onButtonClick = (e) => {
+ e.preventDefault();
+ complete({
+ result: true,
+ sanitize: e.currentTarget.dataset.importSanitize === 'true'
+ });
+ modalNode.modal('hide');
+
+ };
+ const onModalHide = () => {
+ complete({result: false});
+ };
+
+ let completed = false;
+ const complete = (result) => {
+ if (completed) { return; }
+ completed = true;
+
+ modalNode.off('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.removeEventListener('click', onButtonClick, false);
+ }
+
+ resolve(result);
+ };
+
+ // Hook events
+ modalNode.on('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.addEventListener('click', onButtonClick, false);
+ }
+ });
+}
+
+function _isLocalhostUrl(urlString) {
+ try {
+ const url = new URL(urlString);
+ switch (url.hostname.toLowerCase()) {
+ case 'localhost':
+ case '127.0.0.1':
+ case '[::1]':
+ switch (url.protocol.toLowerCase()) {
+ case 'http:':
+ case 'https:':
+ return true;
+ }
+ break;
+ }
+ } catch (e) {
+ // NOP
+ }
+ return false;
+}
+
+function _settingsImportSanitizeProfileOptions(options, dryRun) {
+ const warnings = [];
+
+ const anki = options.anki;
+ if (isObject(anki)) {
+ const fieldTemplates = anki.fieldTemplates;
+ if (typeof fieldTemplates === 'string') {
+ warnings.push('anki.fieldTemplates contains a non-default value');
+ if (!dryRun) {
+ delete anki.fieldTemplates;
+ }
+ }
+ const server = anki.server;
+ if (typeof server === 'string' && server.length > 0 && !_isLocalhostUrl(server)) {
+ warnings.push('anki.server uses a non-localhost URL');
+ if (!dryRun) {
+ delete anki.server;
+ }
+ }
+ }
+
+ const audio = options.audio;
+ if (isObject(audio)) {
+ const customSourceUrl = audio.customSourceUrl;
+ if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !_isLocalhostUrl(customSourceUrl)) {
+ warnings.push('audio.customSourceUrl uses a non-localhost URL');
+ if (!dryRun) {
+ delete audio.customSourceUrl;
+ }
+ }
+ }
+
+ return warnings;
+}
+
+function _settingsImportSanitizeOptions(optionsFull, dryRun) {
+ const warnings = new Set();
+
+ const profiles = optionsFull.profiles;
+ if (Array.isArray(profiles)) {
+ for (const profile of profiles) {
+ if (!isObject(profile)) { continue; }
+ const options = profile.options;
+ if (!isObject(options)) { continue; }
+
+ const warnings2 = _settingsImportSanitizeProfileOptions(options, dryRun);
+ for (const warning of warnings2) {
+ warnings.add(warning);
+ }
+ }
+ }
+
+ return warnings;
+}
+
+function _utf8Decode(arrayBuffer) {
+ try {
+ return new TextDecoder('utf-8').decode(arrayBuffer);
+ } catch (e) {
+ const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
+ return decodeURIComponent(escape(binaryString));
+ }
+}
+
+async function _importSettingsFile(file) {
+ const dataString = _utf8Decode(await utilReadFileArrayBuffer(file));
+ const data = JSON.parse(dataString);
+
+ // Type check
+ if (!isObject(data)) {
+ throw new Error(`Invalid data type: ${typeof data}`);
+ }
+
+ // Version check
+ const version = data.version;
+ if (!(
+ typeof version === 'number' &&
+ Number.isFinite(version) &&
+ version === Math.floor(version)
+ )) {
+ throw new Error(`Invalid version: ${version}`);
+ }
+
+ if (!(
+ version >= 0 &&
+ version <= SETTINGS_EXPORT_CURRENT_VERSION
+ )) {
+ throw new Error(`Unsupported version: ${version}`);
+ }
+
+ // Verify options exists
+ let optionsFull = data.options;
+ if (!isObject(optionsFull)) {
+ throw new Error(`Invalid options type: ${typeof optionsFull}`);
+ }
+
+ // Upgrade options
+ optionsFull = optionsUpdateVersion(optionsFull, {});
+
+ // Check for warnings
+ const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true);
+
+ // Show sanitization warnings
+ if (sanitizationWarnings.size > 0) {
+ const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings);
+ if (!result) { return; }
+
+ if (sanitize !== false) {
+ _settingsImportSanitizeOptions(optionsFull, false);
+ }
+ }
+
+ // Assign options
+ await _settingsImportSetOptionsFull(optionsFull);
+
+ // Reload settings page
+ window.location.reload();
+}
+
+function _onSettingsImportClick() {
+ document.querySelector('#settings-import-file').click();
+}
+
+function _onSettingsImportFileChange(e) {
+ const files = e.target.files;
+ if (files.length === 0) { return; }
+
+ const file = files[0];
+ e.target.value = null;
+ _importSettingsFile(file).catch(_showSettingsImportError);
+}
+
+
+// Resetting
+
+function _onSettingsResetClick() {
+ $('#settings-reset-modal').modal('show');
+}
+
+async function _onSettingsResetConfirmClick() {
+ $('#settings-reset-modal').modal('hide');
+
+ // Get default options
+ const optionsFull = optionsGetDefault();
+
+ // Assign options
+ await _settingsImportSetOptionsFull(optionsFull);
+
+ // Reload settings page
+ window.location.reload();
+}
+
+
+// Setup
+
+window.addEventListener('DOMContentLoaded', () => {
+ document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false);
+ document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false);
+ document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false);
+ document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false);
+ document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false);
+}, false);
diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index cc9db087..4d041451 100644
--- a/ext/bg/js/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index 065a8abc..ed171ae9 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -189,6 +189,7 @@ class SettingsDictionaryEntryUI {
this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title;
this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`;
+ this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported;
this.applyValues();
@@ -272,7 +273,7 @@ class SettingsDictionaryEntryUI {
progress.hidden = true;
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
}
}
@@ -356,9 +357,10 @@ async function dictSettingsInitialize() {
document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false);
document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false);
document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false);
+ document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false);
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDictionaryOptionsChanged(options);
onDatabaseUpdated(options);
}
@@ -366,6 +368,9 @@ async function dictSettingsInitialize() {
async function onDictionaryOptionsChanged(options) {
if (dictionaryUI === null) { return; }
dictionaryUI.setOptionsDictionaries(options.dictionaries);
+
+ const optionsFull = await apiOptionsGetFull();
+ document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
}
async function onDatabaseUpdated(options) {
@@ -420,7 +425,7 @@ async function updateMainDictionarySelect(options, dictionaries) {
async function onDictionaryMainChanged(e) {
const value = e.target.value;
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
options.general.mainDictionary = value;
settingsSaveOptions();
}
@@ -526,14 +531,14 @@ async function onDictionaryPurge(e) {
dictionarySpinnerShow(true);
await utilDatabasePurge();
- for (const options of toIterable(await getOptionsArray())) {
+ for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
options.dictionaries = utilBackgroundIsolate({});
options.general.mainDictionary = '';
}
await settingsSaveOptions();
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
} catch (err) {
dictionaryErrorsShow([err]);
@@ -552,6 +557,9 @@ async function onDictionaryPurge(e) {
}
async function onDictionaryImport(e) {
+ const files = [...e.target.files];
+ e.target.value = null;
+
const dictFile = $('#dict-file');
const dictControls = $('#dict-importer').hide();
const dictProgress = $('#dict-import-progress').show();
@@ -572,8 +580,11 @@ async function onDictionaryImport(e) {
}
};
- const exceptions = [];
- const files = [...e.target.files];
+ const optionsFull = await apiOptionsGetFull();
+
+ const importDetails = {
+ prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
+ };
for (let i = 0, ii = files.length; i < ii; ++i) {
setProgress(0.0);
@@ -582,25 +593,26 @@ async function onDictionaryImport(e) {
dictImportInfo.textContent = `(${i + 1} of ${ii})`;
}
- const summary = await utilDatabaseImport(files[i], updateProgress, exceptions);
- for (const options of toIterable(await getOptionsArray())) {
+ const {result, errors} = await utilDatabaseImport(files[i], updateProgress, importDetails);
+ for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
dictionaryOptions.enabled = true;
- options.dictionaries[summary.title] = dictionaryOptions;
- if (summary.sequenced && options.general.mainDictionary === '') {
- options.general.mainDictionary = summary.title;
+ options.dictionaries[result.title] = dictionaryOptions;
+ if (result.sequenced && options.general.mainDictionary === '') {
+ options.general.mainDictionary = result.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);
+ if (errors.length > 0) {
+ errors.push(...errors);
+ errors.push(`Dictionary may not have been imported properly: ${errors.length} error${errors.length === 1 ? '' : 's'} reported.`);
+ dictionaryErrorsShow(errors);
}
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
}
} catch (err) {
@@ -616,3 +628,12 @@ async function onDictionaryImport(e) {
dictProgress.hide();
}
}
+
+
+async function onDatabaseEnablePrefixWildcardSearchesChanged(e) {
+ const optionsFull = await getOptionsFullMutable();
+ const v = !!e.target.checked;
+ if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
+ optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
+ await settingsSaveOptions();
+}
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index 7456e7a4..56828a15 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,12 +13,17 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-async function getOptionsArray() {
- const optionsFull = await apiOptionsGetFull();
- return optionsFull.profiles.map((profile) => profile.options);
+function getOptionsMutable(optionsContext) {
+ return utilBackend().getOptions(
+ utilBackgroundIsolate(optionsContext)
+ );
+}
+
+function getOptionsFullMutable() {
+ return utilBackend().getFullOptions();
}
async function formRead(options) {
@@ -75,7 +80,6 @@ async function formRead(options) {
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();
@@ -140,9 +144,8 @@ async function formWrite(options) {
$('#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);
- onAnkiTemplatesValidateCompile();
+ await ankiTemplatesUpdateValue();
await onAnkiOptionsChanged(options);
await onDictionaryOptionsChanged(options);
@@ -161,7 +164,9 @@ function formUpdateVisibility(options) {
if (options.general.debugInfo) {
const temp = utilIsolate(options);
- temp.anki.fieldTemplates = '...';
+ if (typeof temp.anki.fieldTemplates === 'string') {
+ temp.anki.fieldTemplates = '...';
+ }
const text = JSON.stringify(temp, null, 4);
$('#debug').text(text);
}
@@ -169,7 +174,7 @@ function formUpdateVisibility(options) {
async function onFormOptionsChanged() {
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formRead(options);
await settingsSaveOptions();
@@ -195,21 +200,10 @@ async function onOptionsUpdate({source}) {
if (source === thisSource) { return; }
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formWrite(options);
}
-function onMessage({action, params}, sender, callback) {
- switch (action) {
- case 'optionsUpdate':
- onOptionsUpdate(params);
- break;
- case 'getUrl':
- callback({url: window.location.href});
- break;
- }
-}
-
function showExtensionInformation() {
const node = document.getElementById('extension-info');
@@ -233,7 +227,7 @@ async function onReady() {
storageInfoInitialize();
- chrome.runtime.onMessage.addListener(onMessage);
+ yomichan.on('optionsUpdate', onOptionsUpdate);
}
$(document).ready(() => onReady());
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index 49409968..2b727cbd 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,6 +24,7 @@ class SettingsPopupPreview {
this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet;
this.popupShown = false;
this.themeChangeTimeout = null;
+ this.textSource = null;
}
static create() {
@@ -46,16 +47,18 @@ class SettingsPopupPreview {
window.apiOptionsGet = (...args) => this.apiOptionsGet(...args);
// Overwrite frontend
- this.frontend = Frontend.create();
- window.yomichan_frontend = this.frontend;
+ const popupHost = new PopupProxyHost();
+ await popupHost.prepare();
+
+ const popup = popupHost.createPopup(null, 0);
+ popup.setChildrenSupported(false);
+
+ this.frontend = new Frontend(popup);
this.frontend.setEnabled = function () {};
this.frontend.searchClear = function () {};
- this.frontend.popup.childrenSupported = false;
- this.frontend.popup.interactive = false;
-
- await this.frontend.isPrepared();
+ await this.frontend.prepare();
// Overwrite popup
Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args);
@@ -95,7 +98,7 @@ class SettingsPopupPreview {
onWindowResize() {
if (this.frontend === null) { return; }
- const textSource = this.frontend.textSourceLast;
+ const textSource = this.textSource;
if (textSource === null) { return; }
const elementRect = textSource.getRect();
@@ -105,11 +108,10 @@ class SettingsPopupPreview {
onMessage(e) {
const {action, params} = e.data;
- const handlers = SettingsPopupPreview.messageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- handler(this, params);
- }
+ const handler = SettingsPopupPreview._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+
+ handler(this, params);
}
onThemeDarkCheckboxChanged(node) {
@@ -160,13 +162,14 @@ class SettingsPopupPreview {
const source = new TextSourceRange(range, range.toString(), null);
try {
- await this.frontend.searchSource(source, 'script');
+ await this.frontend.onSearchSource(source, 'script');
} finally {
source.cleanup();
}
- await this.frontend.lastShowPromise;
+ this.textSource = source;
+ await this.frontend.showContentCompleted();
- if (this.frontend.popup.isVisible()) {
+ if (this.frontend.popup.isVisibleSync()) {
this.popupShown = true;
}
@@ -174,11 +177,11 @@ class SettingsPopupPreview {
}
}
-SettingsPopupPreview.messageHandlers = {
- setText: (self, {text}) => self.setText(text),
- setCustomCss: (self, {css}) => self.setCustomCss(css),
- setCustomOuterCss: (self, {css}) => self.setCustomOuterCss(css)
-};
+SettingsPopupPreview._messageHandlers = new Map([
+ ['setText', (self, {text}) => self.setText(text)],
+ ['setCustomCss', (self, {css}) => self.setCustomCss(css)],
+ ['setCustomOuterCss', (self, {css}) => self.setCustomOuterCss(css)]
+]);
SettingsPopupPreview.instance = SettingsPopupPreview.create();
diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js
index d8579eb1..0d20471e 100644
--- a/ext/bg/js/settings/popup-preview.js
+++ b/ext/bg/js/settings/popup-preview.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index 8c218e97..c4e68b53 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let currentProfileIndex = 0;
@@ -27,7 +27,7 @@ function getOptionsContext() {
async function profileOptionsSetup() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
currentProfileIndex = optionsFull.profileCurrent;
profileOptionsSetupEventListeners();
@@ -120,7 +120,7 @@ async function profileOptionsUpdateTarget(optionsFull) {
profileFormWrite(optionsFull);
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formWrite(options);
}
@@ -164,13 +164,13 @@ async function onProfileOptionsChanged(e) {
return;
}
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
await profileFormRead(optionsFull);
await settingsSaveOptions();
}
async function onTargetProfileChanged() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
if (index === null || currentProfileIndex === index) {
return;
@@ -182,7 +182,7 @@ async function onTargetProfileChanged() {
}
async function onProfileAdd() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100);
optionsFull.profiles.push(profile);
@@ -210,7 +210,7 @@ async function onProfileRemove(e) {
async function onProfileRemoveConfirm() {
$('#profile-remove-modal').modal('hide');
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
if (optionsFull.profiles.length <= 1) {
return;
}
@@ -234,7 +234,7 @@ function onProfileNameChanged() {
}
async function onProfileMove(offset) {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = currentProfileIndex + offset;
if (index < 0 || index >= optionsFull.profiles.length) {
return;
@@ -267,7 +267,7 @@ async function onProfileCopy() {
async function onProfileCopyConfirm() {
$('#profile-copy-modal').modal('hide');
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
if (index === null || index === currentProfileIndex) {
return;
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
index 51ca6855..6c10f665 100644
--- a/ext/bg/js/settings/storage.js
+++ b/ext/bg/js/settings/storage.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index 9320477f..eae4e014 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -143,11 +143,11 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
},"33":function(container,depth0,helpers,partials,data) {
return "class=\"source-term\"";
},"35":function(container,depth0,helpers,partials,data) {
- return "class=\"source-term term-button-fade\"";
+ return "class=\"source-term invisible\"";
},"37":function(container,depth0,helpers,partials,data) {
return "class=\"next-term\"";
},"39":function(container,depth0,helpers,partials,data) {
- return "class=\"next-term term-button-fade\"";
+ return "class=\"next-term invisible\"";
},"41":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
@@ -491,11 +491,11 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
},"67":function(container,depth0,helpers,partials,data) {
return "class=\"source-term\"";
},"69":function(container,depth0,helpers,partials,data) {
- return "class=\"source-term term-button-fade\"";
+ return "class=\"source-term invisible\"";
},"71":function(container,depth0,helpers,partials,data) {
return "class=\"next-term\"";
},"73":function(container,depth0,helpers,partials,data) {
- return "class=\"next-term term-button-fade\"";
+ return "class=\"next-term invisible\"";
},"75":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 202014c9..7473c6ad 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -230,7 +230,7 @@ class Translator {
const titles = Object.keys(dictionaries);
const deinflections = (
details.wildcard ?
- await this.findTermWildcard(text, titles) :
+ await this.findTermWildcard(text, titles, details.wildcard) :
await this.findTermDeinflections(text, titles)
);
@@ -268,8 +268,8 @@ class Translator {
return [definitions, length];
}
- async findTermWildcard(text, titles) {
- const definitions = await this.database.findTermsBulk([text], titles, true);
+ async findTermWildcard(text, titles, wildcard) {
+ const definitions = await this.database.findTermsBulk([text], titles, wildcard);
if (definitions.length === 0) {
return [];
}
@@ -308,7 +308,7 @@ class Translator {
deinflectionArray.push(deinflection);
}
- const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, false);
+ const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, null);
for (const definition of definitions) {
const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 3dd5fd55..333e814b 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,11 +13,40 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-function utilIsolate(data) {
- return JSON.parse(JSON.stringify(data));
+function utilIsolate(value) {
+ if (value === null) { return null; }
+
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ return value;
+ }
+
+ const stringValue = JSON.stringify(value);
+ return typeof stringValue === 'string' ? JSON.parse(stringValue) : null;
+}
+
+function utilFunctionIsolate(func) {
+ return function (...args) {
+ try {
+ args = args.map((v) => utilIsolate(v));
+ return func.call(this, ...args);
+ } catch (e) {
+ try {
+ String(func);
+ } catch (e2) {
+ // Dead object
+ return;
+ }
+ throw e;
+ }
+ };
}
function utilBackgroundIsolate(data) {
@@ -25,6 +54,11 @@ function utilBackgroundIsolate(data) {
return backgroundPage.utilIsolate(data);
}
+function utilBackgroundFunctionIsolate(func) {
+ const backgroundPage = chrome.extension.getBackgroundPage();
+ return backgroundPage.utilFunctionIsolate(func);
+}
+
function utilSetEqual(setA, setB) {
if (setA.size !== setB.size) {
return false;
@@ -54,6 +88,8 @@ function utilSetDifference(setA, setB) {
function utilStringHashCode(string) {
let hashCode = 0;
+ if (typeof string !== 'string') { return hashCode; }
+
for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) {
hashCode = ((hashCode << 5) - hashCode) + charCode;
hashCode |= 0;
@@ -63,44 +99,52 @@ function utilStringHashCode(string) {
}
function utilBackend() {
- return chrome.extension.getBackgroundPage().yomichan_backend;
+ return chrome.extension.getBackgroundPage().yomichanBackend;
}
-function utilAnkiGetModelNames() {
- return utilBackend().anki.getModelNames();
+async function utilAnkiGetModelNames() {
+ return utilIsolate(await utilBackend().anki.getModelNames());
}
-function utilAnkiGetDeckNames() {
- return utilBackend().anki.getDeckNames();
+async function utilAnkiGetDeckNames() {
+ return utilIsolate(await utilBackend().anki.getDeckNames());
}
-function utilDatabaseGetDictionaryInfo() {
- return utilBackend().translator.database.getDictionaryInfo();
+async function utilDatabaseGetDictionaryInfo() {
+ return utilIsolate(await utilBackend().translator.database.getDictionaryInfo());
}
-function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
- return utilBackend().translator.database.getDictionaryCounts(dictionaryNames, getTotal);
+async function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
+ return utilIsolate(await utilBackend().translator.database.getDictionaryCounts(
+ utilBackgroundIsolate(dictionaryNames),
+ utilBackgroundIsolate(getTotal)
+ ));
}
-function utilAnkiGetModelFieldNames(modelName) {
- return utilBackend().anki.getModelFieldNames(modelName);
+async function utilAnkiGetModelFieldNames(modelName) {
+ return utilIsolate(await utilBackend().anki.getModelFieldNames(
+ utilBackgroundIsolate(modelName)
+ ));
}
-function utilDatabasePurge() {
- return utilBackend().translator.purgeDatabase();
+async function utilDatabasePurge() {
+ return utilIsolate(await utilBackend().translator.purgeDatabase());
}
-function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
- return utilBackend().translator.database.deleteDictionary(dictionaryName, onProgress);
+async function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
+ return utilIsolate(await utilBackend().translator.database.deleteDictionary(
+ utilBackgroundIsolate(dictionaryName),
+ utilBackgroundFunctionIsolate(onProgress)
+ ));
}
-async function utilDatabaseImport(data, progress, exceptions) {
- // Edge cannot read data on the background page due to the File object
- // being created from a different window. Read on the same page instead.
- if (EXTENSION_IS_BROWSER_EDGE) {
- data = await utilReadFile(data);
- }
- return utilBackend().translator.database.importDictionary(data, progress, exceptions);
+async function utilDatabaseImport(data, onProgress, details) {
+ data = await utilReadFile(data);
+ return utilIsolate(await utilBackend().translator.database.importDictionary(
+ utilBackgroundIsolate(data),
+ utilBackgroundFunctionIsolate(onProgress),
+ utilBackgroundIsolate(details)
+ ));
}
function utilReadFile(file) {
@@ -111,3 +155,12 @@ function utilReadFile(file) {
reader.readAsBinaryString(file);
});
}
+
+function utilReadFileArrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ reader.readAsArrayBuffer(file);
+ });
+}
diff --git a/ext/bg/legal.html b/ext/bg/legal.html
index 082239d7..c1e606d7 100644
--- a/ext/bg/legal.html
+++ b/ext/bg/legal.html
@@ -17,7 +17,7 @@
<div class="container">
<h3>Yomichan License</h3>
<pre>
-Copyright (C) 2016-2019 Alex Yatskov
+Copyright (C) 2016-2020 Alex Yatskov
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
@@ -30,7 +30,7 @@ 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 &lt;http://www.gnu.org/licenses/&gt;.
+along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;.
</pre>
<h3>EDRDG License</h3>
<pre>
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 58bb9ba8..409243dd 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -62,11 +62,11 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/api.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/templates.js"></script>
- <script src="/fg/js/api.js"></script>
<script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script>
<script src="/mixed/js/audio.js"></script>
@@ -74,6 +74,7 @@
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/mixed/js/scroll.js"></script>
+ <script src="/mixed/js/text-scanner.js"></script>
<script src="/bg/js/search-query-parser.js"></script>
<script src="/bg/js/search.js"></script>
diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html
index 339467d4..f33ecedf 100644
--- a/ext/bg/settings-popup-preview.html
+++ b/ext/bg/settings-popup-preview.html
@@ -119,13 +119,13 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/api.js"></script>
+ <script src="/mixed/js/text-scanner.js"></script>
- <script src="/fg/js/api.js"></script>
<script src="/fg/js/document.js"></script>
<script src="/fg/js/frontend-api-receiver.js"></script>
<script src="/fg/js/popup.js"></script>
<script src="/fg/js/source.js"></script>
- <script src="/fg/js/util.js"></script>
<script src="/fg/js/popup-proxy-host.js"></script>
<script src="/fg/js/frontend.js"></script>
<script src="/bg/js/settings/popup-preview-frame.js"></script>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 3c5494b8..4c973674 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -299,7 +299,7 @@
<input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
</div>
- <div class="form-group" style="display: none;" id="text-to-speech-voice-container">
+ <div class="form-group" id="text-to-speech-voice-container" hidden>
<label for="text-to-speech-voice">Text-to-speech voice</label>
<div class="input-group">
<select class="form-control" id="text-to-speech-voice"></select>
@@ -365,12 +365,12 @@
<div class="form-group options-advanced">
<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">
+ <input type="number" min="0" id="scan-delay" class="form-control">
</div>
<div class="form-group options-advanced">
<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">
+ <input type="number" min="1" step="1" id="scan-length" class="form-control">
</div>
<div class="form-group">
@@ -406,7 +406,7 @@
<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">
+ <input type="number" min="0" step="1" id="popup-nesting-max-depth" class="form-control">
</div>
</div>
@@ -491,6 +491,18 @@
<div hidden><input type="file" id="dict-file" accept=".zip,application/zip" multiple></div>
</div>
+ <div>
+ <h3>Dictionary Options</h3>
+ </div>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="database-enable-prefix-wildcard-searches"> Enable prefix wildcard searches</label>
+ <p class="help-block">
+ This option only applies to newly imported dictionaries.
+ Enabling this option will also cause dictionary data to take up slightly more storage space.
+ </p>
+ </div>
+
<div class="modal fade" tabindex="-1" role="dialog" id="dict-purge-modal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
@@ -538,6 +550,9 @@
<div class="checkbox options-advanced">
<label><input type="checkbox" class="dict-allow-secondary-searches"> Allow secondary searches</label>
</div>
+ <div class="checkbox dict-prefix-wildcard-searches-supported-container">
+ <label><input type="checkbox" class="dict-prefix-wildcard-searches-supported" disabled> Prefix wildcard searches supported</label>
+ </div>
<div class="form-group options-advanced">
<label class="dict-result-priority-label">Result priority</label>
<input type="number" class="form-control dict-priority">
@@ -659,7 +674,7 @@
<div class="form-group options-advanced">
<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">
+ <input type="number" min="1" step="1" id="sentence-detection-extent" class="form-control">
</div>
<div class="form-group options-advanced">
@@ -739,7 +754,9 @@
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 reset to the default template settings.
</p>
- <textarea autocomplete="off" spellcheck="false" wrap="soft" class="form-control" rows="10" id="field-templates"></textarea>
+ <div class="ignore-form-changes">
+ <textarea autocomplete="off" spellcheck="false" wrap="soft" class="form-control" rows="10" id="field-templates"></textarea>
+ </div>
<div>
<button class="btn btn-danger" id="field-templates-reset">Reset Templates</button>
</div>
@@ -836,6 +853,102 @@
</ul>
</div>
+
+ <div>
+ <h3>Backup</h3>
+
+ <p class="help-block">
+ Yomichan can import and export settings files which can be used to restore settings,
+ share settings across devices, or help to debug problems.
+ These files will only contain settings and will not contain dictionaries.
+ Dictionaries must be imported separately.
+ </p>
+
+ <div>
+ <button class="btn btn-default" id="settings-export">Export Settings</button>
+ <button class="btn btn-default" id="settings-import">Import Settings</button>
+ <button class="btn btn-danger" id="settings-reset">Reset Default Settings</button>
+ </div>
+
+ <div hidden><input type="file" id="settings-import-file" accept=".json,application/json"></div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="settings-import-error-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">Import Error</h4>
+ </div>
+ <div class="modal-body">
+ <p>
+ An error occurred while trying to import the settings file:
+ </p>
+ <p class="text-danger" id="settings-import-error-modal-message"></p>
+ <p>
+ Additional info can be found in the developer console.
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="settings-import-warning-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">Import Security Warning</h4>
+ </div>
+ <div class="modal-body">
+ <p>
+ Settings file contains settings which may pose a security risk.
+ Only import settings from sources you trust.
+ </p>
+ <ul class="text-danger" id="settings-import-warning-modal-message"></ul>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger settings-import-warning-modal-import-button">Import</button>
+ <button type="button" class="btn btn-primary settings-import-warning-modal-import-button" data-import-sanitize="true">Sanitize and Import</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="settings-reset-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">Settings Reset</h4>
+ </div>
+ <div class="modal-body">
+ <p class="text-danger">
+ You are about to reset all Yomichan settings back to their default values.
+ This will delete all custom profiles you may have created.
+ <strong>This action cannot be undone.</strong>
+ </p>
+ <p>
+ Consider making a backup using the "Export Settings" button before resetting
+ if you want to be able to revert.
+ </p>
+ <p>
+ Dictionary data will not be deleted, but any installed dictionaries
+ will need to be re-enabled.
+ </p>
+ </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="settings-reset-modal-confirm">Reset All Settings</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
<div>
<h3>Support Development</h3>
@@ -866,13 +979,11 @@
<script src="/mixed/js/core.js"></script>
<script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/api.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki.js"></script>
- <script src="/bg/js/api.js"></script>
- <script src="/bg/js/audio-ui.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>
@@ -885,6 +996,9 @@
<script src="/bg/js/settings/anki.js"></script>
<script src="/bg/js/settings/anki-templates.js"></script>
<script src="/bg/js/settings/audio.js"></script>
+ <script src="/bg/js/settings/audio-ui.js"></script>
+ <script src="/bg/js/settings/backup.js"></script>
+ <script src="/bg/js/settings/conditions-ui.js"></script>
<script src="/bg/js/settings/dictionaries.js"></script>
<script src="/bg/js/settings/popup-preview.js"></script>
<script src="/bg/js/settings/profiles.js"></script>