diff options
Diffstat (limited to 'ext/bg/js')
-rw-r--r-- | ext/bg/js/anki.js | 31 | ||||
-rw-r--r-- | ext/bg/js/api.js | 29 | ||||
-rw-r--r-- | ext/bg/js/audio-ui.js | 131 | ||||
-rw-r--r-- | ext/bg/js/audio.js | 152 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 4 | ||||
-rw-r--r-- | ext/bg/js/database.js | 99 | ||||
-rw-r--r-- | ext/bg/js/deinflector.js | 73 | ||||
-rw-r--r-- | ext/bg/js/options.js | 27 | ||||
-rw-r--r-- | ext/bg/js/request.js | 4 | ||||
-rw-r--r-- | ext/bg/js/search-frontend.js | 2 | ||||
-rw-r--r-- | ext/bg/js/search.js | 101 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 33 | ||||
-rw-r--r-- | ext/bg/js/templates.js | 16 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 17 |
14 files changed, 500 insertions, 219 deletions
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 = []; |