summaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html5
-rw-r--r--ext/bg/context.html70
-rw-r--r--ext/bg/css/settings.css49
-rw-r--r--ext/bg/js/anki.js12
-rw-r--r--ext/bg/js/api.js130
-rw-r--r--ext/bg/js/audio.js10
-rw-r--r--ext/bg/js/backend-api-forwarder.js4
-rw-r--r--ext/bg/js/backend.js21
-rw-r--r--ext/bg/js/conditions-ui.js20
-rw-r--r--ext/bg/js/conditions.js30
-rw-r--r--ext/bg/js/context.js57
-rw-r--r--ext/bg/js/database.js428
-rw-r--r--ext/bg/js/deinflector.js4
-rw-r--r--ext/bg/js/dictionary.js138
-rw-r--r--ext/bg/js/handlebars.js43
-rw-r--r--ext/bg/js/mecab.js92
-rw-r--r--ext/bg/js/options.js19
-rw-r--r--ext/bg/js/page-exit-prevention.js60
-rw-r--r--ext/bg/js/profile-conditions.js24
-rw-r--r--ext/bg/js/request.js2
-rw-r--r--ext/bg/js/search-frontend.js7
-rw-r--r--ext/bg/js/search-query-parser.js228
-rw-r--r--ext/bg/js/search.js81
-rw-r--r--ext/bg/js/settings.js954
-rw-r--r--ext/bg/js/settings/anki-templates.js109
-rw-r--r--ext/bg/js/settings/anki.js247
-rw-r--r--ext/bg/js/settings/audio.js102
-rw-r--r--ext/bg/js/settings/dictionaries.js618
-rw-r--r--ext/bg/js/settings/main.js239
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js (renamed from ext/bg/js/settings-popup-preview.js)2
-rw-r--r--ext/bg/js/settings/popup-preview.js62
-rw-r--r--ext/bg/js/settings/profiles.js (renamed from ext/bg/js/settings-profiles.js)26
-rw-r--r--ext/bg/js/settings/storage.js138
-rw-r--r--ext/bg/js/templates.js310
-rw-r--r--ext/bg/js/translator.js137
-rw-r--r--ext/bg/js/util.js26
-rw-r--r--ext/bg/legal.html4
-rw-r--r--ext/bg/search.html26
-rw-r--r--ext/bg/settings-popup-preview.html6
-rw-r--r--ext/bg/settings.html241
40 files changed, 3249 insertions, 1532 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 3ab68639..5a6970c3 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -14,14 +14,15 @@
<body>
<div id="clipboard-paste-target" contenteditable="true"></div>
- <script src="/mixed/lib/dexie.min.js"></script>
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/jszip.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
- <script src="/mixed/js/extension.js"></script>
+ <script src="/mixed/js/core.js"></script>
+ <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/audio.js"></script>
<script src="/bg/js/backend-api-forwarder.js"></script>
diff --git a/ext/bg/context.html b/ext/bg/context.html
index 7e08dddd..eda09a68 100644
--- a/ext/bg/context.html
+++ b/ext/bg/context.html
@@ -11,7 +11,6 @@
<link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css">
- <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.css">
<style type="text/css">
body {
padding: 10px;
@@ -89,12 +88,73 @@
.link-group-label {
vertical-align: middle;
}
+
+
+ .toggle {
+ width: 60px;
+ height: 34px;
+ position: relative;
+ overflow: hidden;
+ }
+ .toggle-group {
+ position: absolute;
+ width: 200%;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ user-select: none;
+ }
+ .toggle-group.toggle-group-animated {
+ transition: transform 0.35s;
+ }
+ .toggle-on,
+ .toggle-off {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ }
+ .toggle-on {
+ padding-right: 24px;
+ left: 0;
+ right: 50%;
+ }
+ .toggle-off {
+ padding-left: 24px;
+ left: 50%;
+ right: 0;
+ }
+ .toggle-handle {
+ position: relative;
+ margin: 0 auto;
+ padding-top: 0;
+ padding-bottom: 0;
+ height: 100%;
+ width: 0;
+ border-width: 0 1px;
+ }
+
+ .toggle>input[type=checkbox] {
+ display: none;
+ }
+ .toggle>input[type=checkbox]:not(:checked)~.toggle-group {
+ transform: translateX(-50%);
+ }
</style>
</head>
<body>
<div id="mini">
<div>
- <input type="checkbox" id="enable-search">
+ <label class="btn btn-primary toggle">
+ <input type="checkbox" id="enable-search" />
+ <div class="toggle-group">
+ <span class="btn btn-primary toggle-on">On</span>
+ <span class="btn btn-default active toggle-off">Off</span>
+ <span class="btn btn-default toggle-handle"></span>
+ </div>
+ </label>
</div>
<div class="btn-group">
<a title="Search (Alt + Insert)&#10;(Middle click to open in new tab)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></a>
@@ -118,10 +178,8 @@
</a>
</div>
- <script src="/mixed/lib/jquery.min.js"></script>
- <script src="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.js"></script>
-
- <script src="/mixed/js/extension.js"></script>
+ <script src="/mixed/js/core.js"></script>
+ <script src="/mixed/js/dom.js"></script>
<script src="/bg/js/api.js"></script>
<script src="/bg/js/options.js"></script>
diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index b3d5b884..8adae47c 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -17,9 +17,16 @@
*/
-#anki-spinner, #anki-general, #anki-error,
-#dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress,
-#debug, .options-advanced, .storage-hidden, #storage-spinner {
+#anki-spinner,
+#dict-spinner, #dict-import-progress,
+.storage-hidden, #storage-spinner {
+ display: none;
+}
+
+html:root:not([data-options-anki-enable=true]) #anki-general,
+html:root:not([data-options-general-debug-info=true]) .debug,
+html:root:not([data-options-general-show-advanced=true]) .options-advanced,
+html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group {
display: none;
}
@@ -30,12 +37,6 @@
padding: 10px;
}
-#field-templates {
- font-family: monospace;
- overflow-x: hidden;
- white-space: pre;
-}
-
.bottom-links {
padding-bottom: 1em;
}
@@ -129,14 +130,24 @@
}
#custom-popup-css,
-#custom-popup-outer-css {
+#custom-popup-outer-css,
+#field-templates {
width: 100%;
min-height: 34px;
+ line-height: 18px;
height: 96px;
resize: vertical;
font-family: 'Courier New', Courier, monospace;
white-space: pre;
}
+#field-templates {
+ height: 240px;
+ border-bottom-left-radius: 0;
+}
+#field-templates-reset {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
.btn-inner-middle {
vertical-align: middle;
@@ -158,6 +169,24 @@ input[type=checkbox].storage-button-checkbox {
height: 320px;
}
+.dict-delete-table {
+ display: table;
+ width: 100%;
+}
+.dict-delete-table>*:first-child {
+ display: table-cell;
+ vertical-align: middle;
+ padding-right: 1em;
+}
+.dict-delete-table>*:nth-child(n+2) {
+ display: table-cell;
+ width: 100%;
+ vertical-align: middle;
+}
+.dict-delete-table .progress {
+ margin: 0;
+}
+
[data-show-for-browser],
[data-show-for-operating-system] {
display: none;
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 9f851f13..17b93620 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -74,7 +74,7 @@ class AnkiConnect {
async findNoteIds(notes) {
await this.checkVersion();
- const actions = notes.map(note => ({
+ const actions = notes.map((note) => ({
action: 'findNotes',
params: {
query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}`
@@ -108,11 +108,11 @@ class AnkiConnect {
*/
class AnkiNull {
- async addNote(note) {
+ async addNote() {
return null;
}
- async canAddNotes(notes) {
+ async canAddNotes() {
return [];
}
@@ -124,15 +124,15 @@ class AnkiNull {
return [];
}
- async getModelFieldNames(modelName) {
+ async getModelFieldNames() {
return [];
}
- async guiBrowse(query) {
+ async guiBrowse() {
return [];
}
- async findNoteIds(notes) {
+ async findNoteIds() {
return [];
}
}
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index 3209cc31..b489b8d2 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -25,42 +25,36 @@ async function apiOptionsSet(changedOptions, optionsContext, source) {
const options = await apiOptionsGet(optionsContext);
function getValuePaths(obj) {
- let valuePaths = [];
- let nodes = [{
- obj,
- path: []
- }];
+ const valuePaths = [];
+ const nodes = [{obj, path: []}];
while (nodes.length > 0) {
- let node = nodes.pop();
- Object.keys(node.obj).forEach((key) => {
- let path = node.path.concat(key);
- let value = node.obj[key];
- if (typeof value === 'object') {
- nodes.unshift({
- obj: value,
- path: path
- });
+ 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([value, path]);
+ valuePaths.push([obj, path]);
}
- });
+ }
}
return valuePaths;
}
function modifyOption(path, value, options) {
let pivot = options;
- for (let pathKey of path.slice(0, -1)) {
- if (!(pathKey in pivot)) {
+ for (const key of path.slice(0, -1)) {
+ if (!hasOwn(pivot, key)) {
return false;
}
- pivot = pivot[pathKey];
+ pivot = pivot[key];
}
pivot[path[path.length - 1]] = value;
return true;
}
- for (let [value, path] of getValuePaths(changedOptions)) {
+ for (const [value, path] of getValuePaths(changedOptions)) {
modifyOption(path, value, options);
}
@@ -78,33 +72,83 @@ async function apiOptionsSave(source) {
backend.onOptionsUpdated(source);
}
-async function apiTermsFind(text, optionsContext) {
+async function apiTermsFind(text, details, optionsContext) {
const options = await apiOptionsGet(optionsContext);
- const translator = utilBackend().translator;
+ const [definitions, length] = await utilBackend().translator.findTerms(text, details, options);
+ definitions.splice(options.general.maxResults);
+ return {length, definitions};
+}
- const searcher = {
- 'merge': translator.findTermsMerged,
- 'split': translator.findTermsSplit,
- 'group': translator.findTermsGrouped
- }[options.general.resultOutputMode].bind(translator);
+async function apiTextParse(text, optionsContext) {
+ const options = await apiOptionsGet(optionsContext);
+ const translator = utilBackend().translator;
- const {definitions, length} = await searcher(
- text,
- dictEnabledSet(options),
- options.scanning.alphanumeric,
- options
- );
+ 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;
+}
- return {
- length,
- definitions: definitions.slice(0, options.general.maxResults)
- };
+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});
+ }
+ } 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, dictEnabledSet(options));
- return definitions.slice(0, options.general.maxResults);
+ const definitions = await utilBackend().translator.findKanji(text, options);
+ definitions.splice(options.general.maxResults);
+ return definitions;
}
async function apiDefinitionAdd(definition, mode, context, optionsContext) {
@@ -163,7 +207,7 @@ async function apiDefinitionsAddable(definitions, modes, optionsContext) {
}
if (cannotAdd.length > 0) {
- const noteIdsArray = await anki.findNoteIds(cannotAdd.map(e => e[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) {
@@ -192,7 +236,7 @@ async function apiTemplateRender(template, data, dynamic) {
async function apiCommandExec(command, params) {
const handlers = apiCommandExec.handlers;
- if (handlers.hasOwnProperty(command)) {
+ if (hasOwn(handlers, command)) {
const handler = handlers[command];
handler(params);
}
@@ -360,7 +404,9 @@ async function apiGetBrowser() {
if (info.name === 'Fennec') {
return 'firefox-mobile';
}
- } catch (e) { }
+ } catch (e) {
+ // NOP
+ }
return 'firefox';
} else {
return 'chrome';
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index 3efcce46..dc0ba5eb 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -107,12 +107,12 @@ const audioUrlBuilders = {
'custom': async (definition, optionsContext) => {
const options = await apiOptionsGet(optionsContext);
const customSourceUrl = options.audio.customSourceUrl;
- return customSourceUrl.replace(/\{([^\}]*)\}/g, (m0, m1) => (definition.hasOwnProperty(m1) ? `${definition[m1]}` : m0));
+ return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));
}
};
async function audioGetUrl(definition, mode, optionsContext, download) {
- if (audioUrlBuilders.hasOwnProperty(mode)) {
+ if (hasOwn(audioUrlBuilders, mode)) {
const handler = audioUrlBuilders[mode];
try {
return await handler(definition, optionsContext, download);
@@ -128,12 +128,12 @@ function audioUrlNormalize(url, baseUrl, basePath) {
if (url[0] === '/') {
if (url.length >= 2 && url[1] === '/') {
// Begins with "//"
- url = baseUrl.substr(0, baseUrl.indexOf(':') + 1) + url;
+ url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url;
} else {
// Begins with "/"
url = baseUrl + url;
}
- } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) {
+ } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) {
// No URI scheme => relative path
url = baseUrl + basePath + url;
}
@@ -171,7 +171,7 @@ async function audioInject(definition, fields, sources, optionsContext) {
try {
let audioSourceDefinition = definition;
- if (definition.hasOwnProperty('expressions')) {
+ if (hasOwn(definition, 'expressions')) {
audioSourceDefinition = definition.expressions[0];
}
diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js
index 979afd16..db4d30b9 100644
--- a/ext/bg/js/backend-api-forwarder.js
+++ b/ext/bg/js/backend-api-forwarder.js
@@ -37,8 +37,8 @@ class BackendApiForwarder {
const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'});
- port.onMessage.addListener(message => forwardPort.postMessage(message));
- forwardPort.onMessage.addListener(message => port.postMessage(message));
+ port.onMessage.addListener((message) => forwardPort.postMessage(message));
+ forwardPort.onMessage.addListener((message) => port.postMessage(message));
port.onDisconnect.addListener(() => forwardPort.disconnect());
forwardPort.onDisconnect.addListener(() => port.disconnect());
}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 71393467..d9f9b586 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -21,6 +21,7 @@ class Backend {
constructor() {
this.translator = new Translator();
this.anki = new AnkiNull();
+ this.mecab = new Mecab();
this.options = null;
this.optionsContext = {
depth: 0,
@@ -59,7 +60,7 @@ class Backend {
this.applyOptions();
const callback = () => this.checkLastError(chrome.runtime.lastError);
- chrome.tabs.query({}, tabs => {
+ chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback);
}
@@ -72,12 +73,12 @@ class Backend {
onMessage({action, params}, sender, callback) {
const handlers = Backend.messageHandlers;
- if (handlers.hasOwnProperty(action)) {
+ if (hasOwn(handlers, action)) {
const handler = handlers[action];
const promise = handler(params, sender);
promise.then(
- result => callback({result}),
- error => callback({error: errorToJson(error)})
+ (result) => callback({result}),
+ (error) => callback({error: errorToJson(error)})
);
}
@@ -97,6 +98,12 @@ class Backend {
}
this.anki = options.anki.enable ? new AnkiConnect(options.anki.server) : new AnkiNull();
+
+ if (options.parsing.enableMecabParser) {
+ this.mecab.startListener();
+ } else {
+ this.mecab.stopListener();
+ }
}
async getFullOptions() {
@@ -170,7 +177,7 @@ class Backend {
}
}
- checkLastError(e) {
+ checkLastError() {
// NOP
}
}
@@ -179,7 +186,9 @@ Backend.messageHandlers = {
optionsGet: ({optionsContext}) => apiOptionsGet(optionsContext),
optionsSet: ({changedOptions, optionsContext, source}) => apiOptionsSet(changedOptions, optionsContext, source),
kanjiFind: ({text, optionsContext}) => apiKanjiFind(text, optionsContext),
- termsFind: ({text, optionsContext}) => apiTermsFind(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),
diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js
index 43c6dc08..cc9db087 100644
--- a/ext/bg/js/conditions-ui.js
+++ b/ext/bg/js/conditions-ui.js
@@ -84,7 +84,7 @@ ConditionsUI.Container = class Container {
createDefaultCondition(type) {
let operator = '';
let value = '';
- if (this.conditionDescriptors.hasOwnProperty(type)) {
+ if (hasOwn(this.conditionDescriptors, type)) {
const conditionDescriptor = this.conditionDescriptors[type];
operator = conditionDescriptor.defaultOperator;
({value} = this.getOperatorDefaultValue(type, operator));
@@ -96,15 +96,15 @@ ConditionsUI.Container = class Container {
}
getOperatorDefaultValue(type, operator) {
- if (this.conditionDescriptors.hasOwnProperty(type)) {
+ if (hasOwn(this.conditionDescriptors, type)) {
const conditionDescriptor = this.conditionDescriptors[type];
- if (conditionDescriptor.operators.hasOwnProperty(operator)) {
+ if (hasOwn(conditionDescriptor.operators, operator)) {
const operatorDescriptor = conditionDescriptor.operators[operator];
- if (operatorDescriptor.hasOwnProperty('defaultValue')) {
+ if (hasOwn(operatorDescriptor, 'defaultValue')) {
return {value: operatorDescriptor.defaultValue, fromOperator: true};
}
}
- if (conditionDescriptor.hasOwnProperty('defaultValue')) {
+ if (hasOwn(conditionDescriptor, 'defaultValue')) {
return {value: conditionDescriptor.defaultValue, fromOperator: false};
}
}
@@ -219,7 +219,7 @@ ConditionsUI.Condition = class Condition {
optionGroup.empty();
const type = this.condition.type;
- if (conditionDescriptors.hasOwnProperty(type)) {
+ if (hasOwn(conditionDescriptors, type)) {
const conditionDescriptor = conditionDescriptors[type];
const operators = conditionDescriptor.operators;
for (const operatorName of Object.keys(operators)) {
@@ -240,23 +240,23 @@ ConditionsUI.Condition = class Condition {
};
const objects = [];
- if (conditionDescriptors.hasOwnProperty(type)) {
+ if (hasOwn(conditionDescriptors, type)) {
const conditionDescriptor = conditionDescriptors[type];
objects.push(conditionDescriptor);
- if (conditionDescriptor.operators.hasOwnProperty(operator)) {
+ if (hasOwn(conditionDescriptor.operators, operator)) {
const operatorDescriptor = conditionDescriptor.operators[operator];
objects.push(operatorDescriptor);
}
}
for (const object of objects) {
- if (object.hasOwnProperty('placeholder')) {
+ if (hasOwn(object, 'placeholder')) {
props.placeholder = object.placeholder;
}
if (object.type === 'number') {
props.type = 'number';
for (const prop of ['step', 'min', 'max']) {
- if (object.hasOwnProperty(prop)) {
+ if (hasOwn(object, prop)) {
props[prop] = object[prop];
}
}
diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js
index ed4b14f5..c0f0f301 100644
--- a/ext/bg/js/conditions.js
+++ b/ext/bg/js/conditions.js
@@ -18,14 +18,14 @@
function conditionsValidateOptionValue(object, value) {
- if (object.hasOwnProperty('validate') && !object.validate(value)) {
+ if (hasOwn(object, 'validate') && !object.validate(value)) {
throw new Error('Invalid value for condition');
}
- if (object.hasOwnProperty('transform')) {
+ if (hasOwn(object, 'transform')) {
value = object.transform(value);
- if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) {
+ if (hasOwn(object, 'validateTransformed') && !object.validateTransformed(value)) {
throw new Error('Invalid value for condition');
}
}
@@ -34,12 +34,12 @@ function conditionsValidateOptionValue(object, value) {
}
function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) {
- if (!descriptors.hasOwnProperty(type)) {
+ if (!hasOwn(descriptors, type)) {
throw new Error('Invalid type');
}
const conditionDescriptor = descriptors[type];
- if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
+ if (!hasOwn(conditionDescriptor.operators, operator)) {
throw new Error('Invalid operator');
}
@@ -48,28 +48,28 @@ function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue
let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue);
transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue);
- if (operatorDescriptor.hasOwnProperty('transformReverse')) {
+ if (hasOwn(operatorDescriptor, 'transformReverse')) {
transformedValue = operatorDescriptor.transformReverse(transformedValue);
}
return transformedValue;
}
function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) {
- if (!descriptors.hasOwnProperty(type)) {
+ if (!hasOwn(descriptors, type)) {
throw new Error('Invalid type');
}
const conditionDescriptor = descriptors[type];
- if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
+ if (!hasOwn(conditionDescriptor.operators, operator)) {
throw new Error('Invalid operator');
}
const operatorDescriptor = conditionDescriptor.operators[operator];
- if (operatorDescriptor.hasOwnProperty('transform')) {
- if (operatorDescriptor.hasOwnProperty('transformCache')) {
+ if (hasOwn(operatorDescriptor, 'transform')) {
+ if (hasOwn(operatorDescriptor, 'transformCache')) {
const key = `${optionValue}`;
const transformCache = operatorDescriptor.transformCache;
- if (transformCache.hasOwnProperty(key)) {
+ if (hasOwn(transformCache, key)) {
optionValue = transformCache[key];
} else {
optionValue = operatorDescriptor.transform(optionValue);
@@ -93,23 +93,23 @@ function conditionsTestValue(descriptors, type, operator, optionValue, value) {
function conditionsClearCaches(descriptors) {
for (const type in descriptors) {
- if (!descriptors.hasOwnProperty(type)) {
+ if (!hasOwn(descriptors, type)) {
continue;
}
const conditionDescriptor = descriptors[type];
- if (conditionDescriptor.hasOwnProperty('transformCache')) {
+ if (hasOwn(conditionDescriptor, 'transformCache')) {
conditionDescriptor.transformCache = {};
}
const operatorDescriptors = conditionDescriptor.operators;
for (const operator in operatorDescriptors) {
- if (!operatorDescriptors.hasOwnProperty(operator)) {
+ if (!hasOwn(operatorDescriptors, operator)) {
continue;
}
const operatorDescriptor = operatorDescriptors[operator];
- if (operatorDescriptor.hasOwnProperty('transformCache')) {
+ if (hasOwn(operatorDescriptor, 'transformCache')) {
operatorDescriptor.transformCache = {};
}
}
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 3fb27f0d..0b21f662 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -26,26 +26,28 @@ function showExtensionInfo() {
}
function setupButtonEvents(selector, command, url) {
- const node = $(selector);
- node.on('click', (e) => {
- if (e.button !== 0) { return; }
- apiCommandExec(command, {newTab: e.ctrlKey});
- e.preventDefault();
- })
- .on('auxclick', (e) => {
- if (e.button !== 1) { return; }
- apiCommandExec(command, {newTab: true});
- e.preventDefault();
- });
+ const nodes = document.querySelectorAll(selector);
+ for (const node of nodes) {
+ node.addEventListener('click', (e) => {
+ if (e.button !== 0) { return; }
+ apiCommandExec(command, {newTab: e.ctrlKey});
+ e.preventDefault();
+ }, false);
+ node.addEventListener('auxclick', (e) => {
+ if (e.button !== 1) { return; }
+ apiCommandExec(command, {newTab: true});
+ e.preventDefault();
+ }, false);
- if (typeof url === 'string') {
- node.attr('href', url);
- node.attr('target', '_blank');
- node.attr('rel', 'noopener');
+ if (typeof url === 'string') {
+ node.href = url;
+ node.target = '_blank';
+ node.rel = 'noopener';
+ }
}
}
-$(document).ready(utilAsync(() => {
+window.addEventListener('DOMContentLoaded', () => {
showExtensionInfo();
apiGetEnvironmentInfo().then(({browser}) => {
@@ -63,14 +65,19 @@ $(document).ready(utilAsync(() => {
depth: 0,
url: window.location.href
};
- apiOptionsGet(optionsContext).then(options => {
- const toggle = $('#enable-search');
- toggle.prop('checked', options.general.enable).change();
- toggle.bootstrapToggle();
- toggle.change(() => apiCommandExec('toggle'));
+ apiOptionsGet(optionsContext).then((options) => {
+ const toggle = document.querySelector('#enable-search');
+ toggle.checked = options.general.enable;
+ toggle.addEventListener('change', () => apiCommandExec('toggle'), false);
+
+ const toggle2 = document.querySelector('#enable-search2');
+ toggle2.checked = options.general.enable;
+ toggle2.addEventListener('change', () => apiCommandExec('toggle'), false);
- const toggle2 = $('#enable-search2');
- toggle2.prop('checked', options.general.enable).change();
- toggle2.change(() => apiCommandExec('toggle'));
+ setTimeout(() => {
+ for (const n of document.querySelectorAll('.toggle-group')) {
+ n.classList.add('toggle-group-animated');
+ }
+ }, 10);
});
-}));
+});
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 9f477b24..a20d5f15 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -23,63 +23,137 @@ class Database {
}
async prepare() {
- if (this.db) {
+ if (this.db !== null) {
throw new Error('Database already initialized');
}
- this.db = new Dexie('dict');
- this.db.version(2).stores({
- terms: '++id,dictionary,expression,reading',
- kanji: '++,dictionary,character',
- tagMeta: '++,dictionary',
- dictionaries: '++,title,version'
- });
- this.db.version(3).stores({
- termMeta: '++,dictionary,expression',
- kanjiMeta: '++,dictionary,character',
- tagMeta: '++,dictionary,name'
- });
- this.db.version(4).stores({
- terms: '++id,dictionary,expression,reading,sequence'
- });
-
- await this.db.open();
+ try {
+ this.db = await Database.open('dict', 4, (db, transaction, oldVersion) => {
+ Database.upgrade(db, transaction, oldVersion, [
+ {
+ version: 2,
+ stores: {
+ terms: {
+ primaryKey: {keyPath: 'id', autoIncrement: true},
+ indices: ['dictionary', 'expression', 'reading']
+ },
+ kanji: {
+ primaryKey: {autoIncrement: true},
+ indices: ['dictionary', 'character']
+ },
+ tagMeta: {
+ primaryKey: {autoIncrement: true},
+ indices: ['dictionary']
+ },
+ dictionaries: {
+ primaryKey: {autoIncrement: true},
+ indices: ['title', 'version']
+ }
+ }
+ },
+ {
+ version: 3,
+ stores: {
+ termMeta: {
+ primaryKey: {autoIncrement: true},
+ indices: ['dictionary', 'expression']
+ },
+ kanjiMeta: {
+ primaryKey: {autoIncrement: true},
+ indices: ['dictionary', 'character']
+ },
+ tagMeta: {
+ primaryKey: {autoIncrement: true},
+ indices: ['dictionary', 'name']
+ }
+ }
+ },
+ {
+ version: 4,
+ stores: {
+ terms: {
+ primaryKey: {keyPath: 'id', autoIncrement: true},
+ indices: ['dictionary', 'expression', 'reading', 'sequence']
+ }
+ }
+ }
+ ]);
+ });
+ return true;
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
}
async purge() {
this.validate();
this.db.close();
- await this.db.delete();
+ await Database.deleteDatabase(this.db.name);
this.db = null;
await this.prepare();
}
- async findTermsBulk(termList, titles) {
+ async deleteDictionary(dictionaryName, onProgress, progressSettings) {
+ this.validate();
+
+ const targets = [
+ ['dictionaries', 'title'],
+ ['kanji', 'dictionary'],
+ ['kanjiMeta', 'dictionary'],
+ ['terms', 'dictionary'],
+ ['termMeta', 'dictionary'],
+ ['tagMeta', 'dictionary']
+ ];
+ const promises = [];
+ const progressData = {
+ count: 0,
+ processed: 0,
+ storeCount: targets.length,
+ storesProcesed: 0
+ };
+ let progressRate = (typeof progressSettings === 'object' && progressSettings !== null ? progressSettings.rate : 0);
+ if (typeof progressRate !== 'number' || progressRate <= 0) {
+ progressRate = 1000;
+ }
+
+ for (const [objectStoreName, index] of targets) {
+ const dbTransaction = this.db.transaction([objectStoreName], 'readwrite');
+ const dbObjectStore = dbTransaction.objectStore(objectStoreName);
+ const dbIndex = dbObjectStore.index(index);
+ const only = IDBKeyRange.only(dictionaryName);
+ promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate));
+ }
+
+ await Promise.all(promises);
+ }
+
+ async findTermsBulk(termList, titles, wildcard) {
this.validate();
const promises = [];
const visited = {};
const results = [];
const processRow = (row, index) => {
- if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) {
+ if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) {
visited[row.id] = true;
results.push(Database.createTerm(row, index));
}
};
- const db = this.db.backendDB();
- const dbTransaction = db.transaction(['terms'], 'readonly');
+ const dbTransaction = this.db.transaction(['terms'], 'readonly');
const dbTerms = dbTransaction.objectStore('terms');
const dbIndex1 = dbTerms.index('expression');
const dbIndex2 = dbTerms.index('reading');
for (let i = 0; i < termList.length; ++i) {
- const only = IDBKeyRange.only(termList[i]);
+ const term = termList[i];
+ const query = wildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
promises.push(
- Database.getAll(dbIndex1, only, i, processRow),
- Database.getAll(dbIndex2, only, i, processRow)
+ Database.getAll(dbIndex1, query, i, processRow),
+ Database.getAll(dbIndex2, query, i, processRow)
);
}
@@ -99,8 +173,7 @@ class Database {
}
};
- const db = this.db.backendDB();
- const dbTransaction = db.transaction(['terms'], 'readonly');
+ const dbTransaction = this.db.transaction(['terms'], 'readonly');
const dbTerms = dbTransaction.objectStore('terms');
const dbIndex = dbTerms.index('expression');
@@ -125,8 +198,7 @@ class Database {
}
};
- const db = this.db.backendDB();
- const dbTransaction = db.transaction(['terms'], 'readonly');
+ const dbTransaction = this.db.transaction(['terms'], 'readonly');
const dbTerms = dbTransaction.objectStore('terms');
const dbIndex = dbTerms.index('sequence');
@@ -163,8 +235,7 @@ class Database {
}
};
- const db = this.db.backendDB();
- const dbTransaction = db.transaction([tableName], 'readonly');
+ const dbTransaction = this.db.transaction([tableName], 'readonly');
const dbTerms = dbTransaction.objectStore(tableName);
const dbIndex = dbTerms.index(indexName);
@@ -182,12 +253,11 @@ class Database {
this.validate();
let result = null;
- const db = this.db.backendDB();
- const dbTransaction = db.transaction(['tagMeta'], 'readonly');
+ const dbTransaction = this.db.transaction(['tagMeta'], 'readonly');
const dbTerms = dbTransaction.objectStore('tagMeta');
const dbIndex = dbTerms.index('name');
const only = IDBKeyRange.only(name);
- await Database.getAll(dbIndex, only, null, row => {
+ await Database.getAll(dbIndex, only, null, (row) => {
if (title === row.dictionary) {
result = row;
}
@@ -196,24 +266,76 @@ class Database {
return result;
}
- async summarize() {
+ async getDictionaryInfo() {
+ this.validate();
+
+ const results = [];
+ const dbTransaction = this.db.transaction(['dictionaries'], 'readonly');
+ const dbDictionaries = dbTransaction.objectStore('dictionaries');
+
+ await Database.getAll(dbDictionaries, null, null, (info) => results.push(info));
+
+ return results;
+ }
+
+ async getDictionaryCounts(dictionaryNames, getTotal) {
this.validate();
- return this.db.dictionaries.toArray();
+ const objectStoreNames = [
+ 'kanji',
+ 'kanjiMeta',
+ 'terms',
+ 'termMeta',
+ 'tagMeta'
+ ];
+ const dbCountTransaction = this.db.transaction(objectStoreNames, 'readonly');
+
+ const targets = [];
+ for (const objectStoreName of objectStoreNames) {
+ targets.push([
+ objectStoreName,
+ dbCountTransaction.objectStore(objectStoreName).index('dictionary')
+ ]);
+ }
+
+ // Query is required for Edge, otherwise index.count throws an exception.
+ const query1 = IDBKeyRange.lowerBound('', false);
+ const totalPromise = getTotal ? Database.getCounts(targets, query1) : null;
+
+ const counts = [];
+ const countPromises = [];
+ for (let i = 0; i < dictionaryNames.length; ++i) {
+ counts.push(null);
+ const index = i;
+ const query2 = IDBKeyRange.only(dictionaryNames[i]);
+ const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v);
+ countPromises.push(countPromise);
+ }
+ await Promise.all(countPromises);
+
+ const result = {counts};
+ if (totalPromise !== null) {
+ result.total = await totalPromise;
+ }
+ return result;
}
async importDictionary(archive, progressCallback, exceptions) {
this.validate();
const maxTransactionLength = 1000;
- const bulkAdd = async (table, items, total, current) => {
- if (items.length < maxTransactionLength) {
+ const bulkAdd = async (objectStoreName, items, total, current) => {
+ const db = this.db;
+ for (let i = 0; i < items.length; i += maxTransactionLength) {
if (progressCallback) {
- progressCallback(total, current);
+ progressCallback(total, current + i / items.length);
}
try {
- await table.bulkAdd(items);
+ const count = Math.min(maxTransactionLength, items.length - i);
+ const transaction = db.transaction([objectStoreName], 'readwrite');
+ const objectStore = transaction.objectStore(objectStoreName);
+ await Database.bulkAdd(objectStore, items, i, count);
} catch (e) {
if (exceptions) {
exceptions.push(e);
@@ -221,37 +343,27 @@ class Database {
throw e;
}
}
- } else {
- for (let i = 0; i < items.length; i += maxTransactionLength) {
- if (progressCallback) {
- progressCallback(total, current + i / items.length);
- }
-
- let count = Math.min(maxTransactionLength, items.length - i);
- try {
- await table.bulkAdd(items.slice(i, i + count));
- } catch (e) {
- if (exceptions) {
- exceptions.push(e);
- } else {
- throw e;
- }
- }
- }
}
};
- const indexDataLoaded = async summary => {
+ const indexDataLoaded = async (summary) => {
if (summary.version > 3) {
throw new Error('Unsupported dictionary version');
}
- const count = await this.db.dictionaries.where('title').equals(summary.title).count();
+ const db = this.db;
+ const dbCountTransaction = db.transaction(['dictionaries'], 'readonly');
+ const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title');
+ const only = IDBKeyRange.only(summary.title);
+ const count = await Database.getCount(dbIndex, only);
+
if (count > 0) {
throw new Error('Dictionary is already imported');
}
- await this.db.dictionaries.add(summary);
+ const transaction = db.transaction(['dictionaries'], 'readwrite');
+ const objectStore = transaction.objectStore('dictionaries');
+ await Database.bulkAdd(objectStore, [summary], 0, 1);
};
const termDataLoaded = async (summary, entries, total, current) => {
@@ -284,7 +396,7 @@ class Database {
}
}
- await bulkAdd(this.db.terms, rows, total, current);
+ await bulkAdd('terms', rows, total, current);
};
const termMetaDataLoaded = async (summary, entries, total, current) => {
@@ -298,7 +410,7 @@ class Database {
});
}
- await bulkAdd(this.db.termMeta, rows, total, current);
+ await bulkAdd('termMeta', rows, total, current);
};
const kanjiDataLoaded = async (summary, entries, total, current) => {
@@ -328,7 +440,7 @@ class Database {
}
}
- await bulkAdd(this.db.kanji, rows, total, current);
+ await bulkAdd('kanji', rows, total, current);
};
const kanjiMetaDataLoaded = async (summary, entries, total, current) => {
@@ -342,7 +454,7 @@ class Database {
});
}
- await bulkAdd(this.db.kanjiMeta, rows, total, current);
+ await bulkAdd('kanjiMeta', rows, total, current);
};
const tagDataLoaded = async (summary, entries, total, current) => {
@@ -360,7 +472,7 @@ class Database {
rows.push(row);
}
- await bulkAdd(this.db.tagMeta, rows, total, current);
+ await bulkAdd('tagMeta', rows, total, current);
};
return await Database.importDictionaryZip(
@@ -410,13 +522,13 @@ class Database {
await indexDataLoaded(summary);
- const buildTermBankName = index => `term_bank_${index + 1}.json`;
- const buildTermMetaBankName = index => `term_meta_bank_${index + 1}.json`;
- const buildKanjiBankName = index => `kanji_bank_${index + 1}.json`;
- const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`;
- const buildTagBankName = index => `tag_bank_${index + 1}.json`;
+ const buildTermBankName = (index) => `term_bank_${index + 1}.json`;
+ const buildTermMetaBankName = (index) => `term_meta_bank_${index + 1}.json`;
+ const buildKanjiBankName = (index) => `kanji_bank_${index + 1}.json`;
+ const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`;
+ const buildTagBankName = (index) => `tag_bank_${index + 1}.json`;
- const countBanks = namer => {
+ const countBanks = (namer) => {
let count = 0;
while (zip.files[namer(count)]) {
++count;
@@ -539,4 +651,176 @@ class Database {
};
});
}
+
+ static getCounts(targets, query) {
+ const countPromises = [];
+ const counts = {};
+ for (const [objectStoreName, index] of targets) {
+ const n = objectStoreName;
+ const countPromise = Database.getCount(index, query).then((count) => counts[n] = count);
+ countPromises.push(countPromise);
+ }
+ return Promise.all(countPromises).then(() => counts);
+ }
+
+ static getCount(dbIndex, query) {
+ return new Promise((resolve, reject) => {
+ const request = dbIndex.count(query);
+ request.onerror = (e) => reject(e);
+ request.onsuccess = (e) => resolve(e.target.result);
+ });
+ }
+
+ static getAllKeys(dbIndex, query) {
+ const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor;
+ return fn(dbIndex, query);
+ }
+
+ static getAllKeysFast(dbIndex, query) {
+ return new Promise((resolve, reject) => {
+ const request = dbIndex.getAllKeys(query);
+ request.onerror = (e) => reject(e);
+ request.onsuccess = (e) => resolve(e.target.result);
+ });
+ }
+
+ static getAllKeysUsingCursor(dbIndex, query) {
+ return new Promise((resolve, reject) => {
+ const primaryKeys = [];
+ const request = dbIndex.openKeyCursor(query, 'next');
+ request.onerror = (e) => reject(e);
+ request.onsuccess = (e) => {
+ const cursor = e.target.result;
+ if (cursor) {
+ primaryKeys.push(cursor.primaryKey);
+ cursor.continue();
+ } else {
+ resolve(primaryKeys);
+ }
+ };
+ });
+ }
+
+ static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) {
+ const hasProgress = (typeof onProgress === 'function');
+ const count = await Database.getCount(dbIndex, query);
+ ++progressData.storesProcesed;
+ progressData.count += count;
+ if (hasProgress) {
+ onProgress(progressData);
+ }
+
+ const onValueDeleted = (
+ hasProgress ?
+ () => {
+ const p = ++progressData.processed;
+ if ((p % progressRate) === 0 || p === progressData.count) {
+ onProgress(progressData);
+ }
+ } :
+ () => {}
+ );
+
+ const promises = [];
+ const primaryKeys = await Database.getAllKeys(dbIndex, query);
+ for (const key of primaryKeys) {
+ const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted);
+ promises.push(promise);
+ }
+
+ await Promise.all(promises);
+ }
+
+ static deleteValue(dbObjectStore, key) {
+ return new Promise((resolve, reject) => {
+ const request = dbObjectStore.delete(key);
+ request.onerror = (e) => reject(e);
+ request.onsuccess = () => resolve();
+ });
+ }
+
+ static bulkAdd(objectStore, items, start, count) {
+ return new Promise((resolve, reject) => {
+ if (start + count > items.length) {
+ count = items.length - start;
+ }
+
+ if (count <= 0) {
+ resolve();
+ return;
+ }
+
+ const end = start + count;
+ let completedCount = 0;
+ const onError = (e) => reject(e);
+ const onSuccess = () => {
+ if (++completedCount >= count) {
+ resolve();
+ }
+ };
+
+ for (let i = start; i < end; ++i) {
+ const request = objectStore.add(items[i]);
+ request.onerror = onError;
+ request.onsuccess = onSuccess;
+ }
+ });
+ }
+
+ static open(name, version, onUpgradeNeeded) {
+ return new Promise((resolve, reject) => {
+ const request = window.indexedDB.open(name, version * 10);
+
+ request.onupgradeneeded = (event) => {
+ try {
+ request.transaction.onerror = (e) => reject(e);
+ onUpgradeNeeded(request.result, request.transaction, event.oldVersion / 10, event.newVersion / 10);
+ } catch (e) {
+ reject(e);
+ }
+ };
+
+ request.onerror = (e) => reject(e);
+ request.onsuccess = () => resolve(request.result);
+ });
+ }
+
+ static upgrade(db, transaction, oldVersion, upgrades) {
+ for (const {version, stores} of upgrades) {
+ if (oldVersion >= version) { continue; }
+
+ const objectStoreNames = Object.keys(stores);
+ for (const objectStoreName of objectStoreNames) {
+ const {primaryKey, indices} = stores[objectStoreName];
+
+ const objectStoreNames = transaction.objectStoreNames || db.objectStoreNames;
+ const objectStore = (
+ Database.listContains(objectStoreNames, objectStoreName) ?
+ transaction.objectStore(objectStoreName) :
+ db.createObjectStore(objectStoreName, primaryKey)
+ );
+
+ for (const indexName of indices) {
+ if (Database.listContains(objectStore.indexNames, indexName)) { continue; }
+
+ objectStore.createIndex(indexName, indexName, {});
+ }
+ }
+ }
+ }
+
+ static deleteDatabase(dbName) {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase(dbName);
+ request.onerror = (e) => reject(e);
+ request.onsuccess = () => resolve();
+ });
+ }
+
+ static listContains(list, value) {
+ for (let i = 0, ii = list.length; i < ii; ++i) {
+ if (list[i] === value) { return true; }
+ }
+ return false;
+ }
}
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index ce4b2961..51f4723c 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -44,7 +44,7 @@ class Deinflector {
results.push({
source,
- term: term.slice(0, -kanaIn.length) + kanaOut,
+ term: term.substring(0, term.length - kanaIn.length) + kanaOut,
rules: rulesOut,
definitions: [],
reasons: [reason, ...reasons]
@@ -88,5 +88,5 @@ Deinflector.ruleTypes = {
'vs': 0b0000100, // Verb suru
'vk': 0b0001000, // Verb kuru
'adj-i': 0b0010000, // Adjective i
- 'iru': 0b0100000, // Intermediate -iru endings for progressive or perfect tense
+ 'iru': 0b0100000 // Intermediate -iru endings for progressive or perfect tense
};
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 191058c1..0b35e32e 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -55,39 +55,23 @@ function dictRowsSort(rows, options) {
function dictTermsSort(definitions, dictionaries=null) {
return definitions.sort((v1, v2) => {
+ let i;
if (dictionaries !== null) {
- const p1 = (dictionaries[v1.dictionary] || {}).priority || 0;
- const p2 = (dictionaries[v2.dictionary] || {}).priority || 0;
- if (p1 > p2) {
- return -1;
- } else if (p1 < p2) {
- return 1;
- }
+ i = (
+ ((dictionaries[v2.dictionary] || {}).priority || 0) -
+ ((dictionaries[v1.dictionary] || {}).priority || 0)
+ );
+ if (i !== 0) { return i; }
}
- const sl1 = v1.source.length;
- const sl2 = v2.source.length;
- if (sl1 > sl2) {
- return -1;
- } else if (sl1 < sl2) {
- return 1;
- }
+ i = v2.source.length - v1.source.length;
+ if (i !== 0) { return i; }
- const rl1 = v1.reasons.length;
- const rl2 = v2.reasons.length;
- if (rl1 < rl2) {
- return -1;
- } else if (rl1 > rl2) {
- return 1;
- }
+ i = v2.reasons.length - v1.reasons.length;
+ if (i !== 0) { return i; }
- const s1 = v1.score;
- const s2 = v2.score;
- if (s1 > s2) {
- return -1;
- } else if (s1 < s2) {
- return 1;
- }
+ i = v2.score - v1.score;
+ if (i !== 0) { return i; }
return v2.expression.toString().localeCompare(v1.expression.toString());
});
@@ -97,7 +81,7 @@ function dictTermsUndupe(definitions) {
const definitionGroups = {};
for (const definition of definitions) {
const definitionExisting = definitionGroups[definition.id];
- if (!definitionGroups.hasOwnProperty(definition.id) || definition.expression.length > definitionExisting.expression.length) {
+ if (!hasOwn(definitionGroups, definition.id) || definition.expression.length > definitionExisting.expression.length) {
definitionGroups[definition.id] = definition;
}
}
@@ -115,8 +99,8 @@ function dictTermsCompressTags(definitions) {
let lastPartOfSpeech = '';
for (const definition of definitions) {
- const dictionary = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'dictionary').map(tag => tag.name).sort());
- const partOfSpeech = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'partOfSpeech').map(tag => tag.name).sort());
+ const dictionary = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'dictionary').map((tag) => tag.name).sort());
+ const partOfSpeech = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'partOfSpeech').map((tag) => tag.name).sort());
const filterOutCategories = [];
@@ -133,7 +117,7 @@ function dictTermsCompressTags(definitions) {
lastPartOfSpeech = partOfSpeech;
}
- definition.definitionTags = definition.definitionTags.filter(tag => !filterOutCategories.includes(tag.category));
+ definition.definitionTags = definition.definitionTags.filter((tag) => !filterOutCategories.includes(tag.category));
}
}
@@ -147,7 +131,7 @@ function dictTermsGroup(definitions, dictionaries) {
}
const keyString = key.toString();
- if (groups.hasOwnProperty(keyString)) {
+ if (hasOwn(groups, keyString)) {
groups[keyString].push(definition);
} else {
groups[keyString] = [definition];
@@ -247,7 +231,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) {
result.reading.add(definition.reading);
for (const tag of definition.definitionTags) {
- if (!definitionsByGloss[gloss].definitionTags.find(existingTag => existingTag.name === tag.name)) {
+ if (!definitionsByGloss[gloss].definitionTags.find((existingTag) => existingTag.name === tag.name)) {
definitionsByGloss[gloss].definitionTags.push(tag);
}
}
@@ -262,7 +246,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) {
}
for (const tag of definition.termTags) {
- if (!result.expressions.get(definition.expression).get(definition.reading).find(existingTag => existingTag.name === tag.name)) {
+ if (!result.expressions.get(definition.expression).get(definition.reading).find((existingTag) => existingTag.name === tag.name)) {
result.expressions.get(definition.expression).get(definition.reading).push(tag);
}
}
@@ -326,46 +310,52 @@ function dictFieldSplit(field) {
return field.length === 0 ? [] : field.split(' ');
}
-async function dictFieldFormat(field, definition, mode, options) {
- const markers = [
- 'audio',
- 'character',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'expression',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'kunyomi',
- 'onyomi',
- 'reading',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ];
-
- for (const marker of markers) {
- const data = {
- marker,
- definition,
- group: options.general.resultOutputMode === 'group',
- merge: options.general.resultOutputMode === 'merge',
- modeTermKanji: mode === 'term-kanji',
- modeTermKana: mode === 'term-kana',
- modeKanji: mode === 'kanji',
- compactGlossaries: options.general.compactGlossaries
- };
-
- const html = await apiTemplateRender(options.anki.fieldTemplates, data, true);
- field = field.replace(`{${marker}}`, html);
- }
-
- return field;
+async function dictFieldFormat(field, definition, mode, options, exceptions) {
+ const data = {
+ marker: null,
+ definition,
+ group: options.general.resultOutputMode === 'group',
+ merge: options.general.resultOutputMode === 'merge',
+ modeTermKanji: mode === 'term-kanji',
+ modeTermKana: mode === 'term-kana',
+ modeKanji: mode === 'kanji',
+ compactGlossaries: options.general.compactGlossaries
+ };
+ const markers = dictFieldFormat.markers;
+ const pattern = /\{([\w-]+)\}/g;
+ return await stringReplaceAsync(field, pattern, async (g0, marker) => {
+ if (!markers.has(marker)) {
+ return g0;
+ }
+ data.marker = marker;
+ try {
+ return await apiTemplateRender(options.anki.fieldTemplates, data, true);
+ } catch (e) {
+ if (exceptions) { exceptions.push(e); }
+ return `{${marker}-render-error}`;
+ }
+ });
}
+dictFieldFormat.markers = new Set([
+ 'audio',
+ 'character',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'expression',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'kunyomi',
+ 'onyomi',
+ 'reading',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+]);
async function dictNoteFormat(definition, mode, options) {
const note = {fields: {}, tags: options.anki.tags};
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index fba437da..8f43cf9a 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -79,6 +79,47 @@ function handlebarsSanitizeCssClass(options) {
return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_');
}
+function handlebarsRegexReplace(...args) {
+ // Usage:
+ // {{#regexReplace regex string [flags]}}content{{/regexReplace}}
+ // regex: regular expression string
+ // string: string to replace
+ // flags: optional flags for regular expression
+ // e.g. "i" for case-insensitive, "g" for replace all
+ let value = args[args.length - 1].fn(this);
+ if (args.length >= 3) {
+ try {
+ const flags = args.length > 3 ? args[2] : 'g';
+ const regex = new RegExp(args[0], flags);
+ value = value.replace(regex, args[1]);
+ } catch (e) {
+ return `${e}`;
+ }
+ }
+ return value;
+}
+
+function handlebarsRegexMatch(...args) {
+ // Usage:
+ // {{#regexMatch regex [flags]}}content{{/regexMatch}}
+ // regex: regular expression string
+ // flags: optional flags for regular expression
+ // e.g. "i" for case-insensitive, "g" for match all
+ let value = args[args.length - 1].fn(this);
+ if (args.length >= 2) {
+ try {
+ const flags = args.length > 2 ? args[1] : '';
+ const regex = new RegExp(args[0], flags);
+ const parts = [];
+ value.replace(regex, (g0) => parts.push(g0));
+ value = parts.join('');
+ } catch (e) {
+ return `${e}`;
+ }
+ }
+ return value;
+}
+
function handlebarsRegisterHelpers() {
if (Handlebars.partials !== Handlebars.templates) {
Handlebars.partials = Handlebars.templates;
@@ -88,6 +129,8 @@ function handlebarsRegisterHelpers() {
Handlebars.registerHelper('kanjiLinks', handlebarsKanjiLinks);
Handlebars.registerHelper('multiLine', handlebarsMultiLine);
Handlebars.registerHelper('sanitizeCssClass', handlebarsSanitizeCssClass);
+ Handlebars.registerHelper('regexReplace', handlebarsRegexReplace);
+ Handlebars.registerHelper('regexMatch', handlebarsRegexMatch);
}
}
diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js
new file mode 100644
index 00000000..62111f73
--- /dev/null
+++ b/ext/bg/js/mecab.js
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class Mecab {
+ constructor() {
+ this.port = null;
+ this.listeners = {};
+ this.sequence = 0;
+ }
+
+ onError(error) {
+ logError(error, false);
+ }
+
+ async checkVersion() {
+ try {
+ const {version} = await this.invoke('get_version', {});
+ if (version !== Mecab.version) {
+ this.stopListener();
+ throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${Mecab.version}.`);
+ }
+ } catch (error) {
+ this.onError(error);
+ }
+ }
+
+ async parseText(text) {
+ return await this.invoke('parse_text', {text});
+ }
+
+ startListener() {
+ if (this.port !== null) { return; }
+ this.port = chrome.runtime.connectNative('yomichan_mecab');
+ this.port.onMessage.addListener(this.onNativeMessage.bind(this));
+ this.checkVersion();
+ }
+
+ stopListener() {
+ if (this.port === null) { return; }
+ this.port.disconnect();
+ this.port = null;
+ this.listeners = {};
+ this.sequence = 0;
+ }
+
+ onNativeMessage({sequence, data}) {
+ if (hasOwn(this.listeners, sequence)) {
+ const {callback, timer} = this.listeners[sequence];
+ clearTimeout(timer);
+ callback(data);
+ delete this.listeners[sequence];
+ }
+ }
+
+ invoke(action, params) {
+ if (this.port === null) {
+ return Promise.resolve({});
+ }
+ return new Promise((resolve, reject) => {
+ const sequence = this.sequence++;
+
+ this.listeners[sequence] = {
+ callback: resolve,
+ timer: setTimeout(() => {
+ delete this.listeners[sequence];
+ reject(new Error(`Mecab invoke timed out in ${Mecab.timeout} ms`));
+ }, Mecab.timeout)
+ };
+
+ this.port.postMessage({action, params, sequence});
+ });
+ }
+}
+
+Mecab.timeout = 5000;
+Mecab.version = 1;
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index be1ccfbb..e53a8a13 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -311,6 +311,13 @@ function profileOptionsCreateDefaults() {
dictionaries: {},
+ parsing: {
+ enableScanningParser: true,
+ enableMecabParser: false,
+ selectedParser: null,
+ readingMode: 'hiragana'
+ },
+
anki: {
enable: false,
server: 'http://127.0.0.1:8765',
@@ -329,7 +336,7 @@ function profileOptionsSetDefaults(options) {
const combine = (target, source) => {
for (const key in source) {
- if (!target.hasOwnProperty(key)) {
+ if (!hasOwn(target, key)) {
target[key] = source[key];
}
}
@@ -382,7 +389,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
// Remove invalid
const profiles = options.profiles;
for (let i = profiles.length - 1; i >= 0; --i) {
- if (!utilIsObject(profiles[i])) {
+ if (!isObject(profiles[i])) {
profiles.splice(i, 1);
}
}
@@ -422,7 +429,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
function optionsLoad() {
return new Promise((resolve, reject) => {
- chrome.storage.local.get(['options'], store => {
+ chrome.storage.local.get(['options'], (store) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error));
@@ -430,17 +437,17 @@ function optionsLoad() {
resolve(store.options);
}
});
- }).then(optionsStr => {
+ }).then((optionsStr) => {
if (typeof optionsStr === 'string') {
const options = JSON.parse(optionsStr);
- if (utilIsObject(options)) {
+ if (isObject(options)) {
return options;
}
}
return {};
}).catch(() => {
return {};
- }).then(options => {
+ }).then((options) => {
return (
Array.isArray(options.profiles) ?
optionsUpdateVersion(options, {}) :
diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js
new file mode 100644
index 00000000..aee4e3c2
--- /dev/null
+++ b/ext/bg/js/page-exit-prevention.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class PageExitPrevention {
+ constructor() {
+ }
+
+ start() {
+ PageExitPrevention._addInstance(this);
+ }
+
+ end() {
+ PageExitPrevention._removeInstance(this);
+ }
+
+ static _addInstance(instance) {
+ const size = PageExitPrevention._instances.size;
+ PageExitPrevention._instances.set(instance, true);
+ if (size === 0) {
+ window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
+ }
+ }
+
+ static _removeInstance(instance) {
+ if (
+ PageExitPrevention._instances.delete(instance) &&
+ PageExitPrevention._instances.size === 0
+ ) {
+ window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload);
+ }
+ }
+
+ static _onBeforeUnload(e) {
+ if (PageExitPrevention._instances.size === 0) {
+ return;
+ }
+
+ e.preventDefault();
+ e.returnValue = '';
+ return '';
+ }
+}
+
+PageExitPrevention._instances = new Map();
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index 5daa904e..ebc6680a 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -17,6 +17,26 @@
*/
+function _profileConditionTestDomain(urlDomain, domain) {
+ return (
+ urlDomain.endsWith(domain) &&
+ (
+ domain.length === urlDomain.length ||
+ urlDomain[urlDomain.length - domain.length - 1] === '.'
+ )
+ );
+}
+
+function _profileConditionTestDomainList(url, domainList) {
+ const urlDomain = new URL(url).hostname.toLowerCase();
+ for (const domain of domainList) {
+ if (_profileConditionTestDomain(urlDomain, domain)) {
+ return true;
+ }
+ }
+ return false;
+}
+
const profileConditionsDescriptor = {
popupLevel: {
name: 'Popup Level',
@@ -66,10 +86,10 @@ const profileConditionsDescriptor = {
placeholder: 'Comma separated list of domains',
defaultValue: 'example.com',
transformCache: {},
- transform: (optionValue) => optionValue.split(/[,;\s]+/).map(v => v.trim().toLowerCase()).filter(v => v.length > 0),
+ transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0),
transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),
validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),
- test: ({url}, transformedOptionValue) => (transformedOptionValue.indexOf(new URL(url).hostname.toLowerCase()) >= 0)
+ test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue)
},
matchRegExp: {
name: 'Matches RegExp',
diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js
index 3afc1506..7d73d49b 100644
--- a/ext/bg/js/request.js
+++ b/ext/bg/js/request.js
@@ -29,7 +29,7 @@ function requestJson(url, action, params) {
} else {
xhr.send();
}
- }).then(responseText => {
+ }).then((responseText) => {
try {
return JSON.parse(responseText);
}
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index b21dac17..6ba8467e 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -25,7 +25,12 @@ async function searchFrontendSetup() {
const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage) { return; }
- window.frontendInitializationData = {depth: 1, proxy: false};
+ const ignoreNodes = ['.scan-disable', '.scan-disable *'];
+ if (!options.scanning.enableOnPopupExpressions) {
+ ignoreNodes.push('.expression-scan-toggle', '.expression-scan-toggle *');
+ }
+
+ window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false};
const scriptSrcs = [
'/fg/js/frontend-api-receiver.js',
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
new file mode 100644
index 00000000..8dc2e30a
--- /dev/null
+++ b/ext/bg/js/search-query-parser.js
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class QueryParser {
+ constructor(search) {
+ this.search = search;
+ this.pendingLookup = false;
+ this.clickScanPrevent = false;
+
+ this.parseResults = [];
+ this.selectedParser = null;
+
+ this.queryParser = document.querySelector('#query-parser');
+ this.queryParserSelect = document.querySelector('#query-parser-select');
+
+ this.queryParser.addEventListener('mousedown', (e) => this.onMouseDown(e));
+ this.queryParser.addEventListener('mouseup', (e) => this.onMouseUp(e));
+ }
+
+ onError(error) {
+ logError(error, false);
+ }
+
+ onMouseDown(e) {
+ if (DOM.isMouseButtonPressed(e, 'primary')) {
+ this.clickScanPrevent = false;
+ }
+ }
+
+ onMouseUp(e) {
+ if (
+ this.search.options.scanning.enablePopupSearch &&
+ !this.clickScanPrevent &&
+ DOM.isMouseButtonPressed(e, 'primary')
+ ) {
+ const selectText = this.search.options.scanning.selectText;
+ this.onTermLookup(e, {disableScroll: true, selectText});
+ }
+ }
+
+ onMouseMove(e) {
+ if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) {
+ return;
+ }
+
+ const scanningOptions = this.search.options.scanning;
+ const scanningModifier = scanningOptions.modifier;
+ if (!(
+ Frontend.isScanningModifierPressed(scanningModifier, e) ||
+ (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))
+ )) {
+ return;
+ }
+
+ const selectText = this.search.options.scanning.selectText;
+ this.onTermLookup(e, {disableScroll: true, disableHistory: true, selectText});
+ }
+
+ onMouseLeave(e) {
+ this.clickScanPrevent = true;
+ clearTimeout(e.target.dataset.timer);
+ delete e.target.dataset.timer;
+ }
+
+ onTermLookup(e, params) {
+ this.pendingLookup = true;
+ (async () => {
+ await this.search.onTermLookup(e, params);
+ this.pendingLookup = false;
+ })();
+ }
+
+ onParserChange(e) {
+ const selectedParser = e.target.value;
+ this.selectedParser = selectedParser;
+ apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext());
+ this.renderParseResult(this.getParseResult());
+ }
+
+ refreshSelectedParser() {
+ if (this.parseResults.length > 0) {
+ if (this.selectedParser === null) {
+ this.selectedParser = this.search.options.parsing.selectedParser;
+ }
+ if (this.selectedParser === null || !this.getParseResult()) {
+ const selectedParser = this.parseResults[0].id;
+ this.selectedParser = selectedParser;
+ apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext());
+ }
+ }
+ }
+
+ getParseResult() {
+ return this.parseResults.find((r) => r.id === this.selectedParser);
+ }
+
+ async setText(text) {
+ this.search.setSpinnerVisible(true);
+
+ await this.setPreview(text);
+
+ this.parseResults = await this.parseText(text);
+ this.refreshSelectedParser();
+
+ this.renderParserSelect();
+ await this.renderParseResult();
+
+ this.search.setSpinnerVisible(false);
+ }
+
+ async parseText(text) {
+ const results = [];
+ if (this.search.options.parsing.enableScanningParser) {
+ results.push({
+ name: 'Scanning parser',
+ id: 'scan',
+ parsedText: await apiTextParse(text, this.search.getOptionsContext())
+ });
+ }
+ if (this.search.options.parsing.enableMecabParser) {
+ const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext());
+ for (const mecabDictName in mecabResults) {
+ results.push({
+ name: `MeCab: ${mecabDictName}`,
+ id: `mecab-${mecabDictName}`,
+ parsedText: mecabResults[mecabDictName]
+ });
+ }
+ }
+ return results;
+ }
+
+ 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);
+ }
+ this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', {
+ terms: previewTerms,
+ preview: true
+ });
+
+ for (const charElement of this.queryParser.querySelectorAll('.query-parser-char')) {
+ this.activateScanning(charElement);
+ }
+ }
+
+ renderParserSelect() {
+ this.queryParserSelect.innerHTML = '';
+ if (this.parseResults.length > 1) {
+ const select = document.createElement('select');
+ select.classList.add('form-control');
+ for (const parseResult of this.parseResults) {
+ const option = document.createElement('option');
+ option.value = parseResult.id;
+ option.innerText = parseResult.name;
+ option.defaultSelected = this.selectedParser === parseResult.id;
+ select.appendChild(option);
+ }
+ select.addEventListener('change', this.onParserChange.bind(this));
+ this.queryParserSelect.appendChild(select);
+ }
+ }
+
+ async renderParseResult() {
+ const parseResult = this.getParseResult();
+ if (!parseResult) {
+ this.queryParser.innerHTML = '';
+ return;
+ }
+
+ this.queryParser.innerHTML = await apiTemplateRender(
+ 'query-parser.html',
+ {terms: QueryParser.processParseResultForDisplay(parseResult.parsedText)}
+ );
+
+ for (const charElement of this.queryParser.querySelectorAll('.query-parser-char')) {
+ this.activateScanning(charElement);
+ }
+ }
+
+ activateScanning(element) {
+ element.addEventListener('mousemove', (e) => {
+ clearTimeout(e.target.dataset.timer);
+ if (this.search.options.scanning.modifier === 'none') {
+ e.target.dataset.timer = setTimeout(() => {
+ this.onMouseMove(e);
+ delete e.target.dataset.timer;
+ }, this.search.options.scanning.delay);
+ } else {
+ this.onMouseMove(e);
+ }
+ });
+ element.addEventListener('mouseleave', (e) => {
+ this.onMouseLeave(e);
+ });
+ }
+
+ static processParseResultForDisplay(result) {
+ return result.map((term) => {
+ return term.filter((part) => part.text.trim()).map((part) => {
+ return {
+ text: Array.from(part.text),
+ reading: part.reading,
+ raw: !part.reading || !part.reading.trim()
+ };
+ });
+ });
+ }
+}
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index dbfcb15d..00b7ca4b 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -32,6 +32,8 @@ class DisplaySearch extends Display {
url: window.location.href
};
+ this.queryParser = new QueryParser(this);
+
this.search = document.querySelector('#search');
this.query = document.querySelector('#query');
this.intro = document.querySelector('#intro');
@@ -72,11 +74,11 @@ class DisplaySearch extends Display {
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
if (e.target.checked) {
window.wanakana.bind(this.query);
- this.query.value = window.wanakana.toKana(query);
+ this.setQuery(window.wanakana.toKana(query));
apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());
} else {
window.wanakana.unbind(this.query);
- this.query.value = query;
+ this.setQuery(query);
apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());
}
this.onSearchQueryUpdated(this.query.value, false);
@@ -86,9 +88,9 @@ class DisplaySearch extends Display {
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
if (query !== null) {
if (this.isWanakanaEnabled()) {
- this.query.value = window.wanakana.toKana(query);
+ this.setQuery(window.wanakana.toKana(query));
} else {
- this.query.value = query;
+ this.setQuery(query);
}
this.onSearchQueryUpdated(this.query.value, false);
}
@@ -159,18 +161,19 @@ class DisplaySearch extends Display {
e.preventDefault();
const query = this.query.value;
+ this.queryParser.setText(query);
const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : '';
window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
this.onSearchQueryUpdated(query, true);
}
- onPopState(e) {
+ onPopState() {
const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || '';
if (this.query !== null) {
if (this.isWanakanaEnabled()) {
- this.query.value = window.wanakana.toKana(query);
+ this.setQuery(window.wanakana.toKana(query));
} else {
- this.query.value = query;
+ this.setQuery(query);
}
}
@@ -179,27 +182,14 @@ class DisplaySearch extends Display {
onKeyDown(e) {
const key = Display.getKeyFromEvent(e);
+ const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys;
- let activeModifierMap = {
+ const activeModifierMap = {
'Control': e.ctrlKey,
'Meta': e.metaKey,
'ANY_MOD': true
};
- const ignoreKeys = {
- 'ANY_MOD': ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End']
- .concat(
- Array.from(Array(24).keys())
- .map(i => `F${i + 1}`)
- ),
- 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
- 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
- 'OS': [],
- 'Alt': [],
- 'AltGraph': [],
- 'Shift': []
- }
-
let preventFocus = false;
for (const [modifier, keys] of Object.entries(ignoreKeys)) {
const modifierActive = activeModifierMap[modifier];
@@ -216,19 +206,28 @@ class DisplaySearch extends Display {
async onSearchQueryUpdated(query, animate) {
try {
+ const details = {};
+ const match = /[*\uff0a]+$/.exec(query);
+ if (match !== null) {
+ details.wildcard = true;
+ query = query.substring(0, query.length - match[0].length);
+ }
+
const valid = (query.length > 0);
this.setIntroVisible(!valid, animate);
this.updateSearchButton();
if (valid) {
- const {definitions} = await apiTermsFind(query, this.optionsContext);
+ const {definitions} = await apiTermsFind(query, details, this.optionsContext);
this.setContentTerms(definitions, {
focus: false,
- sentence: null,
+ disableHistory: true,
+ sentence: {text: query, offset: 0},
url: window.location.href
});
} else {
this.container.textContent = '';
}
+ window.parent.postMessage('popupClose', '*');
} catch (e) {
this.onError(e);
}
@@ -236,7 +235,7 @@ class DisplaySearch extends Display {
onRuntimeMessage({action, params}, sender, callback) {
const handlers = DisplaySearch.runtimeMessageHandlers;
- if (handlers.hasOwnProperty(action)) {
+ if (hasOwn(handlers, action)) {
const handler = handlers[action];
const result = handler(this, params);
callback(result);
@@ -247,7 +246,7 @@ class DisplaySearch extends Display {
initClipboardMonitor() {
// ignore copy from search page
- window.addEventListener('copy', (e) => {
+ window.addEventListener('copy', () => {
this.clipboardPrevText = document.getSelection().toString().trim();
});
}
@@ -261,11 +260,11 @@ class DisplaySearch extends Display {
} else if (IS_FIREFOX === false) {
curText = (await apiClipboardGet()).trim();
}
- if (curText && (curText !== this.clipboardPrevText)) {
+ if (curText && (curText !== this.clipboardPrevText) && jpIsJapaneseText(curText)) {
if (this.isWanakanaEnabled()) {
- this.query.value = window.wanakana.toKana(curText);
+ this.setQuery(window.wanakana.toKana(curText));
} else {
- this.query.value = curText;
+ this.setQuery(curText);
}
const queryString = curText.length > 0 ? `?query=${encodeURIComponent(curText)}` : '';
@@ -292,8 +291,9 @@ class DisplaySearch extends Display {
return this.optionsContext;
}
- setCustomCss() {
- // No custom CSS
+ setQuery(query) {
+ this.query.value = query;
+ this.queryParser.setText(query);
}
setIntroVisible(visible, animate) {
@@ -325,7 +325,7 @@ class DisplaySearch extends Display {
this.intro.style.transition = '';
this.intro.style.height = '';
const size = this.intro.getBoundingClientRect();
- this.intro.style.height = `0px`;
+ this.intro.style.height = '0px';
this.intro.style.transition = `height ${duration}s ease-in-out 0s`;
window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation
this.intro.style.height = `${size.height}px`;
@@ -357,7 +357,7 @@ class DisplaySearch extends Display {
}
static getSearchQueryFromLocation(url) {
- let match = /^[^\?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
+ const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
return match !== null ? decodeURIComponent(match[1]) : null;
}
}
@@ -368,4 +368,19 @@ DisplaySearch.runtimeMessageHandlers = {
}
};
+DisplaySearch.onKeyDownIgnoreKeys = {
+ 'ANY_MOD': [
+ 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10',
+ 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
+ 'F21', 'F22', 'F23', 'F24'
+ ],
+ 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
+ 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'],
+ 'OS': [],
+ 'Alt': [],
+ 'AltGraph': [],
+ 'Shift': []
+};
+
window.yomichan_search = DisplaySearch.create();
diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js
deleted file mode 100644
index 05a0604a..00000000
--- a/ext/bg/js/settings.js
+++ /dev/null
@@ -1,954 +0,0 @@
-/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
- * Author: Alex Yatskov <alex@foosoft.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-async function getOptionsArray() {
- const optionsFull = await apiOptionsGetFull();
- return optionsFull.profiles.map(profile => profile.options);
-}
-
-async function formRead(options) {
- options.general.enable = $('#enable').prop('checked');
- options.general.showGuide = $('#show-usage-guide').prop('checked');
- options.general.compactTags = $('#compact-tags').prop('checked');
- options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
- options.general.resultOutputMode = $('#result-output-mode').val();
- options.general.debugInfo = $('#show-debug-info').prop('checked');
- options.general.showAdvanced = $('#show-advanced-options').prop('checked');
- options.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
- options.general.popupDisplayMode = $('#popup-display-mode').val();
- options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val();
- options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();
- options.general.popupWidth = parseInt($('#popup-width').val(), 10);
- options.general.popupHeight = parseInt($('#popup-height').val(), 10);
- options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
- options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
- options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
- options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
- options.general.popupTheme = $('#popup-theme').val();
- options.general.popupOuterTheme = $('#popup-outer-theme').val();
- options.general.customPopupCss = $('#custom-popup-css').val();
- options.general.customPopupOuterCss = $('#custom-popup-outer-css').val();
-
- options.audio.enabled = $('#audio-playback-enabled').prop('checked');
- options.audio.autoPlay = $('#auto-play-audio').prop('checked');
- options.audio.volume = parseFloat($('#audio-playback-volume').val());
- options.audio.customSourceUrl = $('#audio-custom-source').val();
- options.audio.textToSpeechVoice = $('#text-to-speech-voice').val();
-
- options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
- options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
- options.scanning.selectText = $('#select-matched-text').prop('checked');
- options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
- options.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
- options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
- options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked');
- options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
- options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
- options.scanning.delay = parseInt($('#scan-delay').val(), 10);
- options.scanning.length = parseInt($('#scan-length').val(), 10);
- options.scanning.modifier = $('#scan-modifier-key').val();
- options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10);
-
- const optionsAnkiEnableOld = options.anki.enable;
- options.anki.enable = $('#anki-enable').prop('checked');
- options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/));
- options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
- options.anki.server = $('#interface-server').val();
- options.anki.screenshot.format = $('#screenshot-format').val();
- options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
- options.anki.fieldTemplates = $('#field-templates').val();
-
- if (optionsAnkiEnableOld && !ankiErrorShown()) {
- options.anki.terms.deck = $('#anki-terms-deck').val();
- options.anki.terms.model = $('#anki-terms-model').val();
- options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict($('#terms .anki-field-value')));
- options.anki.kanji.deck = $('#anki-kanji-deck').val();
- options.anki.kanji.model = $('#anki-kanji-model').val();
- options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict($('#kanji .anki-field-value')));
- }
-
- options.general.mainDictionary = $('#dict-main').val();
- $('.dict-group').each((index, element) => {
- const dictionary = $(element);
- options.dictionaries[dictionary.data('title')] = utilBackgroundIsolate({
- priority: parseInt(dictionary.find('.dict-priority').val(), 10),
- enabled: dictionary.find('.dict-enabled').prop('checked'),
- allowSecondarySearches: dictionary.find('.dict-allow-secondary-searches').prop('checked')
- });
- });
-}
-
-async function formWrite(options) {
- $('#enable').prop('checked', options.general.enable);
- $('#show-usage-guide').prop('checked', options.general.showGuide);
- $('#compact-tags').prop('checked', options.general.compactTags);
- $('#compact-glossaries').prop('checked', options.general.compactGlossaries);
- $('#result-output-mode').val(options.general.resultOutputMode);
- $('#show-debug-info').prop('checked', options.general.debugInfo);
- $('#show-advanced-options').prop('checked', options.general.showAdvanced);
- $('#max-displayed-results').val(options.general.maxResults);
- $('#popup-display-mode').val(options.general.popupDisplayMode);
- $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition);
- $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);
- $('#popup-width').val(options.general.popupWidth);
- $('#popup-height').val(options.general.popupHeight);
- $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
- $('#popup-vertical-offset').val(options.general.popupVerticalOffset);
- $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
- $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
- $('#popup-theme').val(options.general.popupTheme);
- $('#popup-outer-theme').val(options.general.popupOuterTheme);
- $('#custom-popup-css').val(options.general.customPopupCss);
- $('#custom-popup-outer-css').val(options.general.customPopupOuterCss);
-
- $('#audio-playback-enabled').prop('checked', options.audio.enabled);
- $('#auto-play-audio').prop('checked', options.audio.autoPlay);
- $('#audio-playback-volume').val(options.audio.volume);
- $('#audio-custom-source').val(options.audio.customSourceUrl);
- $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice);
-
- $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
- $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
- $('#select-matched-text').prop('checked', options.scanning.selectText);
- $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
- $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
- $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
- $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch);
- $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
- $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
- $('#scan-delay').val(options.scanning.delay);
- $('#scan-length').val(options.scanning.length);
- $('#scan-modifier-key').val(options.scanning.modifier);
- $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth);
-
- $('#anki-enable').prop('checked', options.anki.enable);
- $('#card-tags').val(options.anki.tags.join(' '));
- $('#sentence-detection-extent').val(options.anki.sentenceExt);
- $('#interface-server').val(options.anki.server);
- $('#screenshot-format').val(options.anki.screenshot.format);
- $('#screenshot-quality').val(options.anki.screenshot.quality);
- $('#field-templates').val(options.anki.fieldTemplates);
-
- try {
- await dictionaryGroupsPopulate(options);
- await formMainDictionaryOptionsPopulate(options);
- } catch (e) {
- dictionaryErrorsShow([e]);
- }
-
- try {
- await ankiDeckAndModelPopulate(options);
- } catch (e) {
- ankiErrorShow(e);
- }
-
- formUpdateVisibility(options);
-}
-
-function formSetupEventListeners() {
- $('#dict-purge-link').click(utilAsync(onDictionaryPurge));
- $('#dict-file').change(utilAsync(onDictionaryImport));
- $('#dict-file-button').click(onDictionaryImportButtonClick);
-
- $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
- $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged));
- $('.anki-model').change(utilAsync(onAnkiModelChanged));
-}
-
-function formUpdateVisibility(options) {
- const general = $('#anki-general');
- if (options.anki.enable) {
- general.show();
- } else {
- general.hide();
- }
-
- const advanced = $('.options-advanced');
- if (options.general.showAdvanced) {
- advanced.show();
- } else {
- advanced.hide();
- }
-
- const mainGroup = $('#dict-main-group');
- if (options.general.resultOutputMode === 'merge') {
- mainGroup.show();
- } else {
- mainGroup.hide();
- }
-
- const debug = $('#debug');
- if (options.general.debugInfo) {
- const temp = utilIsolate(options);
- temp.anki.fieldTemplates = '...';
- const text = JSON.stringify(temp, null, 4);
- debug.html(handlebarsEscape(text));
- debug.show();
- } else {
- debug.hide();
- }
-}
-
-async function formMainDictionaryOptionsPopulate(options) {
- const select = $('#dict-main').empty();
- select.append($('<option class="text-muted" value="">Not selected</option>'));
-
- let mainDictionary = '';
- for (const dictRow of toIterable(await utilDatabaseSummarize())) {
- if (dictRow.sequenced) {
- select.append($(`<option value="${dictRow.title}">${dictRow.title}</option>`));
- if (dictRow.title === options.general.mainDictionary) {
- mainDictionary = dictRow.title;
- }
- }
- }
-
- select.val(mainDictionary);
-}
-
-async function onFormOptionsChanged(e) {
- if (!e.originalEvent && !e.isTrigger) {
- return;
- }
-
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- const optionsAnkiEnableOld = options.anki.enable;
- const optionsAnkiServerOld = options.anki.server;
-
- await formRead(options);
- await settingsSaveOptions();
- formUpdateVisibility(options);
-
- try {
- const ankiUpdated =
- options.anki.enable !== optionsAnkiEnableOld ||
- options.anki.server !== optionsAnkiServerOld;
-
- if (ankiUpdated) {
- ankiSpinnerShow(true);
- await ankiDeckAndModelPopulate(options);
- ankiErrorShow();
- }
- } catch (e) {
- ankiErrorShow(e);
- } finally {
- ankiSpinnerShow(false);
- }
-}
-
-async function onReady() {
- showExtensionInformation();
-
- formSetupEventListeners();
- appearanceInitialize();
- await audioSettingsInitialize();
- await profileOptionsSetup();
-
- storageInfoInitialize();
-
- chrome.runtime.onMessage.addListener(onMessage);
-}
-
-$(document).ready(utilAsync(onReady));
-
-
-/*
- * Appearance
- */
-
-function appearanceInitialize() {
- let previewVisible = false;
- $('#settings-popup-preview-button').on('click', () => {
- if (previewVisible) { return; }
- showAppearancePreview();
- previewVisible = true;
- });
-}
-
-function showAppearancePreview() {
- const container = $('#settings-popup-preview-container');
- const buttonContainer = $('#settings-popup-preview-button-container');
- const settings = $('#settings-popup-preview-settings');
- const text = $('#settings-popup-preview-text');
- const customCss = $('#custom-popup-css');
- const customOuterCss = $('#custom-popup-outer-css');
-
- const frame = document.createElement('iframe');
- frame.src = '/bg/settings-popup-preview.html';
- frame.id = 'settings-popup-preview-frame';
-
- window.wanakana.bind(text[0]);
-
- text.on('input', () => {
- const action = 'setText';
- const params = {text: text.val()};
- frame.contentWindow.postMessage({action, params}, '*');
- });
- customCss.on('input', () => {
- const action = 'setCustomCss';
- const params = {css: customCss.val()};
- frame.contentWindow.postMessage({action, params}, '*');
- });
- customOuterCss.on('input', () => {
- const action = 'setCustomOuterCss';
- const params = {css: customOuterCss.val()};
- frame.contentWindow.postMessage({action, params}, '*');
- });
-
- container.append(frame);
- buttonContainer.remove();
- settings.css('display', '');
-}
-
-
-/*
- * Audio
- */
-
-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'));
- audioSourceUI.save = () => apiOptionsSave();
-
- textToSpeechInitialize();
-}
-
-function textToSpeechInitialize() {
- if (typeof speechSynthesis === 'undefined') { return; }
-
- speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
- updateTextToSpeechVoices();
-
- $('#text-to-speech-voice-test').on('click', () => textToSpeechTest());
-}
-
-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'));
- for (const {voice} of voices) {
- select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`));
- }
-
- select.val(select.attr('data-value'));
-}
-
-function languageTagIsJapanese(languageTag) {
- return (
- languageTag.startsWith('ja-') ||
- languageTag.startsWith('jpn-')
- );
-}
-
-function textToSpeechVoiceCompare(a, b) {
- const aIsJapanese = languageTagIsJapanese(a.voice.lang);
- const bIsJapanese = languageTagIsJapanese(b.voice.lang);
- if (aIsJapanese) {
- if (!bIsJapanese) { return -1; }
- } else {
- if (bIsJapanese) { return 1; }
- }
-
- const aIsDefault = a.voice.default;
- const bIsDefault = b.voice.default;
- if (aIsDefault) {
- if (!bIsDefault) { return -1; }
- } else {
- if (bIsDefault) { return 1; }
- }
-
- if (a.index < b.index) { return -1; }
- if (a.index > b.index) { return 1; }
- return 0;
-}
-
-function textToSpeechTest() {
- try {
- const text = $('#text-to-speech-voice-test').attr('data-speech-text') || '';
- const voiceURI = $('#text-to-speech-voice').val();
- const voice = audioGetTextToSpeechVoice(voiceURI);
- if (voice === null) { return; }
-
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.lang = 'ja-JP';
- utterance.voice = voice;
- utterance.volume = 1.0;
-
- speechSynthesis.speak(utterance);
- } catch (e) {
- // NOP
- }
-}
-
-
-/*
- * Remote options updates
- */
-
-function settingsGetSource() {
- return new Promise((resolve) => {
- chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`));
- });
-}
-
-async function settingsSaveOptions() {
- const source = await settingsGetSource();
- await apiOptionsSave(source);
-}
-
-async function onOptionsUpdate({source}) {
- const thisSource = await settingsGetSource();
- if (source === thisSource) { return; }
-
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(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;
- }
-}
-
-
-/*
- * Dictionary
- */
-
-function dictionaryErrorToString(error) {
- if (error.toString) {
- error = error.toString();
- } else {
- error = `${error}`;
- }
-
- for (const [match, subst] of dictionaryErrorToString.overrides) {
- if (error.includes(match)) {
- error = subst;
- break;
- }
- }
-
- return error;
-}
-dictionaryErrorToString.overrides = [
- [
- 'A mutation operation was attempted on a database that did not allow mutations.',
- 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
- ],
- [
- 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
- 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
- ],
- [
- 'BulkError',
- 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
- ]
-];
-
-function dictionaryErrorsShow(errors) {
- const dialog = $('#dict-error');
- dialog.show().text('');
-
- if (errors !== null && errors.length > 0) {
- const uniqueErrors = {};
- for (let e of errors) {
- e = dictionaryErrorToString(e);
- uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1;
- }
-
- for (const e in uniqueErrors) {
- const count = uniqueErrors[e];
- const div = document.createElement('p');
- if (count > 1) {
- div.textContent = `${e} `;
- const em = document.createElement('em');
- em.textContent = `(${count})`;
- div.appendChild(em);
- } else {
- div.textContent = `${e}`;
- }
- dialog.append($(div));
- }
-
- dialog.show();
- } else {
- dialog.hide();
- }
-}
-
-function dictionarySpinnerShow(show) {
- const spinner = $('#dict-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
- }
-}
-
-function dictionaryGroupsSort() {
- const dictGroups = $('#dict-groups');
- const dictGroupChildren = dictGroups.children('.dict-group').sort((ca, cb) => {
- const pa = parseInt($(ca).find('.dict-priority').val(), 10);
- const pb = parseInt($(cb).find('.dict-priority').val(), 10);
- if (pa < pb) {
- return 1;
- } else if (pa > pb) {
- return -1;
- } else {
- return 0;
- }
- });
-
- dictGroups.append(dictGroupChildren);
-}
-
-async function dictionaryGroupsPopulate(options) {
- const dictGroups = $('#dict-groups').empty();
- const dictWarning = $('#dict-warning').hide();
-
- const dictRows = toIterable(await utilDatabaseSummarize());
- if (dictRows.length === 0) {
- dictWarning.show();
- }
-
- for (const dictRow of toIterable(dictRowsSort(dictRows, options))) {
- const dictOptions = options.dictionaries[dictRow.title] || {
- enabled: false,
- priority: 0,
- allowSecondarySearches: false
- };
-
- const dictHtml = await apiTemplateRender('dictionary.html', {
- enabled: dictOptions.enabled,
- priority: dictOptions.priority,
- allowSecondarySearches: dictOptions.allowSecondarySearches,
- title: dictRow.title,
- version: dictRow.version,
- revision: dictRow.revision,
- outdated: dictRow.version < 3
- });
-
- dictGroups.append($(dictHtml));
- }
-
- formUpdateVisibility(options);
-
- $('.dict-enabled, .dict-priority, .dict-allow-secondary-searches').change(e => {
- dictionaryGroupsSort();
- onFormOptionsChanged(e);
- });
-}
-
-async function onDictionaryPurge(e) {
- e.preventDefault();
-
- const dictControls = $('#dict-importer, #dict-groups, #dict-main-group').hide();
- const dictProgress = $('#dict-purge').show();
-
- try {
- dictionaryErrorsShow(null);
- dictionarySpinnerShow(true);
-
- await utilDatabasePurge();
- for (const options of toIterable(await getOptionsArray())) {
- options.dictionaries = utilBackgroundIsolate({});
- options.general.mainDictionary = '';
- }
- await settingsSaveOptions();
-
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- await dictionaryGroupsPopulate(options);
- await formMainDictionaryOptionsPopulate(options);
- } catch (e) {
- dictionaryErrorsShow([e]);
- } finally {
- dictionarySpinnerShow(false);
-
- dictControls.show();
- dictProgress.hide();
-
- if (storageEstimate.mostRecent !== null) {
- storageUpdateStats();
- }
- }
-}
-
-function onDictionaryImportButtonClick() {
- const dictFile = document.querySelector('#dict-file');
- dictFile.click();
-}
-
-async function onDictionaryImport(e) {
- const dictFile = $('#dict-file');
- const dictControls = $('#dict-importer').hide();
- const dictProgress = $('#dict-import-progress').show();
-
- try {
- dictionaryErrorsShow(null);
- dictionarySpinnerShow(true);
-
- const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`);
- const updateProgress = (total, current) => {
- setProgress(current / total * 100.0);
- if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
- storageUpdateStats();
- }
- };
- setProgress(0.0);
-
- const exceptions = [];
- const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions);
- for (const options of toIterable(await getOptionsArray())) {
- options.dictionaries[summary.title] = utilBackgroundIsolate({
- enabled: true,
- priority: 0,
- allowSecondarySearches: false
- });
- if (summary.sequenced && options.general.mainDictionary === '') {
- options.general.mainDictionary = summary.title;
- }
- }
- await settingsSaveOptions();
-
- if (exceptions.length > 0) {
- exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
- dictionaryErrorsShow(exceptions);
- }
-
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- await dictionaryGroupsPopulate(options);
- await formMainDictionaryOptionsPopulate(options);
- } catch (e) {
- dictionaryErrorsShow([e]);
- } finally {
- dictionarySpinnerShow(false);
-
- dictFile.val('');
- dictControls.show();
- dictProgress.hide();
- }
-}
-
-
-/*
- * Anki
- */
-
-function ankiSpinnerShow(show) {
- const spinner = $('#anki-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
- }
-}
-
-function ankiErrorShow(error) {
- const dialog = $('#anki-error');
- if (error) {
- dialog.show().text(error);
- }
- else {
- dialog.hide();
- }
-}
-
-function ankiErrorShown() {
- return $('#anki-error').is(':visible');
-}
-
-function ankiFieldsToDict(selection) {
- const result = {};
- selection.each((index, element) => {
- result[$(element).data('field')] = $(element).val();
- });
-
- return result;
-}
-
-async function ankiDeckAndModelPopulate(options) {
- const ankiFormat = $('#anki-format').hide();
-
- const deckNames = await utilAnkiGetDeckNames();
- const ankiDeck = $('.anki-deck');
- ankiDeck.find('option').remove();
- deckNames.sort().forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
-
- const modelNames = await utilAnkiGetModelNames();
- const ankiModel = $('.anki-model');
- ankiModel.find('option').remove();
- modelNames.sort().forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
-
- $('#anki-terms-deck').val(options.anki.terms.deck);
- await ankiFieldsPopulate($('#anki-terms-model').val(options.anki.terms.model), options);
-
- $('#anki-kanji-deck').val(options.anki.kanji.deck);
- await ankiFieldsPopulate($('#anki-kanji-model').val(options.anki.kanji.model), options);
-
- ankiFormat.show();
-}
-
-async function ankiFieldsPopulate(element, options) {
- const modelName = element.val();
- if (!modelName) {
- return;
- }
-
- const tab = element.closest('.tab-pane');
- const tabId = tab.attr('id');
- const container = tab.find('tbody').empty();
-
- const markers = {
- 'terms': [
- 'audio',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'expression',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'reading',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ],
- 'kanji': [
- 'character',
- 'dictionary',
- 'glossary',
- 'kunyomi',
- 'onyomi',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ]
- }[tabId] || {};
-
- for (const name of await utilAnkiGetModelFieldNames(modelName)) {
- const value = options.anki[tabId].fields[name] || '';
- const html = Handlebars.templates['model.html']({name, markers, value});
- container.append($(html));
- }
-
- tab.find('.anki-field-value').change(utilAsync(onFormOptionsChanged));
- tab.find('.marker-link').click(onAnkiMarkerClicked);
-}
-
-function onAnkiMarkerClicked(e) {
- e.preventDefault();
- const link = e.target;
- $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change');
-}
-
-async function onAnkiModelChanged(e) {
- try {
- if (!e.originalEvent) {
- return;
- }
-
- const element = $(this);
- const tab = element.closest('.tab-pane');
- const tabId = tab.attr('id');
-
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- await formRead(options);
- options.anki[tabId].fields = utilBackgroundIsolate({});
- await settingsSaveOptions();
-
- ankiSpinnerShow(true);
- await ankiFieldsPopulate(element, options);
- ankiErrorShow();
- } catch (e) {
- ankiErrorShow(e);
- } finally {
- ankiSpinnerShow(false);
- }
-}
-
-async function onAnkiFieldTemplatesReset(e) {
- try {
- e.preventDefault();
- const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- const fieldTemplates = profileOptionsGetDefaultFieldTemplates();
- options.anki.fieldTemplates = fieldTemplates;
- $('#field-templates').val(fieldTemplates);
- await settingsSaveOptions();
- } catch (e) {
- ankiErrorShow(e);
- }
-}
-
-
-/*
- * Storage
- */
-
-function storageBytesToLabeledString(size) {
- const base = 1000;
- const labels = [' bytes', 'KB', 'MB', 'GB'];
- let labelIndex = 0;
- while (size >= base) {
- size /= base;
- ++labelIndex;
- }
- const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
- return `${label}${labels[labelIndex]}`;
-}
-
-async function storageEstimate() {
- try {
- return (storageEstimate.mostRecent = await navigator.storage.estimate());
- } catch (e) { }
- return null;
-}
-storageEstimate.mostRecent = null;
-
-async function isStoragePeristent() {
- try {
- return await navigator.storage.persisted();
- } catch (e) { }
- return false;
-}
-
-async function storageInfoInitialize() {
- storagePersistInitialize();
- const {browser, platform} = await apiGetEnvironmentInfo();
- document.documentElement.dataset.browser = browser;
- document.documentElement.dataset.operatingSystem = platform.os;
-
- await storageShowInfo();
-
- document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false);
-}
-
-async function storageUpdateStats() {
- storageUpdateStats.isUpdating = true;
-
- const estimate = await storageEstimate();
- const valid = (estimate !== null);
-
- if (valid) {
- // Firefox reports usage as 0 when persistent storage is enabled.
- const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
- if (finite) {
- document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
- document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
- }
- document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
- document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
- }
-
- storageUpdateStats.isUpdating = false;
- return valid;
-}
-storageUpdateStats.isUpdating = false;
-
-async function storageShowInfo() {
- storageSpinnerShow(true);
-
- const valid = await storageUpdateStats();
- document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
- document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
-
- storageSpinnerShow(false);
-}
-
-function storageSpinnerShow(show) {
- const spinner = $('#storage-spinner');
- if (show) {
- spinner.show();
- } else {
- spinner.hide();
- }
-}
-
-async function storagePersistInitialize() {
- if (!(navigator.storage && navigator.storage.persist)) {
- // Not supported
- return;
- }
-
- const info = document.querySelector('#storage-persist-info');
- const button = document.querySelector('#storage-persist-button');
- const checkbox = document.querySelector('#storage-persist-button-checkbox');
-
- info.classList.remove('storage-hidden');
- button.classList.remove('storage-hidden');
-
- let persisted = await isStoragePeristent();
- checkbox.checked = persisted;
-
- button.addEventListener('click', async () => {
- if (persisted) {
- return;
- }
- let result = false;
- try {
- result = await navigator.storage.persist();
- } catch (e) {
- // NOP
- }
-
- if (result) {
- persisted = true;
- checkbox.checked = true;
- storageShowInfo();
- } else {
- $('.storage-persist-fail-warning').removeClass('storage-hidden');
- }
- }, false);
-}
-
-
-/*
- * Information
- */
-
-function showExtensionInformation() {
- const node = document.getElementById('extension-info');
- if (node === null) { return; }
-
- const manifest = chrome.runtime.getManifest();
- node.textContent = `${manifest.name} v${manifest.version}`;
-}
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
new file mode 100644
index 00000000..9cdfc134
--- /dev/null
+++ b/ext/bg/js/settings/anki-templates.js
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+function onAnkiFieldTemplatesReset(e) {
+ e.preventDefault();
+ $('#field-template-reset-modal').modal('show');
+}
+
+function onAnkiFieldTemplatesResetConfirm(e) {
+ e.preventDefault();
+
+ $('#field-template-reset-modal').modal('hide');
+
+ const element = document.querySelector('#field-templates');
+ element.value = profileOptionsGetDefaultFieldTemplates();
+ element.dispatchEvent(new Event('change'));
+}
+
+function ankiTemplatesInitialize() {
+ const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji')));
+ const fragment = ankiGetFieldMarkersHtml(markers);
+
+ const list = document.querySelector('#field-templates-list');
+ list.appendChild(fragment);
+ for (const node of list.querySelectorAll('.marker-link')) {
+ node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
+ }
+
+ $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(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));
+}
+
+const ankiTemplatesValidateGetDefinition = (() => {
+ let cachedValue = null;
+ let cachedText = null;
+
+ return async (text, optionsContext) => {
+ if (cachedText !== text) {
+ const {definitions} = await apiTermsFind(text, {}, optionsContext);
+ if (definitions.length === 0) { return null; }
+
+ cachedValue = definitions[0];
+ cachedText = text;
+ }
+ return cachedValue;
+ };
+})();
+
+async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) {
+ const text = document.querySelector('#field-templates-preview-text').value || '';
+ const exceptions = [];
+ let result = `No definition found for ${text}`;
+ try {
+ const optionsContext = getOptionsContext();
+ const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
+ if (definition !== null) {
+ const options = await apiOptionsGet(optionsContext);
+ result = await dictFieldFormat(field, definition, mode, options, exceptions);
+ }
+ } catch (e) {
+ exceptions.push(e);
+ }
+
+ const hasException = exceptions.length > 0;
+ infoNode.hidden = !(showSuccessResult || hasException);
+ infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
+ infoNode.classList.toggle('text-danger', hasException);
+ if (invalidateInput) {
+ const input = document.querySelector('#field-templates');
+ input.classList.toggle('is-invalid', hasException);
+ }
+}
+
+function onAnkiTemplatesValidateCompile() {
+ const infoNode = document.querySelector('#field-template-compile-result');
+ ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
+}
+
+function onAnkiTemplateMarkerClicked(e) {
+ e.preventDefault();
+ document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
+}
+
+function onAnkiTemplateRender(e) {
+ e.preventDefault();
+
+ const field = document.querySelector('#field-template-render-text').value;
+ const infoNode = document.querySelector('#field-template-render-result');
+ infoNode.hidden = true;
+ ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false);
+}
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
new file mode 100644
index 00000000..e1aabbaf
--- /dev/null
+++ b/ext/bg/js/settings/anki.js
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+// Private
+
+let _ankiDataPopulated = false;
+
+
+function _ankiSpinnerShow(show) {
+ const spinner = $('#anki-spinner');
+ if (show) {
+ spinner.show();
+ } else {
+ spinner.hide();
+ }
+}
+
+function _ankiSetError(error) {
+ const node = document.querySelector('#anki-error');
+ if (!node) { return; }
+ if (error) {
+ node.hidden = false;
+ node.textContent = `${error}`;
+ }
+ else {
+ node.hidden = true;
+ node.textContent = '';
+ }
+}
+
+function _ankiSetDropdownOptions(dropdown, optionValues) {
+ const fragment = document.createDocumentFragment();
+ for (const optionValue of optionValues) {
+ const option = document.createElement('option');
+ option.value = optionValue;
+ option.textContent = optionValue;
+ fragment.appendChild(option);
+ }
+ dropdown.textContent = '';
+ dropdown.appendChild(fragment);
+}
+
+async function _ankiDeckAndModelPopulate(options) {
+ const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
+ const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
+ const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
+ const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
+ try {
+ _ankiSpinnerShow(true);
+ const [deckNames, modelNames] = await Promise.all([utilAnkiGetDeckNames(), utilAnkiGetModelNames()]);
+ deckNames.sort();
+ modelNames.sort();
+ termsDeck.values = deckNames;
+ kanjiDeck.values = deckNames;
+ termsModel.values = modelNames;
+ kanjiModel.values = modelNames;
+ _ankiSetError(null);
+ } catch (error) {
+ _ankiSetError(error);
+ } finally {
+ _ankiSpinnerShow(false);
+ }
+
+ for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
+ const node = document.querySelector(selector);
+ _ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]);
+ node.value = value;
+ }
+}
+
+function _ankiCreateFieldTemplate(name, value, markers) {
+ const template = document.querySelector('#anki-field-template').content;
+ const content = document.importNode(template, true).firstChild;
+
+ content.querySelector('.anki-field-name').textContent = name;
+
+ const field = content.querySelector('.anki-field-value');
+ field.dataset.field = name;
+ field.value = value;
+
+ content.querySelector('.anki-field-marker-list').appendChild(ankiGetFieldMarkersHtml(markers));
+
+ return content;
+}
+
+async function _ankiFieldsPopulate(tabId, options) {
+ const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
+ const container = tab.querySelector('tbody');
+ const markers = ankiGetFieldMarkers(tabId);
+
+ const fragment = document.createDocumentFragment();
+ const fields = options.anki[tabId].fields;
+ for (const name of Object.keys(fields)) {
+ const value = fields[name];
+ const html = _ankiCreateFieldTemplate(name, value, markers);
+ fragment.appendChild(html);
+ }
+
+ container.textContent = '';
+ container.appendChild(fragment);
+
+ for (const node of container.querySelectorAll('.anki-field-value')) {
+ node.addEventListener('change', (e) => onFormOptionsChanged(e), false);
+ }
+ for (const node of container.querySelectorAll('.marker-link')) {
+ node.addEventListener('click', (e) => _onAnkiMarkerClicked(e), false);
+ }
+}
+
+function _onAnkiMarkerClicked(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ const input = $(link).closest('.input-group').find('.anki-field-value')[0];
+ input.value = `{${link.textContent}}`;
+ input.dispatchEvent(new Event('change'));
+}
+
+async function _onAnkiModelChanged(e) {
+ const node = e.currentTarget;
+ let fieldNames;
+ try {
+ const modelName = node.value;
+ fieldNames = await utilAnkiGetModelFieldNames(modelName);
+ _ankiSetError(null);
+ } catch (error) {
+ _ankiSetError(error);
+ return;
+ } finally {
+ _ankiSpinnerShow(false);
+ }
+
+ const tabId = node.dataset.ankiCardType;
+ if (tabId !== 'terms' && tabId !== 'kanji') { return; }
+
+ const fields = {};
+ for (const name of fieldNames) {
+ fields[name] = '';
+ }
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ options.anki[tabId].fields = utilBackgroundIsolate(fields);
+ await settingsSaveOptions();
+
+ await _ankiFieldsPopulate(tabId, options);
+}
+
+
+// Public
+
+function ankiErrorShown() {
+ const node = document.querySelector('#anki-error');
+ return node && !node.hidden;
+}
+
+function ankiFieldsToDict(elements) {
+ const result = {};
+ for (const element of elements) {
+ result[element.dataset.field] = element.value;
+ }
+ return result;
+}
+
+
+function ankiGetFieldMarkersHtml(markers) {
+ const template = document.querySelector('#anki-field-marker-template').content;
+ const fragment = document.createDocumentFragment();
+ for (const marker of markers) {
+ const markerNode = document.importNode(template, true).firstChild;
+ markerNode.querySelector('.marker-link').textContent = marker;
+ fragment.appendChild(markerNode);
+ }
+ return fragment;
+}
+
+function ankiGetFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'expression',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'reading',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'dictionary',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+}
+
+
+function ankiInitialize() {
+ for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
+ node.addEventListener('change', (e) => _onAnkiModelChanged(e), false);
+ }
+}
+
+async function onAnkiOptionsChanged(options) {
+ if (!options.anki.enable) {
+ _ankiDataPopulated = false;
+ return;
+ }
+
+ if (_ankiDataPopulated) { return; }
+
+ await _ankiDeckAndModelPopulate(options);
+ _ankiDataPopulated = true;
+ await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]);
+}
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
new file mode 100644
index 00000000..f63551ed
--- /dev/null
+++ b/ext/bg/js/settings/audio.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+let 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'));
+ audioSourceUI.save = () => settingsSaveOptions();
+
+ textToSpeechInitialize();
+}
+
+function textToSpeechInitialize() {
+ if (typeof speechSynthesis === 'undefined') { return; }
+
+ speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
+ updateTextToSpeechVoices();
+
+ $('#text-to-speech-voice-test').on('click', () => textToSpeechTest());
+}
+
+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'));
+ for (const {voice} of voices) {
+ select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`));
+ }
+
+ select.val(select.attr('data-value'));
+}
+
+function languageTagIsJapanese(languageTag) {
+ return (
+ languageTag.startsWith('ja-') ||
+ languageTag.startsWith('jpn-')
+ );
+}
+
+function textToSpeechVoiceCompare(a, b) {
+ const aIsJapanese = languageTagIsJapanese(a.voice.lang);
+ const bIsJapanese = languageTagIsJapanese(b.voice.lang);
+ if (aIsJapanese) {
+ if (!bIsJapanese) { return -1; }
+ } else {
+ if (bIsJapanese) { return 1; }
+ }
+
+ const aIsDefault = a.voice.default;
+ const bIsDefault = b.voice.default;
+ if (aIsDefault) {
+ if (!bIsDefault) { return -1; }
+ } else {
+ if (bIsDefault) { return 1; }
+ }
+
+ if (a.index < b.index) { return -1; }
+ if (a.index > b.index) { return 1; }
+ return 0;
+}
+
+function textToSpeechTest() {
+ try {
+ const text = $('#text-to-speech-voice-test').attr('data-speech-text') || '';
+ const voiceURI = $('#text-to-speech-voice').val();
+ const voice = audioGetTextToSpeechVoice(voiceURI);
+ if (voice === null) { return; }
+
+ const utterance = new SpeechSynthesisUtterance(text);
+ utterance.lang = 'ja-JP';
+ utterance.voice = voice;
+ utterance.volume = 1.0;
+
+ speechSynthesis.speak(utterance);
+ } catch (e) {
+ // NOP
+ }
+}
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
new file mode 100644
index 00000000..065a8abc
--- /dev/null
+++ b/ext/bg/js/settings/dictionaries.js
@@ -0,0 +1,618 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+let dictionaryUI = null;
+
+
+class SettingsDictionaryListUI {
+ constructor(container, template, extraContainer, extraTemplate) {
+ this.container = container;
+ this.template = template;
+ this.extraContainer = extraContainer;
+ this.extraTemplate = extraTemplate;
+ this.optionsDictionaries = null;
+ this.dictionaries = null;
+ this.dictionaryEntries = [];
+ this.extra = null;
+
+ document.querySelector('#dict-delete-confirm').addEventListener('click', (e) => this.onDictionaryConfirmDelete(e), false);
+ }
+
+ setOptionsDictionaries(optionsDictionaries) {
+ this.optionsDictionaries = optionsDictionaries;
+ if (this.dictionaries !== null) {
+ this.setDictionaries(this.dictionaries);
+ }
+ }
+
+ setDictionaries(dictionaries) {
+ for (const dictionaryEntry of this.dictionaryEntries) {
+ dictionaryEntry.cleanup();
+ }
+
+ this.dictionaryEntries = [];
+ this.dictionaries = toIterable(dictionaries);
+
+ if (this.optionsDictionaries === null) {
+ return;
+ }
+
+ let changed = false;
+ for (const dictionaryInfo of this.dictionaries) {
+ if (this.createEntry(dictionaryInfo)) {
+ changed = true;
+ }
+ }
+
+ this.updateDictionaryOrder();
+
+ const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title);
+ const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0);
+ if (removeKeys.length > 0) {
+ for (const key of toIterable(removeKeys)) {
+ delete this.optionsDictionaries[key];
+ }
+ changed = true;
+ }
+
+ if (changed) {
+ this.save();
+ }
+ }
+
+ createEntry(dictionaryInfo) {
+ const title = dictionaryInfo.title;
+ let changed = false;
+ let optionsDictionary;
+ const optionsDictionaries = this.optionsDictionaries;
+ if (hasOwn(optionsDictionaries, title)) {
+ optionsDictionary = optionsDictionaries[title];
+ } else {
+ optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions();
+ optionsDictionaries[title] = optionsDictionary;
+ changed = true;
+ }
+
+ const content = document.importNode(this.template.content, true).firstChild;
+
+ this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary));
+
+ return changed;
+ }
+
+ static createDictionaryOptions() {
+ return utilBackgroundIsolate({
+ priority: 0,
+ enabled: false,
+ allowSecondarySearches: false
+ });
+ }
+
+ createExtra(totalCounts, remainders, totalRemainder) {
+ const content = document.importNode(this.extraTemplate.content, true).firstChild;
+ this.extraContainer.appendChild(content);
+ return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content);
+ }
+
+ setCounts(dictionaryCounts, totalCounts) {
+ const remainders = Object.assign({}, totalCounts);
+ const keys = Object.keys(remainders);
+
+ for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) {
+ const counts = dictionaryCounts[i];
+ this.dictionaryEntries[i].setCounts(counts);
+
+ for (const key of keys) {
+ remainders[key] -= counts[key];
+ }
+ }
+
+ let totalRemainder = 0;
+ for (const key of keys) {
+ totalRemainder += remainders[key];
+ }
+
+ if (this.extra !== null) {
+ this.extra.cleanup();
+ this.extra = null;
+ }
+
+ if (totalRemainder > 0) {
+ this.extra = this.createExtra(totalCounts, remainders, totalRemainder);
+ }
+ }
+
+ updateDictionaryOrder() {
+ const sortInfo = this.dictionaryEntries.map((e, i) => [e, i]);
+ sortInfo.sort((a, b) => {
+ const i = b[0].optionsDictionary.priority - a[0].optionsDictionary.priority;
+ return (i !== 0 ? i : a[1] - b[1]);
+ });
+
+ for (const [e] of sortInfo) {
+ this.container.appendChild(e.content);
+ }
+ }
+
+ save() {
+ // Overwrite
+ }
+
+ onDictionaryConfirmDelete(e) {
+ e.preventDefault();
+ const n = document.querySelector('#dict-delete-modal');
+ const title = n.dataset.dict;
+ delete n.dataset.dict;
+ $(n).modal('hide');
+
+ const index = this.dictionaryEntries.findIndex((e) => e.dictionaryInfo.title === title);
+ if (index >= 0) {
+ this.dictionaryEntries[index].deleteDictionary();
+ }
+ }
+}
+
+class SettingsDictionaryEntryUI {
+ constructor(parent, dictionaryInfo, content, optionsDictionary) {
+ this.parent = parent;
+ this.dictionaryInfo = dictionaryInfo;
+ this.optionsDictionary = optionsDictionary;
+ this.counts = null;
+ this.eventListeners = [];
+ this.isDeleting = false;
+
+ this.content = content;
+ this.enabledCheckbox = this.content.querySelector('.dict-enabled');
+ this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches');
+ this.priorityInput = this.content.querySelector('.dict-priority');
+ this.deleteButton = this.content.querySelector('.dict-delete-button');
+
+ if (this.dictionaryInfo.version < 3) {
+ this.content.querySelector('.dict-outdated').hidden = false;
+ }
+
+ this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title;
+ this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`;
+
+ this.applyValues();
+
+ this.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false);
+ this.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false);
+ this.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false);
+ this.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false);
+ }
+
+ cleanup() {
+ if (this.content !== null) {
+ if (this.content.parentNode !== null) {
+ this.content.parentNode.removeChild(this.content);
+ }
+ this.content = null;
+ }
+ this.dictionaryInfo = null;
+ this.clearEventListeners();
+ }
+
+ setCounts(counts) {
+ this.counts = counts;
+ const node = this.content.querySelector('.dict-counts');
+ node.textContent = JSON.stringify({
+ info: this.dictionaryInfo,
+ counts
+ }, null, 4);
+ node.removeAttribute('hidden');
+ }
+
+ save() {
+ this.parent.save();
+ }
+
+ addEventListener(node, type, listener, options) {
+ node.addEventListener(type, listener, options);
+ this.eventListeners.push([node, type, listener, options]);
+ }
+
+ clearEventListeners() {
+ for (const [node, type, listener, options] of this.eventListeners) {
+ node.removeEventListener(type, listener, options);
+ }
+ this.eventListeners = [];
+ }
+
+ applyValues() {
+ this.enabledCheckbox.checked = this.optionsDictionary.enabled;
+ this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches;
+ this.priorityInput.value = `${this.optionsDictionary.priority}`;
+ }
+
+ async deleteDictionary() {
+ if (this.isDeleting) {
+ return;
+ }
+
+ const progress = this.content.querySelector('.progress');
+ progress.hidden = false;
+ const progressBar = this.content.querySelector('.progress-bar');
+ this.isDeleting = true;
+
+ const prevention = new PageExitPrevention();
+ try {
+ prevention.start();
+
+ const onProgress = ({processed, count, storeCount, storesProcesed}) => {
+ let percent = 0.0;
+ if (count > 0 && storesProcesed > 0) {
+ percent = (processed / count) * (storesProcesed / storeCount) * 100.0;
+ }
+ progressBar.style.width = `${percent}%`;
+ };
+
+ await utilDatabaseDeleteDictionary(this.dictionaryInfo.title, onProgress, {rate: 1000});
+ } catch (e) {
+ dictionaryErrorsShow([e]);
+ } finally {
+ prevention.end();
+ this.isDeleting = false;
+ progress.hidden = true;
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ onDatabaseUpdated(options);
+ }
+ }
+
+ onEnabledChanged(e) {
+ this.optionsDictionary.enabled = !!e.target.checked;
+ this.save();
+ }
+
+ onAllowSecondarySearchesChanged(e) {
+ this.optionsDictionary.allowSecondarySearches = !!e.target.checked;
+ this.save();
+ }
+
+ onPriorityChanged(e) {
+ let value = Number.parseFloat(e.target.value);
+ if (Number.isNaN(value)) {
+ value = this.optionsDictionary.priority;
+ } else {
+ this.optionsDictionary.priority = value;
+ this.save();
+ }
+
+ e.target.value = `${value}`;
+
+ this.parent.updateDictionaryOrder();
+ }
+
+ onDeleteButtonClicked(e) {
+ e.preventDefault();
+
+ if (this.isDeleting) {
+ return;
+ }
+
+ const title = this.dictionaryInfo.title;
+ const n = document.querySelector('#dict-delete-modal');
+ n.dataset.dict = title;
+ document.querySelector('#dict-remove-modal-dict-name').textContent = title;
+ $(n).modal('show');
+ }
+}
+
+class SettingsDictionaryExtraUI {
+ constructor(parent, totalCounts, remainders, totalRemainder, content) {
+ this.parent = parent;
+ this.content = content;
+
+ this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`;
+
+ const node = this.content.querySelector('.dict-counts');
+ node.textContent = JSON.stringify({
+ counts: totalCounts,
+ remainders: remainders
+ }, null, 4);
+ node.removeAttribute('hidden');
+ }
+
+ cleanup() {
+ if (this.content !== null) {
+ if (this.content.parentNode !== null) {
+ this.content.parentNode.removeChild(this.content);
+ }
+ this.content = null;
+ }
+ }
+}
+
+
+async function dictSettingsInitialize() {
+ dictionaryUI = new SettingsDictionaryListUI(
+ document.querySelector('#dict-groups'),
+ document.querySelector('#dict-template'),
+ document.querySelector('#dict-groups-extra'),
+ document.querySelector('#dict-extra-template')
+ );
+ dictionaryUI.save = () => settingsSaveOptions();
+
+ document.querySelector('#dict-purge-button').addEventListener('click', (e) => onDictionaryPurgeButtonClick(e), false);
+ document.querySelector('#dict-purge-confirm').addEventListener('click', (e) => onDictionaryPurge(e), false);
+ 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);
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ onDictionaryOptionsChanged(options);
+ onDatabaseUpdated(options);
+}
+
+async function onDictionaryOptionsChanged(options) {
+ if (dictionaryUI === null) { return; }
+ dictionaryUI.setOptionsDictionaries(options.dictionaries);
+}
+
+async function onDatabaseUpdated(options) {
+ try {
+ const dictionaries = await utilDatabaseGetDictionaryInfo();
+ dictionaryUI.setDictionaries(dictionaries);
+
+ document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
+
+ updateMainDictionarySelect(options, dictionaries);
+
+ const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map((v) => v.title), true);
+ dictionaryUI.setCounts(counts, total);
+ } catch (e) {
+ dictionaryErrorsShow([e]);
+ }
+}
+
+async function updateMainDictionarySelect(options, dictionaries) {
+ const select = document.querySelector('#dict-main');
+ select.textContent = ''; // Empty
+
+ let option = document.createElement('option');
+ option.className = 'text-muted';
+ option.value = '';
+ option.textContent = 'Not selected';
+ select.appendChild(option);
+
+ let value = '';
+ const currentValue = options.general.mainDictionary;
+ for (const {title, sequenced} of toIterable(dictionaries)) {
+ if (!sequenced) { continue; }
+
+ option = document.createElement('option');
+ option.value = title;
+ option.textContent = title;
+ select.appendChild(option);
+
+ if (title === currentValue) {
+ value = title;
+ }
+ }
+
+ select.value = value;
+
+ if (options.general.mainDictionary !== value) {
+ options.general.mainDictionary = value;
+ settingsSaveOptions();
+ }
+}
+
+async function onDictionaryMainChanged(e) {
+ const value = e.target.value;
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ options.general.mainDictionary = value;
+ settingsSaveOptions();
+}
+
+
+function dictionaryErrorToString(error) {
+ if (error.toString) {
+ error = error.toString();
+ } else {
+ error = `${error}`;
+ }
+
+ for (const [match, subst] of dictionaryErrorToString.overrides) {
+ if (error.includes(match)) {
+ error = subst;
+ break;
+ }
+ }
+
+ return error;
+}
+dictionaryErrorToString.overrides = [
+ [
+ 'A mutation operation was attempted on a database that did not allow mutations.',
+ 'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
+ ],
+ [
+ 'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
+ 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
+ ],
+ [
+ 'BulkError',
+ 'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
+ ]
+];
+
+function dictionaryErrorsShow(errors) {
+ const dialog = document.querySelector('#dict-error');
+ dialog.textContent = '';
+
+ if (errors !== null && errors.length > 0) {
+ const uniqueErrors = {};
+ for (let e of errors) {
+ console.error(e);
+ e = dictionaryErrorToString(e);
+ uniqueErrors[e] = hasOwn(uniqueErrors, e) ? uniqueErrors[e] + 1 : 1;
+ }
+
+ for (const e in uniqueErrors) {
+ const count = uniqueErrors[e];
+ const div = document.createElement('p');
+ if (count > 1) {
+ div.textContent = `${e} `;
+ const em = document.createElement('em');
+ em.textContent = `(${count})`;
+ div.appendChild(em);
+ } else {
+ div.textContent = `${e}`;
+ }
+ dialog.appendChild(div);
+ }
+
+ dialog.hidden = false;
+ } else {
+ dialog.hidden = true;
+ }
+}
+
+
+function dictionarySpinnerShow(show) {
+ const spinner = $('#dict-spinner');
+ if (show) {
+ spinner.show();
+ } else {
+ spinner.hide();
+ }
+}
+
+function onDictionaryImportButtonClick() {
+ const dictFile = document.querySelector('#dict-file');
+ dictFile.click();
+}
+
+function onDictionaryPurgeButtonClick(e) {
+ e.preventDefault();
+ $('#dict-purge-modal').modal('show');
+}
+
+async function onDictionaryPurge(e) {
+ e.preventDefault();
+
+ $('#dict-purge-modal').modal('hide');
+
+ const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
+ const dictProgress = document.querySelector('#dict-purge');
+ dictProgress.hidden = false;
+
+ const prevention = new PageExitPrevention();
+
+ try {
+ prevention.start();
+ dictionaryErrorsShow(null);
+ dictionarySpinnerShow(true);
+
+ await utilDatabasePurge();
+ for (const options of toIterable(await getOptionsArray())) {
+ options.dictionaries = utilBackgroundIsolate({});
+ options.general.mainDictionary = '';
+ }
+ await settingsSaveOptions();
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ onDatabaseUpdated(options);
+ } catch (err) {
+ dictionaryErrorsShow([err]);
+ } finally {
+ prevention.end();
+
+ dictionarySpinnerShow(false);
+
+ dictControls.show();
+ dictProgress.hidden = true;
+
+ if (storageEstimate.mostRecent !== null) {
+ storageUpdateStats();
+ }
+ }
+}
+
+async function onDictionaryImport(e) {
+ const dictFile = $('#dict-file');
+ const dictControls = $('#dict-importer').hide();
+ const dictProgress = $('#dict-import-progress').show();
+ const dictImportInfo = document.querySelector('#dict-import-info');
+
+ const prevention = new PageExitPrevention();
+
+ try {
+ prevention.start();
+ dictionaryErrorsShow(null);
+ dictionarySpinnerShow(true);
+
+ const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
+ const updateProgress = (total, current) => {
+ setProgress(current / total * 100.0);
+ if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
+ storageUpdateStats();
+ }
+ };
+
+ const exceptions = [];
+ const files = [...e.target.files];
+
+ for (let i = 0, ii = files.length; i < ii; ++i) {
+ setProgress(0.0);
+ if (ii > 1) {
+ dictImportInfo.hidden = false;
+ dictImportInfo.textContent = `(${i + 1} of ${ii})`;
+ }
+
+ const summary = await utilDatabaseImport(files[i], updateProgress, exceptions);
+ for (const options of toIterable(await getOptionsArray())) {
+ const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
+ dictionaryOptions.enabled = true;
+ options.dictionaries[summary.title] = dictionaryOptions;
+ if (summary.sequenced && options.general.mainDictionary === '') {
+ options.general.mainDictionary = summary.title;
+ }
+ }
+
+ await settingsSaveOptions();
+
+ if (exceptions.length > 0) {
+ exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
+ dictionaryErrorsShow(exceptions);
+ }
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ onDatabaseUpdated(options);
+ }
+ } catch (err) {
+ dictionaryErrorsShow([err]);
+ } finally {
+ prevention.end();
+ dictionarySpinnerShow(false);
+
+ dictImportInfo.hidden = false;
+ dictImportInfo.textContent = '';
+ dictFile.val('');
+ dictControls.show();
+ dictProgress.hide();
+ }
+}
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
new file mode 100644
index 00000000..7456e7a4
--- /dev/null
+++ b/ext/bg/js/settings/main.js
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+async function getOptionsArray() {
+ const optionsFull = await apiOptionsGetFull();
+ return optionsFull.profiles.map((profile) => profile.options);
+}
+
+async function formRead(options) {
+ options.general.enable = $('#enable').prop('checked');
+ options.general.showGuide = $('#show-usage-guide').prop('checked');
+ options.general.compactTags = $('#compact-tags').prop('checked');
+ options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
+ options.general.resultOutputMode = $('#result-output-mode').val();
+ options.general.debugInfo = $('#show-debug-info').prop('checked');
+ options.general.showAdvanced = $('#show-advanced-options').prop('checked');
+ options.general.maxResults = parseInt($('#max-displayed-results').val(), 10);
+ options.general.popupDisplayMode = $('#popup-display-mode').val();
+ options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val();
+ options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val();
+ options.general.popupWidth = parseInt($('#popup-width').val(), 10);
+ options.general.popupHeight = parseInt($('#popup-height').val(), 10);
+ options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0);
+ options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10);
+ options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0);
+ options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
+ options.general.popupTheme = $('#popup-theme').val();
+ options.general.popupOuterTheme = $('#popup-outer-theme').val();
+ options.general.customPopupCss = $('#custom-popup-css').val();
+ options.general.customPopupOuterCss = $('#custom-popup-outer-css').val();
+
+ options.audio.enabled = $('#audio-playback-enabled').prop('checked');
+ options.audio.autoPlay = $('#auto-play-audio').prop('checked');
+ options.audio.volume = parseFloat($('#audio-playback-volume').val());
+ options.audio.customSourceUrl = $('#audio-custom-source').val();
+ options.audio.textToSpeechVoice = $('#text-to-speech-voice').val();
+
+ options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
+ options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
+ options.scanning.selectText = $('#select-matched-text').prop('checked');
+ options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
+ options.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
+ options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
+ options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked');
+ options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked');
+ options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked');
+ options.scanning.delay = parseInt($('#scan-delay').val(), 10);
+ options.scanning.length = parseInt($('#scan-length').val(), 10);
+ options.scanning.modifier = $('#scan-modifier-key').val();
+ options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10);
+
+ options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked');
+ options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked');
+ options.parsing.readingMode = $('#parsing-reading-mode').val();
+
+ const optionsAnkiEnableOld = options.anki.enable;
+ options.anki.enable = $('#anki-enable').prop('checked');
+ options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/));
+ options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10);
+ options.anki.server = $('#interface-server').val();
+ options.anki.screenshot.format = $('#screenshot-format').val();
+ options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
+ options.anki.fieldTemplates = $('#field-templates').val();
+
+ if (optionsAnkiEnableOld && !ankiErrorShown()) {
+ options.anki.terms.deck = $('#anki-terms-deck').val();
+ options.anki.terms.model = $('#anki-terms-model').val();
+ options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
+ options.anki.kanji.deck = $('#anki-kanji-deck').val();
+ options.anki.kanji.model = $('#anki-kanji-model').val();
+ options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
+ }
+}
+
+async function formWrite(options) {
+ $('#enable').prop('checked', options.general.enable);
+ $('#show-usage-guide').prop('checked', options.general.showGuide);
+ $('#compact-tags').prop('checked', options.general.compactTags);
+ $('#compact-glossaries').prop('checked', options.general.compactGlossaries);
+ $('#result-output-mode').val(options.general.resultOutputMode);
+ $('#show-debug-info').prop('checked', options.general.debugInfo);
+ $('#show-advanced-options').prop('checked', options.general.showAdvanced);
+ $('#max-displayed-results').val(options.general.maxResults);
+ $('#popup-display-mode').val(options.general.popupDisplayMode);
+ $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition);
+ $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition);
+ $('#popup-width').val(options.general.popupWidth);
+ $('#popup-height').val(options.general.popupHeight);
+ $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset);
+ $('#popup-vertical-offset').val(options.general.popupVerticalOffset);
+ $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2);
+ $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
+ $('#popup-theme').val(options.general.popupTheme);
+ $('#popup-outer-theme').val(options.general.popupOuterTheme);
+ $('#custom-popup-css').val(options.general.customPopupCss);
+ $('#custom-popup-outer-css').val(options.general.customPopupOuterCss);
+
+ $('#audio-playback-enabled').prop('checked', options.audio.enabled);
+ $('#auto-play-audio').prop('checked', options.audio.autoPlay);
+ $('#audio-playback-volume').val(options.audio.volume);
+ $('#audio-custom-source').val(options.audio.customSourceUrl);
+ $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice);
+
+ $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse);
+ $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled);
+ $('#select-matched-text').prop('checked', options.scanning.selectText);
+ $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
+ $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
+ $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
+ $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch);
+ $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions);
+ $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage);
+ $('#scan-delay').val(options.scanning.delay);
+ $('#scan-length').val(options.scanning.length);
+ $('#scan-modifier-key').val(options.scanning.modifier);
+ $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth);
+
+ $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser);
+ $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser);
+ $('#parsing-reading-mode').val(options.parsing.readingMode);
+
+ $('#anki-enable').prop('checked', options.anki.enable);
+ $('#card-tags').val(options.anki.tags.join(' '));
+ $('#sentence-detection-extent').val(options.anki.sentenceExt);
+ $('#interface-server').val(options.anki.server);
+ $('#screenshot-format').val(options.anki.screenshot.format);
+ $('#screenshot-quality').val(options.anki.screenshot.quality);
+ $('#field-templates').val(options.anki.fieldTemplates);
+
+ onAnkiTemplatesValidateCompile();
+ await onAnkiOptionsChanged(options);
+ await onDictionaryOptionsChanged(options);
+
+ formUpdateVisibility(options);
+}
+
+function formSetupEventListeners() {
+ $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change((e) => onFormOptionsChanged(e));
+}
+
+function formUpdateVisibility(options) {
+ document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`;
+ document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`;
+ document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`;
+ document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`;
+
+ if (options.general.debugInfo) {
+ const temp = utilIsolate(options);
+ temp.anki.fieldTemplates = '...';
+ const text = JSON.stringify(temp, null, 4);
+ $('#debug').text(text);
+ }
+}
+
+async function onFormOptionsChanged() {
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+
+ await formRead(options);
+ await settingsSaveOptions();
+ formUpdateVisibility(options);
+
+ await onAnkiOptionsChanged(options);
+}
+
+
+function settingsGetSource() {
+ return new Promise((resolve) => {
+ chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`));
+ });
+}
+
+async function settingsSaveOptions() {
+ const source = await settingsGetSource();
+ await apiOptionsSave(source);
+}
+
+async function onOptionsUpdate({source}) {
+ const thisSource = await settingsGetSource();
+ if (source === thisSource) { return; }
+
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(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');
+ if (node === null) { return; }
+
+ const manifest = chrome.runtime.getManifest();
+ node.textContent = `${manifest.name} v${manifest.version}`;
+}
+
+
+async function onReady() {
+ showExtensionInformation();
+
+ formSetupEventListeners();
+ appearanceInitialize();
+ await audioSettingsInitialize();
+ await profileOptionsSetup();
+ await dictSettingsInitialize();
+ ankiInitialize();
+ ankiTemplatesInitialize();
+
+ storageInfoInitialize();
+
+ chrome.runtime.onMessage.addListener(onMessage);
+}
+
+$(document).ready(() => onReady());
diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings/popup-preview-frame.js
index 7d641c46..49409968 100644
--- a/ext/bg/js/settings-popup-preview.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -106,7 +106,7 @@ class SettingsPopupPreview {
onMessage(e) {
const {action, params} = e.data;
const handlers = SettingsPopupPreview.messageHandlers;
- if (handlers.hasOwnProperty(action)) {
+ if (hasOwn(handlers, action)) {
const handler = handlers[action];
handler(this, params);
}
diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js
new file mode 100644
index 00000000..d8579eb1
--- /dev/null
+++ b/ext/bg/js/settings/popup-preview.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+function appearanceInitialize() {
+ let previewVisible = false;
+ $('#settings-popup-preview-button').on('click', () => {
+ if (previewVisible) { return; }
+ showAppearancePreview();
+ previewVisible = true;
+ });
+}
+
+function showAppearancePreview() {
+ const container = $('#settings-popup-preview-container');
+ const buttonContainer = $('#settings-popup-preview-button-container');
+ const settings = $('#settings-popup-preview-settings');
+ const text = $('#settings-popup-preview-text');
+ const customCss = $('#custom-popup-css');
+ const customOuterCss = $('#custom-popup-outer-css');
+
+ const frame = document.createElement('iframe');
+ frame.src = '/bg/settings-popup-preview.html';
+ frame.id = 'settings-popup-preview-frame';
+
+ window.wanakana.bind(text[0]);
+
+ text.on('input', () => {
+ const action = 'setText';
+ const params = {text: text.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+ customCss.on('input', () => {
+ const action = 'setCustomCss';
+ const params = {css: customCss.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+ customOuterCss.on('input', () => {
+ const action = 'setCustomOuterCss';
+ const params = {css: customOuterCss.val()};
+ frame.contentWindow.postMessage({action, params}, '*');
+ });
+
+ container.append(frame);
+ buttonContainer.remove();
+ settings.css('display', '');
+}
diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings/profiles.js
index ededc998..8c218e97 100644
--- a/ext/bg/js/settings-profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -35,16 +35,16 @@ async function profileOptionsSetup() {
}
function profileOptionsSetupEventListeners() {
- $('#profile-target').change(utilAsync(onTargetProfileChanged));
- $('#profile-name').change(onProfileNameChanged);
- $('#profile-add').click(utilAsync(onProfileAdd));
- $('#profile-remove').click(utilAsync(onProfileRemove));
- $('#profile-remove-confirm').click(utilAsync(onProfileRemoveConfirm));
- $('#profile-copy').click(utilAsync(onProfileCopy));
- $('#profile-copy-confirm').click(utilAsync(onProfileCopyConfirm));
+ $('#profile-target').change((e) => onTargetProfileChanged(e));
+ $('#profile-name').change((e) => onProfileNameChanged(e));
+ $('#profile-add').click((e) => onProfileAdd(e));
+ $('#profile-remove').click((e) => onProfileRemove(e));
+ $('#profile-remove-confirm').click((e) => onProfileRemoveConfirm(e));
+ $('#profile-copy').click((e) => onProfileCopy(e));
+ $('#profile-copy-confirm').click((e) => onProfileCopyConfirm(e));
$('#profile-move-up').click(() => onProfileMove(-1));
$('#profile-move-down').click(() => onProfileMove(1));
- $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(utilAsync(onProfileOptionsChanged));
+ $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change((e) => onProfileOptionsChanged(e));
}
function tryGetIntegerValue(selector, min, max) {
@@ -95,7 +95,7 @@ async function profileFormWrite(optionsFull) {
$('#profile-add-condition-group')
);
profileConditionsContainer.save = () => {
- apiOptionsSave();
+ settingsSaveOptions();
conditionsClearCaches(profileConditionsDescriptor);
};
profileConditionsContainer.isolate = utilBackgroundIsolate;
@@ -147,7 +147,7 @@ function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) {
let i = 0;
while (true) {
const newName = `${prefix}${space}${index}${suffix}`;
- if (i++ >= maxUniqueAttempts || profiles.findIndex(profile => profile.name === newName) < 0) {
+ if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) {
return newName;
}
if (typeof index !== 'number') {
@@ -166,7 +166,7 @@ async function onProfileOptionsChanged(e) {
const optionsFull = await apiOptionsGetFull();
await profileFormRead(optionsFull);
- await apiOptionsSave();
+ await settingsSaveOptions();
}
async function onTargetProfileChanged() {
@@ -188,7 +188,7 @@ async function onProfileAdd() {
optionsFull.profiles.push(profile);
currentProfileIndex = optionsFull.profiles.length - 1;
await profileOptionsUpdateTarget(optionsFull);
- await apiOptionsSave();
+ await settingsSaveOptions();
}
async function onProfileRemove(e) {
@@ -226,7 +226,7 @@ async function onProfileRemoveConfirm() {
}
await profileOptionsUpdateTarget(optionsFull);
- await apiOptionsSave();
+ await settingsSaveOptions();
}
function onProfileNameChanged() {
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
new file mode 100644
index 00000000..51ca6855
--- /dev/null
+++ b/ext/bg/js/settings/storage.js
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+function storageBytesToLabeledString(size) {
+ const base = 1000;
+ const labels = [' bytes', 'KB', 'MB', 'GB'];
+ let labelIndex = 0;
+ while (size >= base) {
+ size /= base;
+ ++labelIndex;
+ }
+ const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
+ return `${label}${labels[labelIndex]}`;
+}
+
+async function storageEstimate() {
+ try {
+ return (storageEstimate.mostRecent = await navigator.storage.estimate());
+ } catch (e) {
+ // NOP
+ }
+ return null;
+}
+storageEstimate.mostRecent = null;
+
+async function isStoragePeristent() {
+ try {
+ return await navigator.storage.persisted();
+ } catch (e) {
+ // NOP
+ }
+ return false;
+}
+
+async function storageInfoInitialize() {
+ storagePersistInitialize();
+ const {browser, platform} = await apiGetEnvironmentInfo();
+ document.documentElement.dataset.browser = browser;
+ document.documentElement.dataset.operatingSystem = platform.os;
+
+ await storageShowInfo();
+
+ document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false);
+}
+
+async function storageUpdateStats() {
+ storageUpdateStats.isUpdating = true;
+
+ const estimate = await storageEstimate();
+ const valid = (estimate !== null);
+
+ if (valid) {
+ // Firefox reports usage as 0 when persistent storage is enabled.
+ const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
+ if (finite) {
+ document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
+ document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
+ }
+ document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
+ document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
+ }
+
+ storageUpdateStats.isUpdating = false;
+ return valid;
+}
+storageUpdateStats.isUpdating = false;
+
+async function storageShowInfo() {
+ storageSpinnerShow(true);
+
+ const valid = await storageUpdateStats();
+ document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
+ document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
+
+ storageSpinnerShow(false);
+}
+
+function storageSpinnerShow(show) {
+ const spinner = $('#storage-spinner');
+ if (show) {
+ spinner.show();
+ } else {
+ spinner.hide();
+ }
+}
+
+async function storagePersistInitialize() {
+ if (!(navigator.storage && navigator.storage.persist)) {
+ // Not supported
+ return;
+ }
+
+ const info = document.querySelector('#storage-persist-info');
+ const button = document.querySelector('#storage-persist-button');
+ const checkbox = document.querySelector('#storage-persist-button-checkbox');
+
+ info.classList.remove('storage-hidden');
+ button.classList.remove('storage-hidden');
+
+ let persisted = await isStoragePeristent();
+ checkbox.checked = persisted;
+
+ button.addEventListener('click', async () => {
+ if (persisted) {
+ return;
+ }
+ let result = false;
+ try {
+ result = await navigator.storage.persist();
+ } catch (e) {
+ // NOP
+ }
+
+ if (result) {
+ persisted = true;
+ checkbox.checked = true;
+ storageShowInfo();
+ } else {
+ $('.storage-persist-fail-warning').removeClass('storage-hidden');
+ }
+ }, false);
+}
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index 59516d97..9320477f 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -1,32 +1,5 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
-templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) {
- return " <p class=\"text-warning\">This dictionary is outdated and may not support new extension features; please import the latest version.</p>\n";
-},"3":function(container,depth0,helpers,partials,data) {
- return "checked";
-},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
- var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
-
- return "<div class=\"dict-group well well-sm\" data-title=\""
- + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
- + "\">\n <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> "
- + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
- + " <small>rev."
- + alias4(((helper = (helper = helpers.revision || (depth0 != null ? depth0.revision : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"revision","hash":{},"data":data}) : helper)))
- + "</small></h4>\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.outdated : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "\n <div class=\"checkbox\">\n <label><input type=\"checkbox\" class=\"dict-enabled\" "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enabled : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "> Enable search</label>\n </div>\n <div class=\"checkbox options-advanced\">\n <label><input type=\"checkbox\" class=\"dict-allow-secondary-searches\" "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.allowSecondarySearches : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + "> Allow secondary searches</label>\n </div>\n <div class=\"form-group options-advanced\">\n <label for=\"dict-"
- + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
- + "\">Result priority</label>\n <input type=\"number\" value=\""
- + alias4(((helper = (helper = helpers.priority || (depth0 != null ? depth0.priority : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"priority","hash":{},"data":data}) : helper)))
- + "\" id=\"dict-"
- + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
- + "\" class=\"form-control dict-priority\">\n </div>\n</div>\n";
-},"useData":true});
templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1;
@@ -60,19 +33,18 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
return "<div class=\"entry\" data-type=\"kanji\">\n <div class=\"actions\">\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n <div class=\"glyph\">"
+ + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n <div class=\"glyph expression-scan-toggle\">"
+ container.escapeExpression(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper)))
+ "</div>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n <table class=\"table table-condensed glyph-data\">\n <tr>\n <th>Glossary</th>\n <th>Readings</th>\n <th>Statistics</th>\n </tr>\n <tr>\n <td class=\"glossary\">\n"
- + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(21, data, 0),"inverse":container.program(24, data, 0),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(19, data, 0),"inverse":container.program(22, data, 0),"data":data})) != null ? stack1 : "")
+ " </td>\n <td class=\"reading\">\n "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n "
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n </td>\n <td>"
+ ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.misc : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ "</td>\n </tr>\n <tr>\n <th colspan=\"3\">Classifications</th>\n </tr>\n <tr>\n <td colspan=\"3\">"
@@ -82,19 +54,17 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ "</td>\n </tr>\n <tr>\n <th colspan=\"3\">Dictionary Indices</th>\n </tr>\n <tr>\n <td colspan=\"3\">"
+ ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.index : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ "</td>\n </tr>\n </table>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(31, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>\n";
},"11":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add Kanji (Alt + K)\" alt></a>\n";
},"13":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";
-},"15":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"16":function(container,depth0,helpers,partials,data) {
+},"14":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-frequency\">"
@@ -102,13 +72,13 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ ":"
+ alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"18":function(container,depth0,helpers,partials,data) {
+},"16":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(17, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"19":function(container,depth0,helpers,partials,data) {
+},"17":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-"
@@ -118,68 +88,81 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ "\">"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"21":function(container,depth0,helpers,partials,data) {
+},"19":function(container,depth0,helpers,partials,data) {
var stack1;
return " <ol>"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</ol>\n";
-},"22":function(container,depth0,helpers,partials,data) {
+},"20":function(container,depth0,helpers,partials,data) {
return "<li><span class=\"glossary-item\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</span></li>";
-},"24":function(container,depth0,helpers,partials,data) {
+},"22":function(container,depth0,helpers,partials,data) {
var stack1;
return " <span class=\"glossary-item\">"
+ container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0))
+ "</span>\n";
-},"26":function(container,depth0,helpers,partials,data) {
+},"24":function(container,depth0,helpers,partials,data) {
var stack1;
return "<dl>"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</dl>";
-},"27":function(container,depth0,helpers,partials,data) {
+},"25":function(container,depth0,helpers,partials,data) {
return "<dd>"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</dd>";
-},"29":function(container,depth0,helpers,partials,data) {
+},"27":function(container,depth0,helpers,partials,data) {
var stack1;
return "<dl>"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</dl>";
-},"31":function(container,depth0,helpers,partials,data) {
+},"29":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, buffer =
" <pre>";
- stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(30, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</pre>\n";
-},"32":function(container,depth0,helpers,partials,data) {
+},"30":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "");
-},"34":function(container,depth0,helpers,partials,data,blockParams,depths) {
- var stack1;
+},"32":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"35":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return "<div class=\"term-navigation\">\n <a href=\"#\" "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.program(35, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n <a href=\"#\" "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.program(39, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n"
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"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\"";
+},"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\"";
+},"41":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"source":(depths[1] != null ? depths[1].source : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"36":function(container,depth0,helpers,partials,data) {
+ + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
+},"42":function(container,depth0,helpers,partials,data) {
return "<hr>";
-},"38":function(container,depth0,helpers,partials,data) {
+},"44":function(container,depth0,helpers,partials,data) {
return "<p class=\"note\">No results found</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return "\n\n"
- + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+ + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(44, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
var decorators = container.decorators;
@@ -190,23 +173,58 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
}
,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});
-templates['model.html'] = template({"1":function(container,depth0,helpers,partials,data) {
- return " <li><a class=\"marker-link\" href=\"#\">"
+templates['query-parser.html'] = template({"1":function(container,depth0,helpers,partials,data) {
+ var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
+
+ return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.preview : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(alias1,depth0,{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + "</span>";
+},"2":function(container,depth0,helpers,partials,data) {
+ return "<span class=\"query-parser-term-preview\">";
+},"4":function(container,depth0,helpers,partials,data) {
+ return "<span class=\"query-parser-term\">";
+},"6":function(container,depth0,helpers,partials,data) {
+ var stack1;
+
+ return ((stack1 = container.invokePartial(partials.part,depth0,{"name":"part","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
+},"8":function(container,depth0,helpers,partials,data) {
+ var stack1;
+
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.raw : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(12, data, 0),"data":data})) != null ? stack1 : "");
+},"9":function(container,depth0,helpers,partials,data) {
+ var stack1;
+
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"10":function(container,depth0,helpers,partials,data) {
+ return "<span class=\"query-parser-char\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
- + "</a></li>\n";
-},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
- var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+ + "</span>";
+},"12":function(container,depth0,helpers,partials,data) {
+ var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return "<tr>\n <td class=\"col-sm-2\">"
- + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
- + "</td>\n <td class=\"col-sm-10\">\n <div class=\"input-group\">\n <input type=\"text\" class=\"anki-field-value form-control\" data-field=\""
- + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
- + "\" value=\""
- + alias4(((helper = (helper = helpers.value || (depth0 != null ? depth0.value : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"value","hash":{},"data":data}) : helper)))
- + "\">\n <div class=\"input-group-btn\">\n <button type=\"button\" class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\">\n <span class=\"caret\"></span>\n </button>\n <ul class=\"dropdown-menu dropdown-menu-right\">\n"
- + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.markers : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + " </ul>\n </div>\n </div>\n </td>\n</tr>\n";
-},"useData":true});
+ return "<ruby>"
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + "<rt>"
+ + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper)))
+ + "</rt></ruby>";
+},"14":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ var stack1;
+
+ return ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"preview":(depths[1] != null ? depths[1].preview : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ var stack1;
+
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.terms : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
+
+ var decorators = container.decorators;
+
+ fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn;
+ fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(8, data, 0, blockParams, depths),"inverse":container.noop,"args":["part"],"data":data}) || fn;
+ return fn;
+ }
+
+,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});
templates['terms.html'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =
"<div class=\"dict-";
@@ -298,17 +316,16 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
return "<div class=\"entry\" data-type=\"term\">\n <div class=\"actions\">\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(27, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(47, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.program(45, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(50, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(52, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n <div class=\"glossary\">\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ " </div>\n\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(64, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>\n";
},"25":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n";
@@ -318,47 +335,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"28":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n";
-},"30":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";
-},"32":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"30":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"33":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(31, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"31":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", buffer =
- "<div class=\"expression\"><span class=\"expression-"
+ "<div class=\"expression expression-scan-toggle\"><span class=\"expression-"
+ container.escapeExpression(((helper = (helper = helpers.termFrequency || (depth0 != null ? depth0.termFrequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"termFrequency","hash":{},"data":data}) : helper)))
+ "\">";
- stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
+ stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</span><div class=\"peek-wrapper\">"
- + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(40, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div><span class=\""
- + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(45, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(43, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\">、</span></div>";
-},"34":function(container,depth0,helpers,partials,data) {
+},"32":function(container,depth0,helpers,partials,data) {
var stack1, helper, options;
- stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.furigana) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { return stack1; }
else { return ''; }
-},"35":function(container,depth0,helpers,partials,data) {
+},"33":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "");
-},"37":function(container,depth0,helpers,partials,data) {
+},"35":function(container,depth0,helpers,partials,data) {
return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>";
-},"39":function(container,depth0,helpers,partials,data) {
+},"37":function(container,depth0,helpers,partials,data) {
var stack1;
return "<div class=\"tags\">"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(40, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>";
-},"40":function(container,depth0,helpers,partials,data) {
+},"38":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-"
@@ -368,13 +383,13 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ "\">"
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"42":function(container,depth0,helpers,partials,data) {
+},"40":function(container,depth0,helpers,partials,data) {
var stack1;
return "<div class=\"frequencies\">"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(43, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>";
-},"43":function(container,depth0,helpers,partials,data) {
+},"41":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-frequency\">"
@@ -382,45 +397,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ":"
+ alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"45":function(container,depth0,helpers,partials,data) {
+},"43":function(container,depth0,helpers,partials,data) {
return "invisible";
-},"47":function(container,depth0,helpers,partials,data) {
+},"45":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer =
- " <div class=\"expression\">";
- stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
+ " <div class=\"expression expression-scan-toggle\">";
+ stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper));
if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</div>\n"
- + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"48":function(container,depth0,helpers,partials,data) {
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(46, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"46":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div style=\"display: inline-block;\">\n"
+ ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"50":function(container,depth0,helpers,partials,data) {
+},"48":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div class=\"reasons\">\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(49, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"51":function(container,depth0,helpers,partials,data) {
+},"49":function(container,depth0,helpers,partials,data) {
var stack1;
return " <span class=\"reasons\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</span> "
- + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(50, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n";
-},"52":function(container,depth0,helpers,partials,data) {
+},"50":function(container,depth0,helpers,partials,data) {
return "&laquo;";
-},"54":function(container,depth0,helpers,partials,data) {
+},"52":function(container,depth0,helpers,partials,data) {
var stack1;
return " <div>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(55, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(53, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n";
-},"55":function(container,depth0,helpers,partials,data) {
+},"53":function(container,depth0,helpers,partials,data) {
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
return " <span class=\"label label-default tag-frequency\">"
@@ -428,61 +443,74 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ":"
+ alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper)))
+ "</span>\n";
-},"57":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"55":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
-},"58":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(59, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+},"56":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return " <ol>\n"
- + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(59, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </ol>\n";
-},"59":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"57":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return " <li>"
+ ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ "</li>\n";
-},"61":function(container,depth0,helpers,partials,data) {
+},"59":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"63":function(container,depth0,helpers,partials,data,blockParams,depths) {
+},"61":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
- return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(64, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
-},"64":function(container,depth0,helpers,partials,data) {
+ return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(62, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+},"62":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "")
+ " ";
-},"66":function(container,depth0,helpers,partials,data) {
+},"64":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, buffer =
" <pre>";
- stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
+ stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper));
if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + "</pre>\n";
-},"68":function(container,depth0,helpers,partials,data,blockParams,depths) {
- var stack1;
-
- return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
-},"69":function(container,depth0,helpers,partials,data,blockParams,depths) {
- var stack1;
+},"66":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
- return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(70, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ return "<div class=\"term-navigation\">\n <a href=\"#\" "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(67, data, 0, blockParams, depths),"inverse":container.program(69, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n <a href=\"#\" "
+ + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(71, data, 0, blockParams, depths),"inverse":container.program(73, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")
+ + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n"
+ + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(75, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"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\"";
+},"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\"";
+},"75":function(container,depth0,helpers,partials,data,blockParams,depths) {
+ var stack1;
+
+ return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(76, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "\n"
- + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"source":(depths[1] != null ? depths[1].source : depths[1]),"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
-},"70":function(container,depth0,helpers,partials,data) {
+ + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
+},"76":function(container,depth0,helpers,partials,data) {
return "<hr>";
-},"72":function(container,depth0,helpers,partials,data) {
+},"78":function(container,depth0,helpers,partials,data) {
return "<p class=\"note\">No results found.</p>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return "\n\n"
- + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.program(72, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
+ + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.program(78, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "");
},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
var decorators = container.decorators;
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 9d90136b..202014c9 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -42,27 +42,16 @@ class Translator {
await this.database.purge();
}
- async findTermsGrouped(text, dictionaries, alphanumeric, options) {
- const titles = Object.keys(dictionaries);
- const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
-
- const definitionsGrouped = dictTermsGroup(definitions, dictionaries);
- await this.buildTermFrequencies(definitionsGrouped, titles);
-
- if (options.general.compactTags) {
- for (const definition of definitionsGrouped) {
- dictTermsCompressTags(definition.definitions);
- }
- }
-
- return {length, definitions: definitionsGrouped};
+ async deleteDictionary(dictionaryName) {
+ this.tagCache = {};
+ await this.database.deleteDictionary(dictionaryName);
}
async getSequencedDefinitions(definitions, mainDictionary) {
const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary);
const defaultDefinitions = definitionsBySequence['-1'];
- const sequenceList = Object.keys(definitionsBySequence).map(v => Number(v)).filter(v => v >= 0);
+ const sequenceList = Object.keys(definitionsBySequence).map((v) => Number(v)).filter((v) => v >= 0);
const sequencedDefinitions = sequenceList.map((key) => ({
definitions: definitionsBySequence[key],
rawDefinitions: []
@@ -135,7 +124,7 @@ class Translator {
for (const expression of result.expressions.keys()) {
for (const reading of result.expressions.get(expression).keys()) {
const termTags = result.expressions.get(expression).get(reading);
- const score = termTags.map(tag => tag.score).reduce((p, v) => p + v, 0);
+ const score = termTags.map((tag) => tag.score).reduce((p, v) => p + v, 0);
expressions.push({
expression: expression,
reading: reading,
@@ -152,10 +141,41 @@ class Translator {
return result;
}
- async findTermsMerged(text, dictionaries, alphanumeric, options) {
- const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches);
+ async findTerms(text, details, options) {
+ switch (options.general.resultOutputMode) {
+ case 'group':
+ return await this.findTermsGrouped(text, details, options);
+ case 'merge':
+ return await this.findTermsMerged(text, details, options);
+ case 'split':
+ return await this.findTermsSplit(text, details, options);
+ default:
+ return [[], 0];
+ }
+ }
+
+ async findTermsGrouped(text, details, options) {
+ const dictionaries = dictEnabledSet(options);
+ const titles = Object.keys(dictionaries);
+ const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details);
+
+ const definitionsGrouped = dictTermsGroup(definitions, dictionaries);
+ await this.buildTermFrequencies(definitionsGrouped, titles);
+
+ if (options.general.compactTags) {
+ for (const definition of definitionsGrouped) {
+ dictTermsCompressTags(definition.definitions);
+ }
+ }
+
+ return [definitionsGrouped, length];
+ }
+
+ async findTermsMerged(text, details, options) {
+ const dictionaries = dictEnabledSet(options);
+ const secondarySearchTitles = Object.keys(options.dictionaries).filter((dict) => options.dictionaries[dict].allowSecondarySearches);
const titles = Object.keys(dictionaries);
- const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
+ const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details);
const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary);
const definitionsMerged = [];
const mergedByTermIndices = new Set();
@@ -186,29 +206,33 @@ class Translator {
}
}
- return {length, definitions: dictTermsSort(definitionsMerged)};
+ return [dictTermsSort(definitionsMerged), length];
}
- async findTermsSplit(text, dictionaries, alphanumeric) {
+ async findTermsSplit(text, details, options) {
+ const dictionaries = dictEnabledSet(options);
const titles = Object.keys(dictionaries);
- const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric);
+ const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details);
await this.buildTermFrequencies(definitions, titles);
- return {length, definitions};
+ return [definitions, length];
}
- async findTerms(text, dictionaries, alphanumeric) {
+ async findTermsInternal(text, dictionaries, alphanumeric, details) {
if (!alphanumeric && text.length > 0) {
const c = text[0];
if (!jpIsKana(c) && !jpIsKanji(c)) {
- return {length: 0, definitions: []};
+ return [[], 0];
}
}
- const textHiragana = jpKatakanaToHiragana(text);
const titles = Object.keys(dictionaries);
- const deinflections = await this.findTermDeinflections(text, textHiragana, titles);
+ const deinflections = (
+ details.wildcard ?
+ await this.findTermWildcard(text, titles) :
+ await this.findTermDeinflections(text, titles)
+ );
let definitions = [];
for (const deinflection of deinflections) {
@@ -241,10 +265,26 @@ class Translator {
length = Math.max(length, definition.source.length);
}
- return {length, definitions};
+ return [definitions, length];
}
- async findTermDeinflections(text, text2, titles) {
+ async findTermWildcard(text, titles) {
+ const definitions = await this.database.findTermsBulk([text], titles, true);
+ if (definitions.length === 0) {
+ return [];
+ }
+
+ return [{
+ source: text,
+ term: text,
+ rules: 0,
+ definitions,
+ reasons: []
+ }];
+ }
+
+ async findTermDeinflections(text, titles) {
+ const text2 = jpKatakanaToHiragana(text);
const deinflections = (text === text2 ? this.getDeinflections(text) : this.getDeinflections2(text, text2));
if (deinflections.length === 0) {
@@ -257,7 +297,7 @@ class Translator {
for (const deinflection of deinflections) {
const term = deinflection.term;
let deinflectionArray;
- if (uniqueDeinflectionsMap.hasOwnProperty(term)) {
+ if (hasOwn(uniqueDeinflectionsMap, term)) {
deinflectionArray = uniqueDeinflectionsMap[term];
} else {
deinflectionArray = [];
@@ -268,7 +308,7 @@ class Translator {
deinflectionArray.push(deinflection);
}
- const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles);
+ const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, false);
for (const definition of definitions) {
const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);
@@ -280,41 +320,42 @@ class Translator {
}
}
- return deinflections.filter(e => e.definitions.length > 0);
+ return deinflections.filter((e) => e.definitions.length > 0);
}
getDeinflections(text) {
const deinflections = [];
for (let i = text.length; i > 0; --i) {
- const textSlice = text.slice(0, i);
- deinflections.push(...this.deinflector.deinflect(textSlice));
+ const textSubstring = text.substring(0, i);
+ deinflections.push(...this.deinflector.deinflect(textSubstring));
}
return deinflections;
}
- getDeinflections2(text, text2) {
+ getDeinflections2(text1, text2) {
const deinflections = [];
- for (let i = text.length; i > 0; --i) {
- const textSlice = text.slice(0, i);
- const text2Slice = text2.slice(0, i);
- deinflections.push(...this.deinflector.deinflect(textSlice));
- if (textSlice !== text2Slice) {
- deinflections.push(...this.deinflector.deinflect(text2Slice));
+ for (let i = text1.length; i > 0; --i) {
+ const text1Substring = text1.substring(0, i);
+ const text2Substring = text2.substring(0, i);
+ deinflections.push(...this.deinflector.deinflect(text1Substring));
+ if (text1Substring !== text2Substring) {
+ deinflections.push(...this.deinflector.deinflect(text2Substring));
}
}
return deinflections;
}
- async findKanji(text, dictionaries) {
+ async findKanji(text, options) {
+ const dictionaries = dictEnabledSet(options);
const titles = Object.keys(dictionaries);
const kanjiUnique = {};
const kanjiList = [];
for (const c of text) {
- if (!kanjiUnique.hasOwnProperty(c)) {
+ if (!hasOwn(kanjiUnique, c)) {
kanjiList.push(c);
kanjiUnique[c] = true;
}
@@ -376,7 +417,7 @@ class Translator {
const expression = term.expression;
term.frequencies = [];
- if (termsUniqueMap.hasOwnProperty(expression)) {
+ if (hasOwn(termsUniqueMap, expression)) {
termsUniqueMap[expression].push(term);
} else {
const termList = [term];
@@ -423,7 +464,7 @@ class Translator {
const category = meta.category;
const group = (
- stats.hasOwnProperty(category) ?
+ hasOwn(stats, category) ?
stats[category] :
(stats[category] = [])
);
@@ -443,7 +484,7 @@ class Translator {
async getTagMetaList(names, title) {
const tagMetaList = [];
const cache = (
- this.tagCache.hasOwnProperty(title) ?
+ hasOwn(this.tagCache, title) ?
this.tagCache[title] :
(this.tagCache[title] = {})
);
@@ -451,7 +492,7 @@ class Translator {
for (const name of names) {
const base = Translator.getNameBase(name);
- if (cache.hasOwnProperty(base)) {
+ if (hasOwn(cache, base)) {
tagMetaList.push(cache[base]);
} else {
const tagMeta = await this.database.findTagForTitle(base, title);
@@ -475,6 +516,6 @@ class Translator {
static getNameBase(name) {
const pos = name.indexOf(':');
- return (pos >= 0 ? name.substr(0, pos) : name);
+ return (pos >= 0 ? name.substring(0, pos) : name);
}
}
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 1ca0833b..3dd5fd55 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -16,12 +16,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-function utilAsync(func) {
- return function(...args) {
- func.apply(this, args);
- };
-}
-
function utilIsolate(data) {
return JSON.parse(JSON.stringify(data));
}
@@ -47,13 +41,13 @@ function utilSetEqual(setA, setB) {
function utilSetIntersection(setA, setB) {
return new Set(
- [...setA].filter(value => setB.has(value))
+ [...setA].filter((value) => setB.has(value))
);
}
function utilSetDifference(setA, setB) {
return new Set(
- [...setA].filter(value => !setB.has(value))
+ [...setA].filter((value) => !setB.has(value))
);
}
@@ -80,8 +74,12 @@ function utilAnkiGetDeckNames() {
return utilBackend().anki.getDeckNames();
}
-function utilDatabaseSummarize() {
- return utilBackend().translator.database.summarize();
+function utilDatabaseGetDictionaryInfo() {
+ return utilBackend().translator.database.getDictionaryInfo();
+}
+
+function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
+ return utilBackend().translator.database.getDictionaryCounts(dictionaryNames, getTotal);
}
function utilAnkiGetModelFieldNames(modelName) {
@@ -92,6 +90,10 @@ function utilDatabasePurge() {
return utilBackend().translator.purgeDatabase();
}
+function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
+ return utilBackend().translator.database.deleteDictionary(dictionaryName, 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.
@@ -109,7 +111,3 @@ function utilReadFile(file) {
reader.readAsBinaryString(file);
});
}
-
-function utilIsObject(value) {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
diff --git a/ext/bg/legal.html b/ext/bg/legal.html
index 30927da6..082239d7 100644
--- a/ext/bg/legal.html
+++ b/ext/bg/legal.html
@@ -41,12 +41,10 @@ and are used in conformance with the Group's <a href="https://www.edrdg.org/edrd
<h3>Third-Party Software Licenses</h3>
<ul>
<li><a href="https://github.com/twbs/bootstrap/blob/v3.3.7/LICENSE" target="_blank" rel="noopener">Bootstrap v3.3.7</a></li>
- <li><a href="https://github.com/minhur/bootstrap-toggle/blob/2.2.0/LICENSE" target="_blank" rel="noopener">Bootstrap Toggle v2.2.0</a></li>
- <li><a href="https://github.com/dfahlander/Dexie.js/blob/v2.0.0-beta.10/LICENSE" target="_blank" rel="noopener">Dexie v2.0.0-beta.10</a></li>
<li><a href="https://github.com/wycats/handlebars.js/blob/v4.0.6/LICENSE" target="_blank" rel="noopener">Handlebars v4.0.6</a></li>
<li><a href="https://github.com/jquery/jquery/blob/3.2.1/LICENSE.txt" target="_blank" rel="noopener">jQuery v3.2.1</a></li>
<li><a href="https://github.com/Stuk/jszip/blob/v3.1.3/LICENSE.markdown" target="_blank" rel="noopener">JSZip v3.1.3</a></li>
- <li><a href="https://github.com/WaniKani/WanaKana/blob/2.2.3/LICENSE" target="_blank" rel="noopener">WanaKana v2.2.3</a></li>
+ <li><a href="https://github.com/WaniKani/WanaKana/blob/4.0.2/LICENSE" target="_blank" rel="noopener">WanaKana v4.0.2</a></li>
</ul>
</div>
</div>
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 91140b95..fef30456 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -25,18 +25,14 @@
<p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>
</div>
- <div class="input-group" style="padding-top: 10px; font-size: 20px; user-select: none;">
+ <div class="input-group" style="padding-top: 20px;">
<span title="Enable kana input method" class="input-group-text">
- <label>
- あ
- <input type="checkbox" id="wanakana-enable" />
- </label>
+ <input type="checkbox" id="wanakana-enable" class="icon-checkbox" />
+ <label for="wanakana-enable" class="scan-disable">あ</label>
</span>
<span title="Enable clipboard monitor" class="input-group-text">
- <label>
- <span class="glyphicon glyphicon-paste"></span>
- <input type="checkbox" id="clipboard-monitor-enable" />
- </label>
+ <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" />
+ <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label>
</span>
</div>
@@ -51,13 +47,21 @@
<img src="/mixed/img/spinner.gif">
</div>
+ <div class="scan-disable">
+ <div id="query-parser-select" class="input-group"></div>
+ <div id="query-parser"></div>
+ </div>
+
+ <hr>
+
<div id="content"></div>
</div>
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
- <script src="/mixed/js/extension.js"></script>
+ <script src="/mixed/js/core.js"></script>
+ <script src="/mixed/js/dom.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
@@ -67,10 +71,12 @@
<script src="/fg/js/source.js"></script>
<script src="/fg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script>
+ <script src="/mixed/js/display-context.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/mixed/js/scroll.js"></script>
+ <script src="/bg/js/search-query-parser.js"></script>
<script src="/bg/js/search.js"></script>
<script src="/bg/js/search-frontend.js"></script>
</body>
diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html
index d27a9a33..339467d4 100644
--- a/ext/bg/settings-popup-preview.html
+++ b/ext/bg/settings-popup-preview.html
@@ -117,7 +117,9 @@
</div>
</div></div></div>
- <script src="/mixed/js/extension.js"></script>
+ <script src="/mixed/js/core.js"></script>
+ <script src="/mixed/js/dom.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>
@@ -126,6 +128,6 @@
<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.js"></script>
+ <script src="/bg/js/settings/popup-preview-frame.js"></script>
</body>
</html>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index a3b75576..3c5494b8 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -272,7 +272,7 @@
<div class="form-group ignore-form-changes" style="display: none;" id="settings-popup-preview-settings">
<label for="settings-popup-preview-text">Popup preview text</label>
- <input type="text" id="settings-popup-preview-text" class="form-control" value="読め">
+ <input type="text" id="settings-popup-preview-text" class="form-control" value="読め" placeholder="Preview text">
</div>
<div class="form-group ignore-form-changes">
@@ -401,7 +401,7 @@
</div>
<div class="checkbox">
- <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of popup expressions</label>
+ <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of expressions in search results</label>
</div>
<div class="form-group">
@@ -410,18 +410,52 @@
</div>
</div>
- <div>
+ <div id="text-parsing">
+ <h3>Text Parsing Options</h3>
+
+ <p class="help-block">
+ Yomichan can attempt to parse entire sentences or longer text blocks on the search page,
+ adding furigana above words and a small space between words.
+ </p>
+
+ <p class="help-block">
+ Two types of parsers are supported. The first one, enabled by default, works using the built-in
+ scanning functionality by automatically advancing in the sentence after a matching word.
+ </p>
+
+ <p class="help-block">
+ The second type is an external program called <a href="https://en.wikipedia.org/wiki/MeCab" target="_blank" rel="noopener">MeCab</a>
+ that uses its own dictionaries and a special parsing algorithm. To get it working, you must first
+ install it and <a href="https://github.com/siikamiika/yomichan-mecab-installer" target="_blank" rel="noopener">a native messaging component</a>
+ that acts as a bridge between the program and Yomichan.
+ </p>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="parsing-scan-enable"> Enable text parsing using installed dictionaries</label>
+ </div>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="parsing-mecab-enable"> Enable text parsing using MeCab</label>
+ </div>
+
+ <div class="form-group">
+ <label for="parsing-reading-mode">Reading mode</label>
+ <select class="form-control" id="parsing-reading-mode">
+ <option value="hiragana">ひらがな</option>
+ <option value="katakana">カタカナ</option>
+ <option value="romaji">Romaji</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="ignore-form-changes">
<div>
<img src="/mixed/img/spinner.gif" class="pull-right" id="dict-spinner" alt>
<h3>Dictionaries</h3>
</div>
<p class="help-block">
- Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled,
- or you can simply <a href="#" id="dict-purge-link">purge the database</a> to delete everything.
- </p>
- <p class="help-block">
- Deleting individual dictionaries is not currently feasible due to limitations of browser database technology.
+ Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled.
</p>
<div class="form-group" id="dict-main-group">
@@ -429,14 +463,16 @@
<select class="form-control" id="dict-main"></select>
</div>
- <div class="text-danger" id="dict-purge">Dictionary data is being purged, please be patient...</div>
- <div class="alert alert-warning" id="dict-warning">No dictionaries have been installed</div>
- <div class="alert alert-danger" id="dict-error"></div>
+ <div class="text-danger" id="dict-purge" hidden>Dictionary data is being purged, please be patient...</div>
+ <div class="alert alert-warning" id="dict-warning" hidden>No dictionaries have been installed</div>
+ <div class="alert alert-danger" id="dict-error" hidden></div>
<div id="dict-groups"></div>
+ <div id="dict-groups-extra"></div>
<div id="dict-import-progress">
Dictionary data is being imported, please be patient...
+ <span id="dict-import-info" hidden></span>
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 0%"></div>
</div>
@@ -448,9 +484,85 @@
<a href="https://foosoft.net/projects/yomichan" target="_blank" rel="noopener">download free dictionaries</a>
for use with this extension and to learn about importing proprietary EPWING dictionaries.
</p>
- <button class="btn btn-primary" id="dict-file-button">Import Dictionary</button>
- <div hidden><input type="file" id="dict-file"></div>
+ <div>
+ <button class="btn btn-primary" id="dict-file-button">Import Dictionary</button>
+ <button class="btn btn-danger" id="dict-purge-button">Purge Database</button>
+ </div>
+ <div hidden><input type="file" id="dict-file" accept=".zip,application/zip" multiple></div>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="dict-purge-modal">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">Confirm database purge</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to delete all data in the database?
+ </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="dict-purge-confirm">Purge Database</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="dict-delete-modal">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">Confirm dictionary deletion</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to delete the dictionary <em id="dict-remove-modal-dict-name"></em>?
+ This operation may take some time and the responsiveness of this browser tab may be reduced.
+ </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="dict-delete-confirm">Delete Dictionary</button>
+ </div>
+ </div>
+ </div>
</div>
+
+ <template id="dict-template"><div class="dict-group well well-sm">
+ <h4><span class="text-muted glyphicon glyphicon-book"></span> <span class="dict-title"></span> <small class="dict-revision"></small></h4>
+ <p class="text-warning dict-outdated" hidden>This dictionary is outdated and may not support new extension features; please import the latest version.</p>
+
+ <div class="checkbox">
+ <label><input type="checkbox" class="dict-enabled"> Enable search</label>
+ </div>
+ <div class="checkbox options-advanced">
+ <label><input type="checkbox" class="dict-allow-secondary-searches"> Allow secondary searches</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">
+ </div>
+ <div class="dict-delete-table">
+ <div>
+ <button class="btn btn-default dict-delete-button">Delete Dictionary</button>
+ </div>
+ <div>
+ <div class="progress" hidden>
+ <div class="progress-bar progress-bar-striped" style="width: 0%"></div>
+ </div>
+ </div>
+ </div>
+ <pre class="debug dict-counts" hidden></pre>
+ </div></template>
+
+ <template id="dict-extra-template"><div class="well well-sm">
+ <h4><span class="text-muted glyphicon glyphicon-alert"></span> <span class="dict-title">Unassociated Data</span> <small class="dict-total-count"></small></h4>
+ <p class="text-warning">
+ The database contains extra data which is not associated with any installed dictionary.
+ Purging the database can fix this issue.
+ </p>
+ <pre class="debug dict-counts" hidden></pre>
+ </div></template>
</div>
<div id="storage-info">
@@ -538,7 +650,7 @@
</div>
</div>
- <div class="alert alert-danger" id="anki-error"></div>
+ <div class="alert alert-danger" id="anki-error" hidden></div>
<div class="form-group">
<label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label>
@@ -582,16 +694,16 @@
</ul>
<div class="tab-content">
- <div id="terms" class="tab-pane fade in active">
+ <div id="terms" class="tab-pane fade in active" data-anki-card-type="terms">
<div class="row">
<div class="form-group col-xs-6">
<label for="anki-terms-deck">Deck</label>
- <select class="form-control anki-deck" id="anki-terms-deck"></select>
+ <select class="form-control anki-deck" id="anki-terms-deck" data-anki-card-type="terms"></select>
</div>
<div class="form-group col-xs-6">
<label for="anki-terms-model">Model</label>
- <select class="form-control anki-model" id="anki-terms-model"></select>
+ <select class="form-control anki-model" id="anki-terms-model" data-anki-card-type="terms"></select>
</div>
</div>
@@ -601,16 +713,16 @@
</table>
</div>
- <div id="kanji" class="tab-pane fade">
+ <div id="kanji" class="tab-pane fade" data-anki-card-type="kanji">
<div class="row">
<div class="form-group col-xs-6">
<label for="anki-kanji-deck">Deck</label>
- <select class="form-control anki-deck" id="anki-kanji-deck"></select>
+ <select class="form-control anki-deck" id="anki-kanji-deck" data-anki-card-type="kanji"></select>
</div>
<div class="form-group col-xs-6">
<label for="anki-kanji-model">Model</label>
- <select class="form-control anki-model" id="anki-kanji-model"></select>
+ <select class="form-control anki-model" id="anki-kanji-model" data-anki-card-type="kanji"></select>
</div>
</div>
@@ -625,11 +737,78 @@
<p class="help-block">
Fields are formatted using the <a href="https://handlebarsjs.com/" target="_blank" rel="noopener">Handlebars.js</a> template rendering
engine. Advanced users can modify these templates for ultimate control of what information gets included in
- their Anki cards. If you encounter problems with your changes you can always <a href="#" id="field-templates-reset">reset to default</a>
- template settings.
+ 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>
+ <button class="btn btn-danger" id="field-templates-reset">Reset Templates</button>
+ </div>
+ <p></p>
+ <pre id="field-template-compile-result" hidden></pre>
+
+ <p>Templates can be tested using the inputs below.</p>
+
+ <div class="form-group">
+ <div class="row">
+ <div class="col-xs-6">
+ <label for="field-templates-preview-text">Preview text</label>
+ <input type="text" id="field-templates-preview-text" class="form-control" value="読め" placeholder="Preview text">
+ </div>
+ <div class="col-xs-6">
+ <label for="field-template-render-text">Test field</label>
+ <div class="input-group">
+ <div class="input-group-btn">
+ <button class="btn btn-default" id="field-template-render" title="Test"><span class="glyphicon glyphicon-play"></span></button>
+ </div>
+ <input type="text" class="form-control" id="field-template-render-text" value="{expression}" placeholder="{marker}">
+ <div class="input-group-btn">
+ <button class="btn btn-default dropdown-toggle" id="field-templates-dropdown" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu dropdown-menu-right" id="field-templates-list"></ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <p></p>
+ <pre id="field-template-render-result" hidden></pre>
+ </div>
+
+ <div class="modal fade" tabindex="-1" role="dialog" id="field-template-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">Confirm template reset</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to reset the field templates to the default value?
+ Any changes you made will be lost.
+ </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="field-templates-reset-confirm">Reset Templates</button>
+ </div>
+ </div>
+ </div>
</div>
+
+ <template id="anki-field-template"><tr>
+ <td class="col-sm-2 anki-field-name"></td>
+ <td class="col-sm-10">
+ <div class="input-group">
+ <input type="text" class="anki-field-value form-control" data-field="" value="">
+ <div class="input-group-btn">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right anki-field-marker-list"></ul>
+ </div>
+ </div>
+ </td>
+ </tr></template>
+
+ <template id="anki-field-marker-template"><li><a class="marker-link" href="#"></a></li></template>
</div>
</div>
</div>
@@ -673,7 +852,7 @@
</p>
</div>
- <pre id="debug"></pre>
+ <pre id="debug" class="debug"></pre>
<div class="pull-right bottom-links">
<small><span id="extension-info"></span> &bull; <a href="search.html">Search</a> &bull; <a href="https://foosoft.net/projects/yomichan/" target="_blank" rel="noopener">Homepage</a> &bull; <a href="legal.html">Legal</a></small>
@@ -685,7 +864,9 @@
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
- <script src="/mixed/js/extension.js"></script>
+ <script src="/mixed/js/core.js"></script>
+ <script src="/mixed/js/dom.js"></script>
+ <script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/anki.js"></script>
<script src="/bg/js/api.js"></script>
@@ -695,12 +876,20 @@
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/options.js"></script>
+ <script src="/bg/js/page-exit-prevention.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script>
- <script src="/bg/js/settings-profiles.js"></script>
- <script src="/bg/js/settings.js"></script>
+ <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/dictionaries.js"></script>
+ <script src="/bg/js/settings/popup-preview.js"></script>
+ <script src="/bg/js/settings/profiles.js"></script>
+ <script src="/bg/js/settings/storage.js"></script>
+
+ <script src="/bg/js/settings/main.js"></script>
</body>
</html>