summaryrefslogtreecommitdiff
path: root/ext/bg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg')
-rw-r--r--ext/bg/background.html1
-rw-r--r--ext/bg/css/settings.css28
-rw-r--r--ext/bg/guide.html2
-rw-r--r--ext/bg/js/anki.js31
-rw-r--r--ext/bg/js/api.js29
-rw-r--r--ext/bg/js/audio-ui.js131
-rw-r--r--ext/bg/js/audio.js152
-rw-r--r--ext/bg/js/backend.js4
-rw-r--r--ext/bg/js/database.js99
-rw-r--r--ext/bg/js/deinflector.js73
-rw-r--r--ext/bg/js/options.js27
-rw-r--r--ext/bg/js/request.js4
-rw-r--r--ext/bg/js/search-frontend.js2
-rw-r--r--ext/bg/js/search.js101
-rw-r--r--ext/bg/js/settings.js33
-rw-r--r--ext/bg/js/templates.js16
-rw-r--r--ext/bg/js/translator.js17
-rw-r--r--ext/bg/lang/deinflect.json163
-rw-r--r--ext/bg/search.html8
-rw-r--r--ext/bg/settings.html63
20 files changed, 662 insertions, 322 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 3b37db87..194d4a45 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -27,6 +27,7 @@
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/translator.js"></script>
<script src="/bg/js/util.js"></script>
+ <script src="/mixed/js/audio.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/bg/js/backend.js"></script>
diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
index 12bbe8a8..6284058a 100644
--- a/ext/bg/css/settings.css
+++ b/ext/bg/css/settings.css
@@ -89,19 +89,24 @@
text-align-last: center;
}
-.condition-group>.condition>div:first-child {
+.condition-group>.condition>*:first-child,
+.audio-source-list>.audio-source>*:first-child {
border-bottom-left-radius: 0;
}
-.condition-group>.condition:nth-child(n+2)>div:first-child {
+.condition-group>.condition:nth-child(n+2)>*:first-child,
+.audio-source-list>.audio-source:nth-child(n+2)>*:first-child {
border-top-left-radius: 0;
}
-.condition-group>.condition:nth-child(n+2)>div:last-child>button {
+.condition-group>.condition:nth-child(n+2)>div:last-child>button,
+.audio-source-list>.audio-source:nth-child(n+2)>*:last-child>button {
border-top-right-radius: 0;
}
-.condition-group>.condition:nth-last-child(n+2)>div:last-child>button {
+.condition-group>.condition:nth-last-child(n+2)>div:last-child>button,
+.audio-source-list>.audio-source:nth-last-child(n+2)>*:last-child>button {
border-bottom-right-radius: 0;
}
-.condition-group-options>.condition-add {
+.condition-group-options>.condition-add,
+.audio-source-options>.audio-source-add {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@@ -110,6 +115,19 @@
display: none;
}
+.audio-source-list {
+ counter-reset: audio-source-id;
+}
+.audio-source-list .audio-source-prefix {
+ flex: 0 0 auto;
+ width: 39px;
+ text-align: center;
+}
+.audio-source-list .audio-source-prefix:after {
+ counter-increment: audio-source-id;
+ content: counter(audio-source-id);
+}
+
#custom-popup-css {
width: 100%;
min-height: 34px;
diff --git a/ext/bg/guide.html b/ext/bg/guide.html
index 7ec1d8d9..2a602f1f 100644
--- a/ext/bg/guide.html
+++ b/ext/bg/guide.html
@@ -23,7 +23,7 @@
<li>Click on the <em>monkey wrench</em> icon in the middle to open the options page.</li>
<li>Import the dictionaries you wish to use for term and Kanji searches.</li>
<li>Hold down <kbd>Shift</kbd> key or the middle mouse button as you move your mouse over text to display definitions.</li>
- <li>Click on the <img src="/mixed/img/play-audio.png" alt> icon to hear the term pronounced by a native speaker.</li>
+ <li>Click on the <img src="/mixed/img/play-audio.svg" alt> icon to hear the term pronounced by a native speaker.</li>
<li>Click on individual Kanji in the term definition results to view additional information about those characters.</li>
</ol>
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index bd4e46cd..9f851f13 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -67,14 +67,39 @@ class AnkiConnect {
if (this.remoteVersion < this.localVersion) {
this.remoteVersion = await this.ankiInvoke('version');
if (this.remoteVersion < this.localVersion) {
- throw 'Extension and plugin versions incompatible';
+ throw new Error('Extension and plugin versions incompatible');
}
}
}
+ async findNoteIds(notes) {
+ await this.checkVersion();
+ const actions = notes.map(note => ({
+ action: 'findNotes',
+ params: {
+ query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}`
+ }
+ }));
+ return await this.ankiInvoke('multi', {actions});
+ }
+
ankiInvoke(action, params) {
return requestJson(this.server, 'POST', {action, params, version: this.localVersion});
}
+
+ static escapeQuery(text) {
+ return text.replace(/"/g, '');
+ }
+
+ static fieldsToQuery(fields) {
+ const fieldNames = Object.keys(fields);
+ if (fieldNames.length === 0) {
+ return '';
+ }
+
+ const key = fieldNames[0];
+ return `${key.toLowerCase()}:"${AnkiConnect.escapeQuery(fields[key])}"`;
+ }
}
@@ -106,4 +131,8 @@ class AnkiNull {
async guiBrowse(query) {
return [];
}
+
+ async findNoteIds(notes) {
+ return [];
+ }
}
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index 222e7ffe..f768e6f9 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -68,7 +68,8 @@ async function apiDefinitionAdd(definition, mode, context, optionsContext) {
await audioInject(
definition,
options.anki.terms.fields,
- options.general.audioSource
+ options.audio.sources,
+ optionsContext
);
}
@@ -97,15 +98,33 @@ async function apiDefinitionsAddable(definitions, modes, optionsContext) {
}
}
- const results = await utilBackend().anki.canAddNotes(notes);
+ const cannotAdd = [];
+ const anki = utilBackend().anki;
+ const results = await anki.canAddNotes(notes);
for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {
const state = {};
for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) {
- state[modes[modeOffset]] = results[resultBase + modeOffset];
+ const index = resultBase + modeOffset;
+ const result = results[index];
+ const info = {canAdd: result};
+ state[modes[modeOffset]] = info;
+ if (!result) {
+ cannotAdd.push([notes[index], info]);
+ }
}
states.push(state);
}
+
+ if (cannotAdd.length > 0) {
+ const noteIdsArray = await anki.findNoteIds(cannotAdd.map(e => e[0]));
+ for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
+ const noteIds = noteIdsArray[i];
+ if (noteIds.length > 0) {
+ cannotAdd[i][1].noteId = noteIds[0];
+ }
+ }
+ }
} catch (e) {
// NOP
}
@@ -156,8 +175,8 @@ apiCommandExec.handlers = {
}
};
-async function apiAudioGetUrl(definition, source) {
- return audioBuildUrl(definition, source);
+async function apiAudioGetUrl(definition, source, optionsContext) {
+ return audioBuildUrl(definition, source, optionsContext);
}
async function apiInjectScreenshot(definition, fields, screenshot) {
diff --git a/ext/bg/js/audio-ui.js b/ext/bg/js/audio-ui.js
new file mode 100644
index 00000000..381129ac
--- /dev/null
+++ b/ext/bg/js/audio-ui.js
@@ -0,0 +1,131 @@
+/*
+ * 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 AudioSourceUI {
+ static instantiateTemplate(templateSelector) {
+ const template = document.querySelector(templateSelector);
+ const content = document.importNode(template.content, true);
+ return $(content.firstChild);
+ }
+}
+
+AudioSourceUI.Container = class Container {
+ constructor(audioSources, container, addButton) {
+ this.audioSources = audioSources;
+ this.container = container;
+ this.addButton = addButton;
+ this.children = [];
+
+ this.container.empty();
+
+ for (const audioSource of toIterable(audioSources)) {
+ this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
+ }
+
+ this.addButton.on('click', () => this.onAddAudioSource());
+ }
+
+ cleanup() {
+ for (const child of this.children) {
+ child.cleanup();
+ }
+
+ this.addButton.off('click');
+ this.container.empty();
+ }
+
+ save() {
+ // Override
+ }
+
+ remove(child) {
+ const index = this.children.indexOf(child);
+ if (index < 0) {
+ return;
+ }
+
+ child.cleanup();
+ this.children.splice(index, 1);
+ this.audioSources.splice(index, 1);
+
+ for (let i = index; i < this.children.length; ++i) {
+ this.children[i].index = i;
+ }
+ }
+
+ onAddAudioSource() {
+ const audioSource = this.getUnusedAudioSource();
+ this.audioSources.push(audioSource);
+ this.save();
+ this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
+ }
+
+ getUnusedAudioSource() {
+ const audioSourcesAvailable = [
+ 'jpod101',
+ 'jpod101-alternate',
+ 'jisho',
+ 'custom'
+ ];
+ for (const source of audioSourcesAvailable) {
+ if (this.audioSources.indexOf(source) < 0) {
+ return source;
+ }
+ }
+ return audioSourcesAvailable[0];
+ }
+};
+
+AudioSourceUI.AudioSource = class AudioSource {
+ constructor(parent, audioSource, index) {
+ this.parent = parent;
+ this.audioSource = audioSource;
+ this.index = index;
+
+ this.container = AudioSourceUI.instantiateTemplate('#audio-source-template').appendTo(parent.container);
+ this.select = this.container.find('.audio-source-select');
+ this.removeButton = this.container.find('.audio-source-remove');
+
+ this.select.val(audioSource);
+
+ this.select.on('change', () => this.onSelectChanged());
+ this.removeButton.on('click', () => this.onRemoveClicked());
+ }
+
+ cleanup() {
+ this.select.off('change');
+ this.removeButton.off('click');
+ this.container.remove();
+ }
+
+ save() {
+ this.parent.save();
+ }
+
+ onSelectChanged() {
+ this.audioSource = this.select.val();
+ this.parent.audioSources[this.index] = this.audioSource;
+ this.save();
+ }
+
+ onRemoveClicked() {
+ this.parent.remove(this);
+ this.save();
+ }
+};
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index 2e5db7cc..9e0ae67c 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -17,8 +17,8 @@
*/
-async function audioBuildUrl(definition, mode, cache={}) {
- if (mode === 'jpod101') {
+const audioUrlBuilders = {
+ 'jpod101': async (definition) => {
let kana = definition.reading;
let kanji = definition.expression;
@@ -35,84 +35,80 @@ async function audioBuildUrl(definition, mode, cache={}) {
params.push(`kana=${encodeURIComponent(kana)}`);
}
- const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- return Promise.resolve(url);
- } else if (mode === 'jpod101-alternate') {
- return new Promise((resolve, reject) => {
- const response = cache[definition.expression];
- if (response) {
- resolve(response);
- } else {
- const data = {
- post: 'dictionary_reference',
- match_type: 'exact',
- search_query: definition.expression
- };
-
- const params = [];
- for (const key in data) {
- params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`);
- }
-
- const xhr = new XMLHttpRequest();
- xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- xhr.addEventListener('error', () => reject('Failed to scrape audio data'));
- xhr.addEventListener('load', () => {
- cache[definition.expression] = xhr.responseText;
- resolve(xhr.responseText);
- });
-
- xhr.send(params.join('&'));
- }
- }).then(response => {
- const dom = new DOMParser().parseFromString(response, 'text/html');
- for (const row of dom.getElementsByClassName('dc-result-row')) {
- try {
- const url = row.querySelector('audio>source[src]').getAttribute('src');
- const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText;
- if (url && reading && (!definition.reading || definition.reading === reading)) {
- return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
- }
- } catch (e) {
- // NOP
- }
- }
+ return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
+ },
+ 'jpod101-alternate': async (definition) => {
+ const response = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data')));
+ xhr.addEventListener('load', () => resolve(xhr.responseText));
+ xhr.send(`post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(definition.expression)}`);
});
- } else if (mode === 'jisho') {
- return new Promise((resolve, reject) => {
- const response = cache[definition.expression];
- if (response) {
- resolve(response);
- } else {
- const xhr = new XMLHttpRequest();
- xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
- xhr.addEventListener('error', () => reject('Failed to scrape audio data'));
- xhr.addEventListener('load', () => {
- cache[definition.expression] = xhr.responseText;
- resolve(xhr.responseText);
- });
-
- xhr.send();
- }
- }).then(response => {
+
+ const dom = new DOMParser().parseFromString(response, 'text/html');
+ for (const row of dom.getElementsByClassName('dc-result-row')) {
try {
- const dom = new DOMParser().parseFromString(response, 'text/html');
- const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`);
- if (audio) {
- const url = audio.getElementsByTagName('source').item(0).getAttribute('src');
- if (url) {
- return audioUrlNormalize(url, 'https://jisho.org', '/search/');
- }
+ const url = row.querySelector('audio>source[src]').getAttribute('src');
+ const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText;
+ if (url && reading && (!definition.reading || definition.reading === reading)) {
+ return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/');
}
} catch (e) {
// NOP
}
+ }
+
+ throw new Error('Failed to find audio URL');
+ },
+ 'jisho': async (definition) => {
+ const response = await new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
+ xhr.addEventListener('error', () => reject(new Error('Failed to scrape audio data')));
+ xhr.addEventListener('load', () => resolve(xhr.responseText));
+ xhr.send();
});
+
+ const dom = new DOMParser().parseFromString(response, 'text/html');
+ try {
+ const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`);
+ if (audio !== null) {
+ const url = audio.getElementsByTagName('source').item(0).getAttribute('src');
+ if (url) {
+ return audioUrlNormalize(url, 'https://jisho.org', '/search/');
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+
+ throw new Error('Failed to find audio URL');
+ },
+ '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));
}
- else {
- return Promise.resolve();
+};
+
+async function audioBuildUrl(definition, mode, optionsContext, cache={}) {
+ const cacheKey = `${mode}:${definition.expression}`;
+ if (cache.hasOwnProperty(cacheKey)) {
+ return Promise.resolve(cache[cacheKey]);
+ }
+
+ if (audioUrlBuilders.hasOwnProperty(mode)) {
+ const handler = audioUrlBuilders[mode];
+ return handler(definition, optionsContext).then(
+ (url) => {
+ cache[cacheKey] = url;
+ return url;
+ },
+ () => null);
}
+ return null;
}
function audioUrlNormalize(url, baseUrl, basePath) {
@@ -145,9 +141,10 @@ function audioBuildFilename(definition) {
return filename += '.mp3';
}
+ return null;
}
-async function audioInject(definition, fields, mode) {
+async function audioInject(definition, fields, sources, optionsContext) {
let usesAudio = false;
for (const name in fields) {
if (fields[name].includes('{audio}')) {
@@ -166,11 +163,12 @@ async function audioInject(definition, fields, mode) {
audioSourceDefinition = definition.expressions[0];
}
- const url = await audioBuildUrl(audioSourceDefinition, mode);
- const filename = audioBuildFilename(audioSourceDefinition);
-
- if (url && filename) {
- definition.audio = {url, filename};
+ const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, false);
+ if (url !== null) {
+ const filename = audioBuildFilename(audioSourceDefinition);
+ if (filename !== null) {
+ definition.audio = {url, filename};
+ }
}
return true;
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 3c5ad885..453f4282 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -75,7 +75,7 @@ class Backend {
const promise = handler(params, sender);
promise
.then(result => callback({result}))
- .catch(error => callback({error: typeof error.toString === 'function' ? error.toString() : error}));
+ .catch(error => callback(errorToJson(error)));
}
return true;
@@ -181,7 +181,7 @@ Backend.messageHandlers = {
noteView: ({noteId}) => apiNoteView(noteId),
templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic),
commandExec: ({command}) => apiCommandExec(command),
- audioGetUrl: ({definition, source}) => apiAudioGetUrl(definition, source),
+ audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),
screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),
forward: ({action, params}, sender) => apiForward(action, params, sender),
frameInformationGet: (params, sender) => apiFrameInformationGet(sender),
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index e8214c3c..771a71c9 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -25,7 +25,7 @@ class Database {
async prepare() {
if (this.db) {
- throw 'Database already initialized';
+ throw new Error('Database already initialized');
}
this.db = new Dexie('dict');
@@ -48,9 +48,7 @@ class Database {
}
async purge() {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
this.db.close();
await this.db.delete();
@@ -61,9 +59,7 @@ class Database {
}
async findTerms(term, titles) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => {
@@ -80,7 +76,12 @@ class Database {
const visited = {};
const results = [];
const createResult = Database.createTerm;
- const filter = (row) => titles.includes(row.dictionary);
+ const processRow = (row, index) => {
+ if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) {
+ visited[row.id] = true;
+ results.push(createResult(row, index));
+ }
+ };
const db = this.db.backendDB();
const dbTransaction = db.transaction(['terms'], 'readonly');
@@ -91,8 +92,8 @@ class Database {
for (let i = 0; i < terms.length; ++i) {
const only = IDBKeyRange.only(terms[i]);
promises.push(
- Database.getAll(dbIndex1, only, i, visited, filter, createResult, results),
- Database.getAll(dbIndex2, only, i, visited, filter, createResult, results)
+ Database.getAll(dbIndex1, only, i, processRow),
+ Database.getAll(dbIndex2, only, i, processRow)
);
}
@@ -102,9 +103,7 @@ class Database {
}
async findTermsExact(term, reading, titles) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.terms.where('expression').equals(term).each(row => {
@@ -117,9 +116,7 @@ class Database {
}
async findTermsBySequence(sequence, mainDictionary) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.terms.where('sequence').equals(sequence).each(row => {
@@ -132,9 +129,7 @@ class Database {
}
async findTermMeta(term, titles) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.termMeta.where('expression').equals(term).each(row => {
@@ -152,10 +147,13 @@ class Database {
async findTermMetaBulk(terms, titles) {
const promises = [];
- const visited = {};
const results = [];
const createResult = Database.createTermMeta;
- const filter = (row) => titles.includes(row.dictionary);
+ const processRow = (row, index) => {
+ if (titles.includes(row.dictionary)) {
+ results.push(createResult(row, index));
+ }
+ };
const db = this.db.backendDB();
const dbTransaction = db.transaction(['termMeta'], 'readonly');
@@ -164,7 +162,7 @@ class Database {
for (let i = 0; i < terms.length; ++i) {
const only = IDBKeyRange.only(terms[i]);
- promises.push(Database.getAll(dbIndex, only, i, visited, filter, createResult, results));
+ promises.push(Database.getAll(dbIndex, only, i, processRow));
}
await Promise.all(promises);
@@ -173,9 +171,7 @@ class Database {
}
async findKanji(kanji, titles) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.kanji.where('character').equals(kanji).each(row => {
@@ -196,9 +192,7 @@ class Database {
}
async findKanjiMeta(kanji, titles) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const results = [];
await this.db.kanjiMeta.where('character').equals(kanji).each(row => {
@@ -224,9 +218,7 @@ class Database {
}
async findTagForTitle(name, title) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const cache = (this.tagCache.hasOwnProperty(title) ? this.tagCache[title] : (this.tagCache[title] = {}));
@@ -243,17 +235,13 @@ class Database {
}
async summarize() {
- if (this.db) {
- return this.db.dictionaries.toArray();
- } else {
- throw 'Database not initialized';
- }
+ this.validate();
+
+ return this.db.dictionaries.toArray();
}
async importDictionary(archive, progressCallback, exceptions) {
- if (!this.db) {
- throw 'Database not initialized';
- }
+ this.validate();
const maxTransactionLength = 1000;
const bulkAdd = async (table, items, total, current) => {
@@ -293,12 +281,12 @@ class Database {
const indexDataLoaded = async summary => {
if (summary.version > 3) {
- throw 'Unsupported dictionary version';
+ throw new Error('Unsupported dictionary version');
}
const count = await this.db.dictionaries.where('title').equals(summary.title).count();
if (count > 0) {
- throw 'Dictionary is already imported';
+ throw new Error('Dictionary is already imported');
}
await this.db.dictionaries.add(summary);
@@ -424,6 +412,12 @@ class Database {
);
}
+ validate() {
+ if (this.db === null) {
+ throw new Error('Database not initialized');
+ }
+ }
+
static async importDictionaryZip(
archive,
indexDataLoaded,
@@ -437,12 +431,12 @@ class Database {
const indexFile = zip.files['index.json'];
if (!indexFile) {
- throw 'No dictionary index found in archive';
+ throw new Error('No dictionary index found in archive');
}
const index = JSON.parse(await indexFile.async('string'));
if (!index.title || !index.revision) {
- throw 'Unrecognized dictionary format';
+ throw new Error('Unrecognized dictionary format');
}
const summary = {
@@ -537,39 +531,32 @@ class Database {
};
}
- static getAll(dbIndex, query, index, visited, filter, createResult, results) {
+ static getAll(dbIndex, query, context, processRow) {
const fn = typeof dbIndex.getAll === 'function' ? Database.getAllFast : Database.getAllUsingCursor;
- return fn(dbIndex, query, index, visited, filter, createResult, results);
+ return fn(dbIndex, query, context, processRow);
}
- static getAllFast(dbIndex, query, index, visited, filter, createResult, results) {
+ static getAllFast(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.getAll(query);
request.onerror = (e) => reject(e);
request.onsuccess = (e) => {
for (const row of e.target.result) {
- if (filter(row, index) && !visited.hasOwnProperty(row.id)) {
- visited[row.id] = true;
- results.push(createResult(row, index));
- }
+ processRow(row, context);
}
resolve();
};
});
}
- static getAllUsingCursor(dbIndex, query, index, visited, filter, createResult, results) {
+ static getAllUsingCursor(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.openCursor(query, 'next');
request.onerror = (e) => reject(e);
request.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
- const row = cursor.value;
- if (filter(row, index) && !visited.hasOwnProperty(row.id)) {
- visited[row.id] = true;
- results.push(createResult(row, index));
- }
+ processRow(cursor.value, context);
cursor.continue();
} else {
resolve();
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index ad77895c..ce4b2961 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -19,51 +19,74 @@
class Deinflector {
constructor(reasons) {
- this.reasons = reasons;
+ this.reasons = Deinflector.normalizeReasons(reasons);
}
deinflect(source) {
const results = [{
source,
term: source,
- rules: [],
+ rules: 0,
definitions: [],
reasons: []
}];
for (let i = 0; i < results.length; ++i) {
- const entry = results[i];
-
- for (const reason in this.reasons) {
- for (const variant of this.reasons[reason]) {
- let accept = entry.rules.length === 0;
- if (!accept) {
- for (const rule of entry.rules) {
- if (variant.rulesIn.includes(rule)) {
- accept = true;
- break;
- }
- }
- }
-
- if (!accept || !entry.term.endsWith(variant.kanaIn)) {
- continue;
- }
-
- const term = entry.term.slice(0, -variant.kanaIn.length) + variant.kanaOut;
- if (term.length === 0) {
+ const {rules, term, reasons} = results[i];
+ for (const [reason, variants] of this.reasons) {
+ for (const [kanaIn, kanaOut, rulesIn, rulesOut] of variants) {
+ if (
+ (rules !== 0 && (rules & rulesIn) === 0) ||
+ !term.endsWith(kanaIn) ||
+ (term.length - kanaIn.length + kanaOut.length) <= 0
+ ) {
continue;
}
results.push({
source,
- term,
- rules: variant.rulesOut,
+ term: term.slice(0, -kanaIn.length) + kanaOut,
+ rules: rulesOut,
definitions: [],
- reasons: [reason, ...entry.reasons]
+ reasons: [reason, ...reasons]
});
}
}
}
return results;
}
+
+ static normalizeReasons(reasons) {
+ const normalizedReasons = [];
+ for (const reason in reasons) {
+ const variants = [];
+ for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasons[reason]) {
+ variants.push([
+ kanaIn,
+ kanaOut,
+ Deinflector.rulesToRuleFlags(rulesIn),
+ Deinflector.rulesToRuleFlags(rulesOut)
+ ]);
+ }
+ normalizedReasons.push([reason, variants]);
+ }
+ return normalizedReasons;
+ }
+
+ static rulesToRuleFlags(rules) {
+ const ruleTypes = Deinflector.ruleTypes;
+ let value = 0;
+ for (const rule of rules) {
+ value |= ruleTypes[rule];
+ }
+ return value;
+ }
}
+
+Deinflector.ruleTypes = {
+ 'v1': 0b0000001, // Verb ichidan
+ 'v5': 0b0000010, // Verb godan
+ 'vs': 0b0000100, // Verb suru
+ 'vk': 0b0001000, // Verb kuru
+ 'adj-i': 0b0010000, // Adjective i
+ 'iru': 0b0100000, // Intermediate -iru endings for progressive or perfect tense
+};
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index 2c9de1ec..d0aa6fd3 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -74,6 +74,18 @@ const profileOptionsVersionUpdates = [
if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) {
options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();
}
+ },
+ (options) => {
+ const oldAudioSource = options.general.audioSource;
+ const disabled = oldAudioSource === 'disabled';
+ options.audio.enabled = !disabled;
+ options.audio.volume = options.general.audioVolume;
+ options.audio.autoPlay = options.general.autoPlayAudio;
+ options.audio.sources = [disabled ? 'jpod101' : oldAudioSource];
+
+ delete options.general.audioSource;
+ delete options.general.audioVolume;
+ delete options.general.autoPlayAudio;
}
];
@@ -247,9 +259,6 @@ function profileOptionsCreateDefaults() {
return {
general: {
enable: true,
- audioSource: 'jpod101',
- audioVolume: 100,
- autoPlayAudio: false,
resultOutputMode: 'group',
debugInfo: false,
maxResults: 32,
@@ -270,6 +279,14 @@ function profileOptionsCreateDefaults() {
customPopupCss: ''
},
+ audio: {
+ enabled: true,
+ sources: ['jpod101', 'jpod101-alternate', 'jisho', 'custom'],
+ volume: 100,
+ autoPlay: false,
+ customSourceUrl: ''
+ },
+
scanning: {
middleMouse: true,
touchInputEnabled: true,
@@ -402,7 +419,7 @@ function optionsLoad() {
chrome.storage.local.get(['options'], store => {
const error = chrome.runtime.lastError;
if (error) {
- reject(error);
+ reject(new Error(error));
} else {
resolve(store.options);
}
@@ -431,7 +448,7 @@ function optionsSave(options) {
chrome.storage.local.set({options: JSON.stringify(options)}, () => {
const error = chrome.runtime.lastError;
if (error) {
- reject(error);
+ reject(new Error(error));
} else {
resolve();
}
diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js
index e4359863..3afc1506 100644
--- a/ext/bg/js/request.js
+++ b/ext/bg/js/request.js
@@ -22,7 +22,7 @@ function requestJson(url, action, params) {
const xhr = new XMLHttpRequest();
xhr.overrideMimeType('application/json');
xhr.addEventListener('load', () => resolve(xhr.responseText));
- xhr.addEventListener('error', () => reject('Failed to connect'));
+ xhr.addEventListener('error', () => reject(new Error('Failed to connect')));
xhr.open(action, url);
if (params) {
xhr.send(JSON.stringify(params));
@@ -34,7 +34,7 @@ function requestJson(url, action, params) {
return JSON.parse(responseText);
}
catch (e) {
- return Promise.reject('Invalid response');
+ return Promise.reject(new Error('Invalid response'));
}
});
}
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index faec29ef..0c1a61ea 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -26,10 +26,8 @@ async function searchFrontendSetup() {
if (!options.scanning.enableOnSearchPage) { return; }
const scriptSrcs = [
- '/fg/js/api.js',
'/fg/js/frontend-api-receiver.js',
'/fg/js/popup.js',
- '/fg/js/util.js',
'/fg/js/popup-proxy-host.js',
'/fg/js/frontend.js'
];
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index 13ed1e08..ead9ba6f 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -29,7 +29,8 @@ class DisplaySearch extends Display {
this.search = document.querySelector('#search');
this.query = document.querySelector('#query');
this.intro = document.querySelector('#intro');
- this.introHidden = false;
+ this.introVisible = true;
+ this.introAnimationTimer = null;
this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract});
@@ -38,12 +39,21 @@ class DisplaySearch extends Display {
}
if (this.query !== null) {
this.query.addEventListener('input', () => this.onSearchInput(), false);
+
+ const query = DisplaySearch.getSearchQueryFromLocation(window.location.href);
+ if (query !== null) {
+ this.query.value = window.wanakana.toKana(query);
+ this.onSearchQueryUpdated(query, false);
+ }
+
window.wanakana.bind(this.query);
}
+
+ this.updateSearchButton();
}
onError(error) {
- window.alert(`Error: ${error.toString ? error.toString() : error}`);
+ logError(error, true);
}
onSearchClear() {
@@ -56,41 +66,102 @@ class DisplaySearch extends Display {
}
onSearchInput() {
- this.search.disabled = (this.query === null || this.query.value.length === 0);
+ this.updateSearchButton();
}
- async onSearch(e) {
+ onSearch(e) {
if (this.query === null) {
return;
}
+ e.preventDefault();
+
+ const query = this.query.value;
+ const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : '';
+ window.history.replaceState(null, '', `${window.location.pathname}${queryString}`);
+ this.onSearchQueryUpdated(query, true);
+ }
+
+ async onSearchQueryUpdated(query, animate) {
try {
- e.preventDefault();
- this.hideIntro();
- const {length, definitions} = await apiTermsFind(this.query.value, this.optionsContext);
- super.termsShow(definitions, await apiOptionsGet(this.optionsContext));
+ const valid = (query.length > 0);
+ this.setIntroVisible(!valid, animate);
+ this.updateSearchButton();
+ if (valid) {
+ const {definitions} = await apiTermsFind(query, this.optionsContext);
+ this.termsShow(definitions, await apiOptionsGet(this.optionsContext));
+ } else {
+ this.container.textContent = '';
+ }
} catch (e) {
this.onError(e);
}
}
- hideIntro() {
- if (this.introHidden) {
+ setIntroVisible(visible, animate) {
+ if (this.introVisible === visible) {
return;
}
- this.introHidden = true;
+ this.introVisible = visible;
if (this.intro === null) {
return;
}
- const size = this.intro.getBoundingClientRect();
- this.intro.style.height = `${size.height}px`;
- this.intro.style.transition = 'height 0.4s ease-in-out 0s';
- window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation
+ if (this.introAnimationTimer !== null) {
+ clearTimeout(this.introAnimationTimer);
+ this.introAnimationTimer = null;
+ }
+
+ if (visible) {
+ this.showIntro(animate);
+ } else {
+ this.hideIntro(animate);
+ }
+ }
+
+ showIntro(animate) {
+ if (animate) {
+ const duration = 0.4;
+ this.intro.style.transition = '';
+ this.intro.style.height = '';
+ const size = this.intro.getBoundingClientRect();
+ 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`;
+ this.introAnimationTimer = setTimeout(() => {
+ this.intro.style.height = '';
+ this.introAnimationTimer = null;
+ }, duration * 1000);
+ } else {
+ this.intro.style.transition = '';
+ this.intro.style.height = '';
+ }
+ }
+
+ hideIntro(animate) {
+ if (animate) {
+ const duration = 0.4;
+ const size = this.intro.getBoundingClientRect();
+ this.intro.style.height = `${size.height}px`;
+ 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
+ } else {
+ this.intro.style.transition = '';
+ }
this.intro.style.height = '0';
}
+
+ updateSearchButton() {
+ this.search.disabled = this.introVisible && (this.query === null || this.query.value.length === 0);
+ }
+
+ static getSearchQueryFromLocation(url) {
+ let match = /^[^\?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
+ return match !== null ? decodeURIComponent(match[1]) : null;
+ }
}
window.yomichan_search = new DisplaySearch();
diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js
index 9838ea02..f3b5ff16 100644
--- a/ext/bg/js/settings.js
+++ b/ext/bg/js/settings.js
@@ -26,10 +26,7 @@ async function formRead(options) {
options.general.showGuide = $('#show-usage-guide').prop('checked');
options.general.compactTags = $('#compact-tags').prop('checked');
options.general.compactGlossaries = $('#compact-glossaries').prop('checked');
- options.general.autoPlayAudio = $('#auto-play-audio').prop('checked');
options.general.resultOutputMode = $('#result-output-mode').val();
- options.general.audioSource = $('#audio-playback-source').val();
- options.general.audioVolume = parseFloat($('#audio-playback-volume').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);
@@ -44,6 +41,11 @@ async function formRead(options) {
options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10);
options.general.customPopupCss = $('#custom-popup-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.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked');
options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked');
options.scanning.selectText = $('#select-matched-text').prop('checked');
@@ -92,10 +94,7 @@ async function formWrite(options) {
$('#show-usage-guide').prop('checked', options.general.showGuide);
$('#compact-tags').prop('checked', options.general.compactTags);
$('#compact-glossaries').prop('checked', options.general.compactGlossaries);
- $('#auto-play-audio').prop('checked', options.general.autoPlayAudio);
$('#result-output-mode').val(options.general.resultOutputMode);
- $('#audio-playback-source').val(options.general.audioSource);
- $('#audio-playback-volume').val(options.general.audioVolume);
$('#show-debug-info').prop('checked', options.general.debugInfo);
$('#show-advanced-options').prop('checked', options.general.showAdvanced);
$('#max-displayed-results').val(options.general.maxResults);
@@ -110,6 +109,11 @@ async function formWrite(options) {
$('#popup-vertical-offset2').val(options.general.popupVerticalOffset2);
$('#custom-popup-css').val(options.general.customPopupCss);
+ $('#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);
+
$('#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);
@@ -154,7 +158,7 @@ function formSetupEventListeners() {
$('#dict-file-button').click(onDictionaryImportButtonClick);
$('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
- $('input, select, textarea').not('.anki-model').not('.profile-form *').change(utilAsync(onFormOptionsChanged));
+ $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged));
$('.anki-model').change(utilAsync(onAnkiModelChanged));
}
@@ -244,6 +248,7 @@ async function onReady() {
showExtensionInformation();
formSetupEventListeners();
+ await audioSettingsInitialize();
await profileOptionsSetup();
storageInfoInitialize();
@@ -255,6 +260,20 @@ $(document).ready(utilAsync(onReady));
/*
+ * 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();
+}
+
+
+/*
* Remote options updates
*/
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index e12d1bf3..c61f5d7f 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -61,7 +61,7 @@ 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.png\" 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\">"
+ 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 : "")
@@ -85,9 +85,9 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ ((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 : "")
+ "</div>\n";
},"11":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" 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-kanji.png\" title=\"Add Kanji (Alt + K)\" alt></a>\n";
+ 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.png\" title=\"Source term (Alt + B)\" alt></a>\n";
+ 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;
@@ -290,7 +290,7 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(23, 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(25, 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(28, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
- + " <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n"
+ + " <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(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(49, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
@@ -302,15 +302,15 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(65, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</div>\n";
},"23":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" 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.png\" 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.png\" title=\"Add reading (Alt + R)\" alt></a>\n";
+ 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";
},"25":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"26":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio (Alt + P)\" alt></a>\n";
+ return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n";
},"28":function(container,depth0,helpers,partials,data) {
- return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" title=\"Source term (Alt + B)\" alt></a>\n";
+ return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n";
},"30":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
@@ -342,7 +342,7 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "");
},"35":function(container,depth0,helpers,partials,data) {
- return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio\" alt></a>";
+ return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>";
},"37":function(container,depth0,helpers,partials,data) {
var stack1;
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 65d746ea..601ee30c 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -238,8 +238,10 @@ class Translator {
const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles);
for (const definition of definitions) {
+ const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);
for (const deinflection of uniqueDeinflectionArrays[definition.index]) {
- if (Translator.definitionContainsAnyRule(definition, deinflection.rules)) {
+ const deinflectionRules = deinflection.rules;
+ if (deinflectionRules === 0 || (definitionRules & deinflectionRules) !== 0) {
deinflection.definitions.push(definition);
}
}
@@ -248,19 +250,6 @@ class Translator {
return deinflections.filter(e => e.definitions.length > 0);
}
- static definitionContainsAnyRule(definition, rules) {
- if (rules.length === 0) {
- return true;
- }
- const definitionRules = definition.rules;
- for (const rule of rules) {
- if (definitionRules.includes(rule)) {
- return true;
- }
- }
- return false;
- }
-
getDeinflections(text) {
const deinflections = [];
diff --git a/ext/bg/lang/deinflect.json b/ext/bg/lang/deinflect.json
index c7977c88..682093e1 100644
--- a/ext/bg/lang/deinflect.json
+++ b/ext/bg/lang/deinflect.json
@@ -1186,7 +1186,7 @@
"kanaIn": "て",
"kanaOut": "る",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v1",
@@ -1197,7 +1197,7 @@
"kanaIn": "いて",
"kanaOut": "く",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1207,7 +1207,7 @@
"kanaIn": "いで",
"kanaOut": "ぐ",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1217,7 +1217,7 @@
"kanaIn": "きて",
"kanaOut": "くる",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"vk"
@@ -1227,7 +1227,7 @@
"kanaIn": "くて",
"kanaOut": "い",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"adj-i"
@@ -1237,7 +1237,7 @@
"kanaIn": "して",
"kanaOut": "す",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1247,7 +1247,7 @@
"kanaIn": "して",
"kanaOut": "する",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"vs"
@@ -1257,7 +1257,7 @@
"kanaIn": "って",
"kanaOut": "う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1267,7 +1267,7 @@
"kanaIn": "って",
"kanaOut": "つ",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1277,7 +1277,7 @@
"kanaIn": "って",
"kanaOut": "る",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1287,7 +1287,7 @@
"kanaIn": "んで",
"kanaOut": "ぬ",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1297,7 +1297,7 @@
"kanaIn": "んで",
"kanaOut": "ぶ",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1307,7 +1307,7 @@
"kanaIn": "んで",
"kanaOut": "む",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1317,7 +1317,7 @@
"kanaIn": "のたもうて",
"kanaOut": "のたまう",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1327,7 +1327,7 @@
"kanaIn": "いって",
"kanaOut": "いく",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1337,7 +1337,7 @@
"kanaIn": "おうて",
"kanaOut": "おう",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1347,7 +1347,7 @@
"kanaIn": "こうて",
"kanaOut": "こう",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1357,7 +1357,7 @@
"kanaIn": "そうて",
"kanaOut": "そう",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1367,7 +1367,7 @@
"kanaIn": "とうて",
"kanaOut": "とう",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1377,7 +1377,7 @@
"kanaIn": "行って",
"kanaOut": "行く",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1387,7 +1387,7 @@
"kanaIn": "逝って",
"kanaOut": "逝く",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1397,7 +1397,7 @@
"kanaIn": "往って",
"kanaOut": "往く",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1407,7 +1407,7 @@
"kanaIn": "請うて",
"kanaOut": "請う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1417,7 +1417,7 @@
"kanaIn": "乞うて",
"kanaOut": "乞う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1427,7 +1427,7 @@
"kanaIn": "恋うて",
"kanaOut": "恋う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1437,7 +1437,7 @@
"kanaIn": "問うて",
"kanaOut": "問う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1447,7 +1447,7 @@
"kanaIn": "負うて",
"kanaOut": "負う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1457,7 +1457,7 @@
"kanaIn": "沿うて",
"kanaOut": "沿う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1467,7 +1467,7 @@
"kanaIn": "添うて",
"kanaOut": "添う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1477,7 +1477,7 @@
"kanaIn": "副うて",
"kanaOut": "副う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
@@ -1487,21 +1487,11 @@
"kanaIn": "厭うて",
"kanaOut": "厭う",
"rulesIn": [
- "iru"
+ "iru"
],
"rulesOut": [
"v5"
]
- },
- {
- "kanaIn": "で",
- "kanaOut": "",
- "rulesIn": [
- "iru"
- ],
- "rulesOut": [
- "neg-de"
- ]
}
],
"-zu": [
@@ -2233,8 +2223,7 @@
"kanaIn": "ない",
"kanaOut": "る",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v1",
@@ -2245,8 +2234,7 @@
"kanaIn": "かない",
"kanaOut": "く",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2256,8 +2244,7 @@
"kanaIn": "がない",
"kanaOut": "ぐ",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2267,8 +2254,7 @@
"kanaIn": "くない",
"kanaOut": "い",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"adj-i"
@@ -2278,8 +2264,7 @@
"kanaIn": "こない",
"kanaOut": "くる",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"vk"
@@ -2289,8 +2274,7 @@
"kanaIn": "さない",
"kanaOut": "す",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2300,8 +2284,7 @@
"kanaIn": "しない",
"kanaOut": "する",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"vs"
@@ -2311,8 +2294,7 @@
"kanaIn": "たない",
"kanaOut": "つ",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2322,8 +2304,7 @@
"kanaIn": "なない",
"kanaOut": "ぬ",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2333,8 +2314,7 @@
"kanaIn": "ばない",
"kanaOut": "ぶ",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2344,8 +2324,7 @@
"kanaIn": "まない",
"kanaOut": "む",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2355,8 +2334,7 @@
"kanaIn": "らない",
"kanaOut": "る",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -2366,8 +2344,7 @@
"kanaIn": "わない",
"kanaOut": "う",
"rulesIn": [
- "adj-i",
- "neg-de"
+ "adj-i"
],
"rulesOut": [
"v5"
@@ -3681,8 +3658,8 @@
],
"progressive or perfect": [
{
- "kanaIn": "いる",
- "kanaOut": "",
+ "kanaIn": "ている",
+ "kanaOut": "て",
"rulesIn": [
"v1"
],
@@ -3691,8 +3668,8 @@
]
},
{
- "kanaIn": "る",
- "kanaOut": "",
+ "kanaIn": "ておる",
+ "kanaOut": "て",
"rulesIn": [
"v1"
],
@@ -3701,14 +3678,54 @@
]
},
{
- "kanaIn": "おる",
- "kanaOut": "",
+ "kanaIn": "てる",
+ "kanaOut": "て",
+ "rulesIn": [
+ "v1"
+ ],
+ "rulesOut": [
+ "iru"
+ ]
+ },
+ {
+ "kanaIn": "でいる",
+ "kanaOut": "で",
+ "rulesIn": [
+ "v1"
+ ],
+ "rulesOut": [
+ "iru"
+ ]
+ },
+ {
+ "kanaIn": "でおる",
+ "kanaOut": "で",
+ "rulesIn": [
+ "v1"
+ ],
+ "rulesOut": [
+ "iru"
+ ]
+ },
+ {
+ "kanaIn": "とる",
+ "kanaOut": "て",
"rulesIn": [
"v1"
],
"rulesOut": [
"iru"
]
+ },
+ {
+ "kanaIn": "ないでいる",
+ "kanaOut": "ない",
+ "rulesIn": [
+ "v1"
+ ],
+ "rulesOut": [
+ "adj-i"
+ ]
}
]
}
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 668b2436..3284ed43 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -20,7 +20,7 @@
<form class="input-group" style="padding-top: 10px;">
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
<span class="input-group-btn">
- <input type="submit" class="btn btn-default form-control" id="search" value="Search" disabled>
+ <input type="submit" class="btn btn-default form-control" id="search" value="Search">
</span>
</form>
@@ -36,14 +36,14 @@
<script src="/mixed/js/extension.js"></script>
- <script src="/bg/js/api.js"></script>
- <script src="/bg/js/audio.js"></script>
<script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/templates.js"></script>
- <script src="/bg/js/util.js"></script>
+ <script src="/fg/js/api.js"></script>
<script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script>
+ <script src="/fg/js/util.js"></script>
+ <script src="/mixed/js/audio.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/japanese.js"></script>
<script src="/mixed/js/scroll.js"></script>
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 0bc5e14c..e4710283 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -14,7 +14,7 @@
<h1>Yomichan Options</h1>
</div>
- <div class="profile-form">
+ <div class="profile-form ignore-form-changes">
<h3>Profiles</h3>
<p class="help-block">
@@ -141,10 +141,6 @@
</div>
<div class="checkbox">
- <label><input type="checkbox" id="auto-play-audio"> Play audio automatically</label>
- </div>
-
- <div class="checkbox">
<label><input type="checkbox" id="show-advanced-options"> Show advanced options</label>
</div>
@@ -162,16 +158,6 @@
</div>
<div class="form-group">
- <label for="audio-playback-source">Audio playback source</label>
- <select class="form-control" id="audio-playback-source">
- <option value="disabled">Disabled</option>
- <option value="jpod101">JapanesePod101</option>
- <option value="jpod101-alternate">JapanesePod101 (alternate)</option>
- <option value="jisho">Jisho.org</option>
- </select>
- </div>
-
- <div class="form-group">
<label for="popup-display-mode">Popup display mode</label>
<select class="form-control" id="popup-display-mode">
<option value="default">Default</option>
@@ -180,11 +166,6 @@
</div>
<div class="form-group options-advanced">
- <label for="audio-playback-volume">Audio playback volume <span class="label-light">(percent)</span></label>
- <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
- </div>
-
- <div class="form-group options-advanced">
<label for="max-displayed-results">Maximum displayed results</label>
<input type="number" min="1" id="max-displayed-results" class="form-control">
</div>
@@ -257,6 +238,47 @@
</div>
<div>
+ <h3>Audio Options</h3>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="audio-playback-enabled"> Enable audio playback in search results</label>
+ </div>
+
+ <div class="checkbox">
+ <label><input type="checkbox" id="auto-play-audio"> Play audio automatically</label>
+ </div>
+
+ <div class="form-group">
+ <label for="audio-playback-volume">Audio playback volume <span class="label-light">(percent)</span></label>
+ <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control">
+ </div>
+
+ <div class="form-group options-advanced">
+ <label for="audio-custom-source">Custom audio source <span class="label-light">(URL)</span></label>
+ <input type="text" id="audio-custom-source" class="form-control" placeholder="Example: http://localhost/audio.mp3?expression={expression}&reading={reading}">
+ </div>
+
+ <div class="form-group ignore-form-changes">
+ <label>Audio playback sources</label>
+ <div class="audio-source-list"></div>
+ <div class="input-group audio-source-options">
+ <button class="btn btn-default audio-source-add" title="Add audio playback source"><span class="glyphicon glyphicon-plus"></span></button>
+ </div>
+
+ <template id="audio-source-template"><div class="input-group audio-source">
+ <div class="input-group-addon audio-source-prefix"></div>
+ <select class="form-control audio-source-select">
+ <option value="jpod101">JapanesePod101</option>
+ <option value="jpod101-alternate">JapanesePod101 (alternate)</option>
+ <option value="jisho">Jisho.org</option>
+ <option value="custom">Custom</option>
+ </select>
+ <div class="input-group-btn"><button class="btn btn-danger audio-source-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div>
+ </div></template>
+ </div>
+ </div>
+
+ <div>
<h3>Scanning Options</h3>
<div class="checkbox">
@@ -574,6 +596,7 @@
<script src="/bg/js/anki.js"></script>
<script src="/bg/js/api.js"></script>
+ <script src="/bg/js/audio-ui.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/conditions-ui.js"></script>
<script src="/bg/js/dictionary.js"></script>