aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-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
-rw-r--r--ext/fg/float.html1
-rw-r--r--ext/fg/js/api.js4
-rw-r--r--ext/fg/js/float.js2
-rw-r--r--ext/fg/js/frontend-api-receiver.js5
-rw-r--r--ext/fg/js/frontend-api-sender.js8
-rw-r--r--ext/fg/js/frontend.js81
-rw-r--r--ext/fg/js/popup-proxy-host.js8
-rw-r--r--ext/fg/js/popup-proxy.js6
-rw-r--r--ext/fg/js/popup.js49
-rw-r--r--ext/fg/js/util.js8
-rw-r--r--ext/manifest.json2
-rw-r--r--ext/mixed/img/add-kanji.pngbin733 -> 0 bytes
-rw-r--r--ext/mixed/img/add-term-kana.pngbin286 -> 0 bytes
-rw-r--r--ext/mixed/img/add-term-kana.svg23
-rw-r--r--ext/mixed/img/add-term-kanji.pngbin733 -> 0 bytes
-rw-r--r--ext/mixed/img/add-term-kanji.svg24
-rw-r--r--ext/mixed/img/entry-current.pngbin743 -> 0 bytes
-rw-r--r--ext/mixed/img/entry-current.svg16
-rw-r--r--ext/mixed/img/play-audio.pngbin610 -> 0 bytes
-rw-r--r--ext/mixed/img/play-audio.svg27
-rw-r--r--ext/mixed/img/source-term.pngbin680 -> 0 bytes
-rw-r--r--ext/mixed/img/source-term.svg31
-rw-r--r--ext/mixed/img/view-note.pngbin622 -> 0 bytes
-rw-r--r--ext/mixed/img/view-note.svg22
-rw-r--r--ext/mixed/js/audio.js60
-rw-r--r--ext/mixed/js/display.js107
-rw-r--r--ext/mixed/js/extension.js35
47 files changed, 1067 insertions, 436 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>
diff --git a/ext/fg/float.html b/ext/fg/float.html
index 52c7faa3..fe1aee8f 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -39,6 +39,7 @@
<script src="/fg/js/util.js"></script>
<script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script>
+ <script src="/mixed/js/audio.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/scroll.js"></script>
diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js
index d0ac649a..a553e514 100644
--- a/ext/fg/js/api.js
+++ b/ext/fg/js/api.js
@@ -45,8 +45,8 @@ function apiTemplateRender(template, data, dynamic) {
return utilInvoke('templateRender', {data, template, dynamic});
}
-function apiAudioGetUrl(definition, source) {
- return utilInvoke('audioGetUrl', {definition, source});
+function apiAudioGetUrl(definition, source, optionsContext) {
+ return utilInvoke('audioGetUrl', {definition, source, optionsContext});
}
function apiCommandExec(command) {
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 88842eef..8fdb6925 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -37,7 +37,7 @@ class DisplayFloat extends Display {
if (window.yomichan_orphaned) {
this.onOrphaned();
} else {
- window.alert(`Error: ${error.toString ? error.toString() : error}`);
+ logError(error, true);
}
}
diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js
index 687e5c3c..fbfb3ab0 100644
--- a/ext/fg/js/frontend-api-receiver.js
+++ b/ext/fg/js/frontend-api-receiver.js
@@ -46,9 +46,8 @@ class FrontendApiReceiver {
result => {
this.sendResult(port, id, senderId, {result});
},
- e => {
- const error = typeof e.toString === 'function' ? e.toString() : e;
- this.sendResult(port, id, senderId, {error});
+ error => {
+ this.sendResult(port, id, senderId, {error: errorToJson(error)});
});
}
diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js
index 2e037e62..c6eeaeb2 100644
--- a/ext/fg/js/frontend-api-sender.js
+++ b/ext/fg/js/frontend-api-sender.js
@@ -31,7 +31,7 @@ class FrontendApiSender {
invoke(action, params, target) {
if (this.disconnected) {
- return Promise.reject('Disconnected');
+ return Promise.reject(new Error('Disconnected'));
}
if (this.port === null) {
@@ -110,8 +110,8 @@ class FrontendApiSender {
clearTimeout(info.timer);
info.timer = null;
- if (typeof data.error === 'string') {
- info.reject(data.error);
+ if (typeof data.error !== 'undefined') {
+ info.reject(jsonToError(data.error));
} else {
info.resolve(data.result);
}
@@ -122,7 +122,7 @@ class FrontendApiSender {
const info = this.callbacks[id];
delete this.callbacks[id];
info.timer = null;
- info.reject(reason);
+ info.reject(new Error(reason));
}
static generateId(length) {
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 58dc0e4a..88cb93a9 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -38,6 +38,9 @@ class Frontend {
this.mouseDownPrevent = false;
this.clickPrevent = false;
this.scrollPrevent = false;
+
+ this.enabled = false;
+ this.eventListeners = [];
}
static create() {
@@ -53,23 +56,7 @@ class Frontend {
async prepare() {
try {
- this.options = await apiOptionsGet(this.getOptionsContext());
-
- window.addEventListener('message', this.onWindowMessage.bind(this));
- window.addEventListener('mousedown', this.onMouseDown.bind(this));
- window.addEventListener('mousemove', this.onMouseMove.bind(this));
- window.addEventListener('mouseover', this.onMouseOver.bind(this));
- window.addEventListener('mouseout', this.onMouseOut.bind(this));
- window.addEventListener('resize', this.onResize.bind(this));
-
- if (this.options.scanning.touchInputEnabled) {
- window.addEventListener('click', this.onClick.bind(this));
- window.addEventListener('touchstart', this.onTouchStart.bind(this));
- window.addEventListener('touchend', this.onTouchEnd.bind(this));
- window.addEventListener('touchcancel', this.onTouchCancel.bind(this));
- window.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false});
- window.addEventListener('contextmenu', this.onContextMenu.bind(this));
- }
+ await this.updateOptions();
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
} catch (e) {
@@ -88,7 +75,6 @@ class Frontend {
if (
this.pendingLookup ||
- !this.options.general.enable ||
(e.buttons & 0x1) !== 0x0 // Left mouse button
) {
return;
@@ -145,7 +131,7 @@ class Frontend {
}
onResize() {
- this.searchClear(true);
+ this.searchClear(false);
}
onClick(e) {
@@ -242,16 +228,59 @@ class Frontend {
}
onError(error) {
- console.log(error);
+ logError(error, false);
}
- async updateOptions() {
- this.options = await apiOptionsGet(this.getOptionsContext());
- if (!this.options.enable) {
+ setEnabled(enabled) {
+ if (enabled) {
+ if (!this.enabled) {
+ this.hookEvents();
+ this.enabled = true;
+ }
+ } else {
+ if (this.enabled) {
+ this.clearEventListeners();
+ this.enabled = false;
+ }
this.searchClear(false);
}
}
+ hookEvents() {
+ this.addEventListener(window, 'message', this.onWindowMessage.bind(this));
+ this.addEventListener(window, 'mousedown', this.onMouseDown.bind(this));
+ this.addEventListener(window, 'mousemove', this.onMouseMove.bind(this));
+ this.addEventListener(window, 'mouseover', this.onMouseOver.bind(this));
+ this.addEventListener(window, 'mouseout', this.onMouseOut.bind(this));
+ this.addEventListener(window, 'resize', this.onResize.bind(this));
+
+ if (this.options.scanning.touchInputEnabled) {
+ this.addEventListener(window, 'click', this.onClick.bind(this));
+ this.addEventListener(window, 'touchstart', this.onTouchStart.bind(this));
+ this.addEventListener(window, 'touchend', this.onTouchEnd.bind(this));
+ this.addEventListener(window, 'touchcancel', this.onTouchCancel.bind(this));
+ this.addEventListener(window, 'touchmove', this.onTouchMove.bind(this), {passive: false});
+ this.addEventListener(window, 'contextmenu', this.onContextMenu.bind(this));
+ }
+ }
+
+ 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 = [];
+ }
+
+ async updateOptions() {
+ this.options = await apiOptionsGet(this.getOptionsContext());
+ this.setEnabled(this.options.general.enable);
+ }
+
popupTimerSet(callback) {
const delay = this.options.scanning.delay;
if (delay > 0) {
@@ -452,7 +481,7 @@ class Frontend {
searchFromTouch(x, y, cause) {
this.popupTimerClear();
- if (!this.options.general.enable || this.pendingLookup) {
+ if (this.pendingLookup) {
return;
}
@@ -527,8 +556,8 @@ Frontend.runtimeMessageHandlers = {
self.updateOptions();
},
- popupSetVisible: (self, {visible}) => {
- self.popup.setVisible(visible);
+ popupSetVisibleOverride: (self, {visible}) => {
+ self.popup.setVisibleOverride(visible);
}
};
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index cb9741be..f933639c 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -41,7 +41,7 @@ class PopupProxyHost {
show: ({id, elementRect, options}) => this.show(id, elementRect, options),
showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options),
hide: ({id, changeFocus}) => this.hide(id, changeFocus),
- setVisible: ({id, visible}) => this.setVisible(id, visible),
+ setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible),
containsPoint: ({id, x, y}) => this.containsPoint(id, x, y),
termsShow: ({id, elementRect, writingMode, definitions, options, context}) => this.termsShow(id, elementRect, writingMode, definitions, options, context),
kanjiShow: ({id, elementRect, writingMode, definitions, options, context}) => this.kanjiShow(id, elementRect, writingMode, definitions, options, context),
@@ -69,7 +69,7 @@ class PopupProxyHost {
getPopup(id) {
if (!this.popups.hasOwnProperty(id)) {
- throw 'Invalid popup ID';
+ throw new Error('Invalid popup ID');
}
return this.popups[id];
@@ -103,9 +103,9 @@ class PopupProxyHost {
return popup.hide(changeFocus);
}
- async setVisible(id, visible) {
+ async setVisibleOverride(id, visible) {
const popup = this.getPopup(id);
- return popup.setVisible(visible);
+ return popup.setVisibleOverride(visible);
}
async containsPoint(id, x, y) {
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 072cebc9..efbd28b2 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -65,9 +65,9 @@ class PopupProxy {
return await this.invokeHostApi('hide', {id: this.id, changeFocus});
}
- async setVisible(visible) {
+ async setVisibleOverride(visible) {
const id = await this.getPopupId();
- return await this.invokeHostApi('setVisible', {id, visible});
+ return await this.invokeHostApi('setVisibleOverride', {id, visible});
}
async containsPoint(x, y) {
@@ -98,7 +98,7 @@ class PopupProxy {
invokeHostApi(action, params={}) {
if (typeof this.parentFrameId !== 'number') {
- return Promise.reject('Invalid frame');
+ return Promise.reject(new Error('Invalid frame'));
}
return this.apiSender.invoke(action, params, `popup-proxy-host#${this.parentFrameId}`);
}
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 9dff6f28..9ca91afa 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -34,6 +34,9 @@ class Popup {
this.container.style.height = '0px';
this.injectPromise = null;
this.isInjected = false;
+ this.visible = false;
+ this.visibleOverride = null;
+ this.updateVisibility();
}
inject(options) {
@@ -105,9 +108,11 @@ class Popup {
container.style.top = `${y}px`;
container.style.width = `${width}px`;
container.style.height = `${height}px`;
- container.style.visibility = 'visible';
- this.hideChildren(true);
+ this.setVisible(true);
+ if (this.child !== null) {
+ this.child.hide(true);
+ }
}
static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) {
@@ -209,41 +214,35 @@ class Popup {
}
hide(changeFocus) {
- if (this.isContainerHidden()) {
- changeFocus = false;
+ if (!this.isVisible()) {
+ return;
+ }
+
+ this.setVisible(false);
+ if (this.child !== null) {
+ this.child.hide(false);
}
- this.hideChildren(changeFocus);
- this.hideContainer();
if (changeFocus) {
this.focusParent();
}
}
- hideChildren(changeFocus) {
- // Recursively hides all children.
- if (this.child !== null && !this.child.isContainerHidden()) {
- this.child.hide(changeFocus);
- }
- }
-
- hideContainer() {
- this.container.style.visibility = 'hidden';
+ isVisible() {
+ return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible);
}
- isContainerHidden() {
- return (this.container.style.visibility === 'hidden');
+ setVisible(visible) {
+ this.visible = visible;
+ this.updateVisibility();
}
- isVisible() {
- return this.isInjected && this.container.style.visibility !== 'hidden';
+ setVisibleOverride(visible) {
+ this.visibleOverride = visible;
+ this.updateVisibility();
}
- setVisible(visible) {
- if (visible) {
- this.container.style.setProperty('display', '');
- } else {
- this.container.style.setProperty('display', 'none', 'important');
- }
+ updateVisibility() {
+ this.container.style.setProperty('visibility', this.isVisible() ? 'visible' : 'hidden', 'important');
}
focusParent() {
diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js
index dc99274e..9a7968a7 100644
--- a/ext/fg/js/util.js
+++ b/ext/fg/js/util.js
@@ -30,19 +30,19 @@ function utilInvoke(action, params={}) {
chrome.runtime.sendMessage(data, (response) => {
utilCheckLastError(chrome.runtime.lastError);
if (response !== null && typeof response === 'object') {
- if (response.error) {
- reject(response.error);
+ if (typeof response.error !== 'undefined') {
+ reject(jsonToError(response.error));
} else {
resolve(response.result);
}
} else {
const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`;
- reject(`${message} (${JSON.stringify(data)})`);
+ reject(new Error(`${message} (${JSON.stringify(data)})`));
}
});
} catch (e) {
window.yomichan_orphaned = true;
- reject(e.message);
+ reject(e);
}
});
}
diff --git a/ext/manifest.json b/ext/manifest.json
index fe6e8e2b..c69b556f 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan (testing)",
- "version": "1.8.5",
+ "version": "1.8.8",
"description": "Japanese dictionary with Anki integration (testing)",
"icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
diff --git a/ext/mixed/img/add-kanji.png b/ext/mixed/img/add-kanji.png
deleted file mode 100644
index 6332fefe..00000000
--- a/ext/mixed/img/add-kanji.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/add-term-kana.png b/ext/mixed/img/add-term-kana.png
deleted file mode 100644
index 41ff8335..00000000
--- a/ext/mixed/img/add-term-kana.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/add-term-kana.svg b/ext/mixed/img/add-term-kana.svg
new file mode 100644
index 00000000..bb964476
--- /dev/null
+++ b/ext/mixed/img/add-term-kana.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient4719" x1="-1.7198" x2="-1.7198" y1="2.7781" y2="1.4552" gradientTransform="matrix(3.7795 0 0 3.7795 14 .5)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#8dba64" offset="0"/>
+ <stop stop-color="#b4d495" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient4745-5" x1="2.1167" x2="1.4552" y1="2.3812" y2="1.7198" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#376b19" offset="0"/>
+ <stop stop-color="#81ab61" offset="1"/>
+ </linearGradient>
+ <radialGradient id="radialGradient4770-4" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(2.2677 -7.9311e-7 7.9312e-7 2.2677 2.7 3.7)" gradientUnits="userSpaceOnUse">
+ <stop stop-opacity=".28986" offset="0"/>
+ <stop stop-opacity="0" offset="1"/>
+ </radialGradient>
+ </defs>
+ <g>
+ <circle cx="7.5" cy="8.5" r="3" fill="url(#linearGradient4719)"/>
+ <circle cx="7.5" cy="8.5" r="3" fill="none" stroke="url(#linearGradient4745-5)"/>
+ <circle cx="7.5" cy="8.5" r="1.5" fill="url(#radialGradient4770-4)"/>
+ <path d="m6 8h1v-1h1v1h1v1h-1v1h-1v-1h-1v-1" fill="#fff"/>
+ </g>
+</svg>
diff --git a/ext/mixed/img/add-term-kanji.png b/ext/mixed/img/add-term-kanji.png
deleted file mode 100644
index 6332fefe..00000000
--- a/ext/mixed/img/add-term-kanji.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/add-term-kanji.svg b/ext/mixed/img/add-term-kanji.svg
new file mode 100644
index 00000000..3737eaec
--- /dev/null
+++ b/ext/mixed/img/add-term-kanji.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient4582" x1="-1.7198" x2="-1.7198" y1="3.5719" y2=".79375" gradientTransform="matrix(3.7795 0 0 3.7795 14.5 -6.308e-7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#6fb558" offset="0"/>
+ <stop stop-color="#a5db9b" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient4758-7" x1="7.5406" x2="5.1594" y1="3.3073" y2=".92604" gradientTransform="matrix(3.7795 0 0 3.7795 -16 -6e-7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#34812c" offset="0"/>
+ <stop stop-color="#87b870" offset="1"/>
+ </linearGradient>
+ <radialGradient id="radialGradient4683-3" cx="2.1167" cy="2.1167" r=".66146" gradientTransform="matrix(4.5354 8.0301e-7 -8.0301e-7 4.5354 -1.6 -1.6)" gradientUnits="userSpaceOnUse">
+ <stop stop-opacity=".28986" offset="0"/>
+ <stop stop-opacity="0" offset="1"/>
+ </radialGradient>
+ </defs>
+ <g>
+ <circle cx="8" cy="8" r="6.5" fill="url(#linearGradient4582)"/>
+ <circle cx="8" cy="8" r="5.75" fill="none" stroke="#fff" stroke-opacity=".50196" stroke-width="1.5"/>
+ <circle cx="8" cy="8" r="6.5" fill="none" stroke="url(#linearGradient4758-7)"/>
+ <circle cx="8" cy="8" r="3" fill="url(#radialGradient4683-3)"/>
+ <path d="m5 7h2v-2h2v2h2v2h-2v2h-2v-2h-2v-2" fill="#fff"/>
+ </g>
+</svg>
diff --git a/ext/mixed/img/entry-current.png b/ext/mixed/img/entry-current.png
deleted file mode 100644
index bab7cc9b..00000000
--- a/ext/mixed/img/entry-current.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/entry-current.svg b/ext/mixed/img/entry-current.svg
new file mode 100644
index 00000000..abf3f76d
--- /dev/null
+++ b/ext/mixed/img/entry-current.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient4930" x1="3.175" x2="1.0583" y1="3.7042" y2=".52917" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#c47a00" offset="0"/>
+ <stop stop-color="#fcbf00" offset="1"/>
+ </linearGradient>
+ <radialGradient id="radialGradient4938" cx="2.1167" cy="2.1167" r="1.9976" gradientTransform="matrix(3.7795 0 0 3.7753 -5.7066e-7 .0088978)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#ffeeb5" offset="0"/>
+ <stop stop-color="#ffe079" offset="1"/>
+ </radialGradient>
+ </defs>
+ <g>
+ <path d="m10.25 9.375 3 4.5-2.25 1.5-3-4.5-3 4.5-2.25-1.5 3-4.5h-5.25v-2.75h5.25l-3-4.5 2.25-1.5 3 4.5 3-4.5 2.25 1.5-3 4.5h5.25v2.75z" fill="url(#radialGradient4938)" stroke="url(#linearGradient4930)" stroke-linejoin="bevel" stroke-opacity=".93333"/>
+ </g>
+</svg>
diff --git a/ext/mixed/img/play-audio.png b/ext/mixed/img/play-audio.png
deleted file mode 100644
index 6056d234..00000000
--- a/ext/mixed/img/play-audio.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/play-audio.svg b/ext/mixed/img/play-audio.svg
new file mode 100644
index 00000000..1d5e2d9c
--- /dev/null
+++ b/ext/mixed/img/play-audio.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient4825" x1="-.39687" x2="-1.1906" y1="3.2411" y2="1.1906" gradientTransform="matrix(3.7795 0 0 3.7795 8.5 -6.308e-7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#777772" offset="0"/>
+ <stop stop-color="#a9a9a9" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient4869-7" x1=".52917" x2=".52917" y1="2.6458" y2="1.4552" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#fff" stop-opacity="0" offset="0"/>
+ <stop stop-color="#fff" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient4853-9" x1="-.26458" x2="-1.5875" y1="3.4396" y2=".7276" gradientTransform="matrix(3.7795 0 0 3.7795 8.5 -2.9535e-7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#5d5d5a" offset="0"/>
+ <stop stop-color="#90908f" offset="1"/>
+ </linearGradient>
+ <radialGradient id="radialGradient4898-9" cx="15" cy="8.5578" r="3.7188" gradientTransform="matrix(2.4322e-8 -2.1513 1.6807 1.9002e-8 1.3671 40.269)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#e1293b" offset="0"/>
+ <stop stop-color="#e9505a" offset="1"/>
+ </radialGradient>
+ </defs>
+ <g>
+ <path d="m0.5 8v2.5h3.5l3.5 3v-11l-3.5 3h-3.5v2.5" fill="url(#linearGradient4825)"/>
+ <path d="m1.25 10.5v-4.25h3l3-2.75" fill="none" stroke="url(#linearGradient4869-7)" stroke-opacity=".50196" stroke-width="1.5"/>
+ <path d="m0.5 8v2.5h3.5l3.5 3v-11l-3.5 3h-3.5v2.5" fill="none" stroke="url(#linearGradient4853-9)" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="m12.75 2.75c1.5 3 1.5 7.5 0 10.5 4.25-1.75 4.25-8.75 0-10.5m-2 2c0.75 1.5 0.75 5 0 6.5 3-1.25 3-5.25 0-6.5m-0.75 1.5-1.5 1.75 1.5 1.75c0.5-1 0.5-2.5 0-3.5" fill="url(#radialGradient4898-9)"/>
+ </g>
+</svg>
diff --git a/ext/mixed/img/source-term.png b/ext/mixed/img/source-term.png
deleted file mode 100644
index 2e53c698..00000000
--- a/ext/mixed/img/source-term.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/source-term.svg b/ext/mixed/img/source-term.svg
new file mode 100644
index 00000000..a70938f2
--- /dev/null
+++ b/ext/mixed/img/source-term.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient5205" x2="-3.9688" y1="3.7042" y2="1.0583" gradientTransform="matrix(3.7795 0 0 3.7795 20.5 -.5)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#012e5b" offset="0"/>
+ <stop stop-color="#6399c6" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5138-3" x1="3.8365" x2=".39687" y1="3.0427" y2=".59531" gradientTransform="matrix(3.7795 0 0 3.7795 5 -6e-7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#012e5b" offset="0"/>
+ <stop stop-color="#6399c6" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5227-0" x1="2.6458" x2=".52917" y1="3.9688" y2="3.3734" gradientTransform="matrix(3.7795 0 0 3.7795 -5.7066e-7 -7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#7cbe76" offset="0"/>
+ <stop stop-color="#abd8a2" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5219-8" x1="2.7781" x2=".13229" y1="4.101" y2="3.175" gradientTransform="matrix(3.7795 0 0 3.7795 -5.7066e-7 -7)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#205e1d" offset="0"/>
+ <stop stop-color="#74c768" offset="1"/>
+ </linearGradient>
+ </defs>
+ <g>
+ <path d="m16 3.5h-1.5l-1 1h-1l-1-1h-6v10h5.75l1 1h1.5l1-1h1.25" fill="#5d8bb3" stroke="url(#linearGradient5205)" stroke-width="1px"/>
+ <path d="m16 2.25h-1.5l-1 1h-1l-1-1h-5v9.25h5l1 1h1l1-1h1.5" fill="#f8f8f8" stroke="url(#linearGradient5138-3)" stroke-width="1px"/>
+ <g fill="none" stroke-width="1px">
+ <path d="m15 8.75h1m-1-2h1m-1-2h1m-8 4h3m-3-2h3m-3-2h3" stroke="#bdbdbd"/>
+ <path d="m12.5 10.25v-4.75" stroke="#a6a6a6"/>
+ <path d="m13.5 5.75v4.25" stroke="#d8d8d8"/>
+ </g>
+ <path d="m10.5 7v-1.5h-7v-2l-3 3v1l3 3v-2h7v-1.5" fill="url(#linearGradient5227-0)" stroke="url(#linearGradient5219-8)" stroke-linejoin="round"/>
+ </g>
+</svg>
diff --git a/ext/mixed/img/view-note.png b/ext/mixed/img/view-note.png
deleted file mode 100644
index 7d863f94..00000000
--- a/ext/mixed/img/view-note.png
+++ /dev/null
Binary files differ
diff --git a/ext/mixed/img/view-note.svg b/ext/mixed/img/view-note.svg
new file mode 100644
index 00000000..3e6f1dce
--- /dev/null
+++ b/ext/mixed/img/view-note.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <linearGradient id="linearGradient5010" x2="-3.9688" y1="3.7042" y2="1.0583" gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#012e5b" offset="0"/>
+ <stop stop-color="#6399c6" offset="1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient5018-3" x1="3.8365" x2=".39687" y1="3.0427" y2=".59531" gradientTransform="scale(3.7795 3.7795)" gradientUnits="userSpaceOnUse">
+ <stop stop-color="#012e5b" offset="0"/>
+ <stop stop-color="#6399c6" offset="1"/>
+ </linearGradient>
+ </defs>
+ <g stroke-width="1px">
+ <path d="m9.75 13.5-1 1h-1.5l-1-1h-5.75v-10h6l1 1h1l1-1h6v10z" fill="#5d8bb3" stroke="url(#linearGradient5010)"/>
+ <path d="m14.5 11.5h-5l-1 1h-1l-1-1h-5v-9.25h5l1 1h1l1-1h5v9.25" fill="#f8f8f8" stroke="url(#linearGradient5018-3)"/>
+ <g fill="none">
+ <path d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3" stroke="#bdbdbd"/>
+ <path d="m7.5 10.25v-4.75" stroke="#a6a6a6"/>
+ <path d="m8.5 5.75v4.25" stroke="#d8d8d8"/>
+ </g>
+ </g>
+</svg>
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
new file mode 100644
index 00000000..b905140c
--- /dev/null
+++ b/ext/mixed/js/audio.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/>.
+ */
+
+
+function audioGetFromUrl(url) {
+ return new Promise((resolve, reject) => {
+ const audio = new Audio(url);
+ audio.addEventListener('loadeddata', () => {
+ if (audio.duration === 5.694694 || audio.duration === 5.720718) {
+ // Hardcoded values for invalid audio
+ reject(new Error('Could not retrieve audio'));
+ } else {
+ resolve(audio);
+ }
+ });
+ audio.addEventListener('error', () => reject(audio.error));
+ });
+}
+
+async function audioGetFromSources(expression, sources, optionsContext, createAudioObject, cache=null) {
+ const key = `${expression.expression}:${expression.reading}`;
+ if (cache !== null && cache.hasOwnProperty(expression)) {
+ return cache[key];
+ }
+
+ for (let i = 0, ii = sources.length; i < ii; ++i) {
+ const source = sources[i];
+ const url = await apiAudioGetUrl(expression, source, optionsContext);
+ if (url === null) {
+ continue;
+ }
+
+ try {
+ const audio = createAudioObject ? await audioGetFromUrl(url) : null;
+ const result = {audio, url, source};
+ if (cache !== null) {
+ cache[key] = result;
+ }
+ return result;
+ } catch (e) {
+ // NOP
+ }
+ }
+ return {audio: null, source: null};
+}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index dc64dbea..22181301 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -26,6 +26,8 @@ class Display {
this.context = null;
this.sequence = 0;
this.index = 0;
+ this.audioPlaying = null;
+ this.audioFallback = null;
this.audioCache = {};
this.optionsContext = {};
this.eventListeners = [];
@@ -39,11 +41,11 @@ class Display {
}
onError(error) {
- throw 'Override me';
+ throw new Error('Override me');
}
onSearchClear() {
- throw 'Override me';
+ throw new Error('Override me');
}
onSourceTermView(e) {
@@ -133,7 +135,7 @@ class Display {
const entry = link.closest('.entry');
const definitionIndex = this.entryIndexFind(entry);
const expressionIndex = Display.indexOf(entry.querySelectorAll('.expression .action-play-audio'), link);
- this.audioPlay(this.definitions[definitionIndex], expressionIndex);
+ this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex);
}
onNoteAdd(e) {
@@ -189,7 +191,7 @@ class Display {
addable: options.anki.enable,
grouped: options.general.resultOutputMode === 'group',
merged: options.general.resultOutputMode === 'merge',
- playback: options.general.audioSource !== 'disabled',
+ playback: options.audio.enabled,
compactGlossaries: options.general.compactGlossaries,
debug: options.general.debugInfo
};
@@ -209,7 +211,7 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- if (this.options.general.autoPlayAudio && this.options.general.audioSource !== 'disabled') {
+ if (this.options.audio.enabled && this.options.audio.autoPlay) {
this.autoPlayAudio();
}
@@ -274,7 +276,7 @@ class Display {
}
autoPlayAudio() {
- this.audioPlay(this.definitions[0], this.firstExpressionIndex);
+ this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
async adderButtonUpdate(modes, sequence) {
@@ -286,15 +288,23 @@ class Display {
for (let i = 0; i < states.length; ++i) {
const state = states[i];
+ let noteId = null;
for (const mode in state) {
const button = this.adderButtonFind(i, mode);
if (button === null) {
continue;
}
- button.classList.toggle('disabled', !state[mode]);
+ const info = state[mode];
+ if (!info.canAdd && noteId === null && info.noteId) {
+ noteId = info.noteId;
+ }
+ button.classList.toggle('disabled', !info.canAdd);
button.classList.remove('pending');
}
+ if (noteId !== null) {
+ this.viewerButtonShow(i, noteId);
+ }
}
} catch (e) {
this.onError(e);
@@ -380,13 +390,9 @@ class Display {
if (adderButton !== null) {
adderButton.classList.add('disabled');
}
- const viewerButton = this.viewerButtonFind(index);
- if (viewerButton !== null) {
- viewerButton.classList.remove('pending', 'disabled');
- viewerButton.dataset.noteId = noteId;
- }
+ this.viewerButtonShow(index, noteId);
} else {
- throw 'Note could note be added';
+ throw new Error('Note could not be added');
}
} catch (e) {
this.onError(e);
@@ -395,37 +401,44 @@ class Display {
}
}
- async audioPlay(definition, expressionIndex) {
+ async audioPlay(definition, expressionIndex, entryIndex) {
try {
this.setSpinnerVisible(true);
const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex];
- let url = await apiAudioGetUrl(expression, this.options.general.audioSource);
- if (!url) {
- url = '/mixed/mp3/button.mp3';
- }
- for (const key in this.audioCache) {
- this.audioCache[key].pause();
+ if (this.audioPlaying !== null) {
+ this.audioPlaying.pause();
+ this.audioPlaying = null;
}
- let audio = this.audioCache[url];
- if (audio) {
- audio.currentTime = 0;
- audio.volume = this.options.general.audioVolume / 100.0;
- audio.play();
+ const sources = this.options.audio.sources;
+ let {audio, source} = await audioGetFromSources(expression, sources, this.optionsContext, true, this.audioCache);
+ let info;
+ if (audio === null) {
+ if (this.audioFallback === null) {
+ this.audioFallback = new Audio('/mixed/mp3/button.mp3');
+ }
+ audio = this.audioFallback;
+ info = 'Could not find audio';
} else {
- audio = new Audio(url);
- audio.onloadeddata = () => {
- if (audio.duration === 5.694694 || audio.duration === 5.720718) {
- audio = new Audio('/mixed/mp3/button.mp3');
- }
+ info = `From source ${1 + sources.indexOf(source)}: ${source}`;
+ }
- this.audioCache[url] = audio;
- audio.volume = this.options.general.audioVolume / 100.0;
- audio.play();
- };
+ const button = this.audioButtonFindImage(entryIndex);
+ if (button !== null) {
+ let titleDefault = button.dataset.titleDefault;
+ if (!titleDefault) {
+ titleDefault = button.title || "";
+ button.dataset.titleDefault = titleDefault;
+ }
+ button.title = `${titleDefault}\n${info}`;
}
+
+ this.audioPlaying = audio;
+ audio.currentTime = 0;
+ audio.volume = this.options.audio.volume / 100.0;
+ audio.play();
} catch (e) {
this.onError(e);
} finally {
@@ -445,7 +458,7 @@ class Display {
async getScreenshot() {
try {
- await this.setPopupVisible(false);
+ await this.setPopupVisibleOverride(false);
await Display.delay(1); // Wait for popup to be hidden.
const {format, quality} = this.options.anki.screenshot;
@@ -454,7 +467,7 @@ class Display {
return {dataUrl, format};
} finally {
- await this.setPopupVisible(true);
+ await this.setPopupVisibleOverride(null);
}
}
@@ -462,8 +475,8 @@ class Display {
return this.options.general.resultOutputMode === 'merge' ? 0 : -1;
}
- setPopupVisible(visible) {
- return apiForward('popupSetVisible', {visible});
+ setPopupVisibleOverride(visible) {
+ return apiForward('popupSetVisibleOverride', {visible});
}
setSpinnerVisible(visible) {
@@ -504,6 +517,20 @@ class Display {
return entry !== null ? entry.querySelector('.action-view-note') : null;
}
+ viewerButtonShow(index, noteId) {
+ const viewerButton = this.viewerButtonFind(index);
+ if (viewerButton === null) {
+ return;
+ }
+ viewerButton.classList.remove('pending', 'disabled');
+ viewerButton.dataset.noteId = noteId;
+ }
+
+ audioButtonFindImage(index) {
+ const entry = this.getEntry(index);
+ return entry !== null ? entry.querySelector('.action-play-audio>img') : null;
+ }
+
static delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
@@ -539,7 +566,7 @@ class Display {
static getKeyFromEvent(event) {
const key = event.key;
- return key.length === 1 ? key.toUpperCase() : key;
+ return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
}
}
@@ -633,7 +660,7 @@ Display.onKeyDownHandlers = {
if (e.altKey) {
const entry = self.getEntry(self.index);
if (entry !== null && entry.dataset.type === 'term') {
- self.audioPlay(self.definitions[self.index], self.firstExpressionIndex);
+ self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index);
}
return true;
}
diff --git a/ext/mixed/js/extension.js b/ext/mixed/js/extension.js
index 5c803132..861e52a5 100644
--- a/ext/mixed/js/extension.js
+++ b/ext/mixed/js/extension.js
@@ -34,7 +34,7 @@ function toIterable(value) {
}
}
- throw 'Could not convert to iterable';
+ throw new Error('Could not convert to iterable');
}
function extensionHasChrome() {
@@ -53,6 +53,39 @@ function extensionHasBrowser() {
}
}
+function errorToJson(error) {
+ return {
+ name: error.name,
+ message: error.message,
+ stack: error.stack
+ };
+}
+
+function jsonToError(jsonError) {
+ const error = new Error(jsonError.message);
+ error.name = jsonError.name;
+ error.stack = jsonError.stack;
+ return error;
+}
+
+function logError(error, alert) {
+ const manifest = chrome.runtime.getManifest();
+ let errorMessage = `${manifest.name} v${manifest.version} has encountered an error.\n`;
+ errorMessage += `Originating URL: ${window.location.href}\n`;
+
+ const errorString = `${error.toString ? error.toString() : error}`;
+ const stack = `${error.stack}`.trimRight();
+ errorMessage += (!stack.startsWith(errorString) ? `${errorString}\n${stack}` : `${stack}`);
+
+ errorMessage += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues';
+
+ console.error(errorMessage);
+
+ if (alert) {
+ window.alert(`${errorString}\n\nCheck the developer console for more details.`);
+ }
+}
+
const EXTENSION_IS_BROWSER_EDGE = (
extensionHasBrowser() &&
(!extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined'))