From 3c48290cd83744983df2e708b892a8415bcf750f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 15:17:25 -0400 Subject: Add isExtensionUrl utility function to yomichan object --- ext/fg/js/popup.js | 11 +---------- ext/mixed/js/core.js | 9 +++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 42f08afa..99610e17 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -539,19 +539,10 @@ class Popup { }; } - static _isOnExtensionPage() { - try { - const url = chrome.runtime.getURL('/'); - return window.location.href.substring(0, url.length) === url; - } catch (e) { - // NOP - } - } - static async _injectStylesheet(id, type, value, useWebExtensionApi) { const injectedStylesheets = Popup._injectedStylesheets; - if (Popup._isOnExtensionPage()) { + if (yomichan.isExtensionUrl(window.location.href)) { // Permissions error will occur if trying to use the WebExtension API to inject // into an extension page. useWebExtensionApi = false; diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 2d11c11a..6a3298fc 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -316,6 +316,15 @@ const yomichan = (() => { this.trigger('orphaned', {error}); } + isExtensionUrl(url) { + try { + const urlBase = chrome.runtime.getURL('/'); + return url.substring(0, urlBase.length) === urlBase; + } catch (e) { + return false; + } + } + getTemporaryListenerResult(eventHandler, userCallback, timeout=null) { if (!( typeof eventHandler.addListener === 'function' && -- cgit v1.2.3 From 51e17b35e3a855c0db6c4be94a8cb416b14c8ad7 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 15:21:43 -0400 Subject: Convert some util* functions into api* functions --- ext/bg/js/backend.js | 45 +++++++++++++++++++++++++++++++++++++- ext/bg/js/settings/anki.js | 10 ++++----- ext/bg/js/settings/dictionaries.js | 12 +++++----- ext/bg/js/util.js | 29 ------------------------ ext/mixed/js/api.js | 24 ++++++++++++++++++++ 5 files changed, 79 insertions(+), 41 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index be8ea322..bc687a24 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -102,7 +102,13 @@ class Backend { ['getQueryParserTemplatesHtml', {handler: this._onApiGetQueryParserTemplatesHtml.bind(this), async: true}], ['getZoom', {handler: this._onApiGetZoom.bind(this), async: true}], ['getMessageToken', {handler: this._onApiGetMessageToken.bind(this), async: false}], - ['getDefaultAnkiFieldTemplates', {handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this), async: false}] + ['getDefaultAnkiFieldTemplates', {handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this), async: false}], + ['getAnkiDeckNames', {handler: this._onApiGetAnkiDeckNames.bind(this), async: true}], + ['getAnkiModelNames', {handler: this._onApiGetAnkiModelNames.bind(this), async: true}], + ['getAnkiModelFieldNames', {handler: this._onApiGetAnkiModelFieldNames.bind(this), async: true}], + ['getDictionaryInfo', {handler: this._onApiGetDictionaryInfo.bind(this), async: true}], + ['getDictionaryCounts', {handler: this._onApiGetDictionaryCounts.bind(this), async: true}], + ['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}] ]); this._commandHandlers = new Map([ @@ -704,6 +710,36 @@ class Backend { return this.defaultAnkiFieldTemplates; } + async _onApiGetAnkiDeckNames(params, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.anki.getDeckNames(); + } + + async _onApiGetAnkiModelNames(params, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.anki.getModelNames(); + } + + async _onApiGetAnkiModelFieldNames({modelName}, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.anki.getModelFieldNames(modelName); + } + + async _onApiGetDictionaryInfo(params, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.translator.database.getDictionaryInfo(); + } + + async _onApiGetDictionaryCounts({dictionaryNames, getTotal}, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.translator.database.getDictionaryCounts(dictionaryNames, getTotal); + } + + async _onApiPurgeDatabase(params, sender) { + this._validatePrivilegedMessageSender(sender); + return await this.translator.purgeDatabase(); + } + // Command handlers async _onCommandSearch(params) { @@ -800,6 +836,13 @@ class Backend { // Utilities + _validatePrivilegedMessageSender(sender) { + const url = sender.url; + if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) { + throw new Error('Invalid message sender'); + } + } + async _getAudioUri(definition, source, details) { let optionsContext = (typeof details === 'object' && details !== null ? details.optionsContext : null); if (!(typeof optionsContext === 'object' && optionsContext !== null)) { diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index b32a9517..ff1277ed 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -16,13 +16,13 @@ */ /* global + * apiGetAnkiDeckNames + * apiGetAnkiModelFieldNames + * apiGetAnkiModelNames * getOptionsContext * getOptionsMutable * onFormOptionsChanged * settingsSaveOptions - * utilAnkiGetDeckNames - * utilAnkiGetModelFieldNames - * utilAnkiGetModelNames * utilBackgroundIsolate */ @@ -107,7 +107,7 @@ async function _ankiDeckAndModelPopulate(options) { const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'}; try { _ankiSpinnerShow(true); - const [deckNames, modelNames] = await Promise.all([utilAnkiGetDeckNames(), utilAnkiGetModelNames()]); + const [deckNames, modelNames] = await Promise.all([apiGetAnkiDeckNames(), apiGetAnkiModelNames()]); deckNames.sort(); modelNames.sort(); termsDeck.values = deckNames; @@ -180,7 +180,7 @@ async function _onAnkiModelChanged(e) { let fieldNames; try { const modelName = node.value; - fieldNames = await utilAnkiGetModelFieldNames(modelName); + fieldNames = await apiGetAnkiModelFieldNames(modelName); _ankiSetError(null); } catch (error) { _ankiSetError(error); diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 1a6d452b..7eed4273 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -17,8 +17,11 @@ /* global * PageExitPrevention + * apiGetDictionaryCounts + * apiGetDictionaryInfo * apiOptionsGet * apiOptionsGetFull + * apiPurgeDatabase * getOptionsContext * getOptionsFullMutable * getOptionsMutable @@ -27,10 +30,7 @@ * storageUpdateStats * utilBackgroundIsolate * utilDatabaseDeleteDictionary - * utilDatabaseGetDictionaryCounts - * utilDatabaseGetDictionaryInfo * utilDatabaseImport - * utilDatabasePurge */ let dictionaryUI = null; @@ -431,7 +431,7 @@ async function onDictionaryOptionsChanged() { async function onDatabaseUpdated() { try { - const dictionaries = await utilDatabaseGetDictionaryInfo(); + const dictionaries = await apiGetDictionaryInfo(); dictionaryUI.setDictionaries(dictionaries); document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); @@ -439,7 +439,7 @@ async function onDatabaseUpdated() { updateMainDictionarySelectOptions(dictionaries); await updateMainDictionarySelectValue(); - const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map((v) => v.title), true); + const {counts, total} = await apiGetDictionaryCounts(dictionaries.map((v) => v.title), true); dictionaryUI.setCounts(counts, total); } catch (e) { dictionaryErrorsShow([e]); @@ -618,7 +618,7 @@ async function onDictionaryPurge(e) { dictionaryErrorsShow(null); dictionarySpinnerShow(true); - await utilDatabasePurge(); + await apiPurgeDatabase(); for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { options.dictionaries = utilBackgroundIsolate({}); options.general.mainDictionary = ''; diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 69536f02..106365ac 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -79,35 +79,6 @@ function utilBackend() { return backend; } -async function utilAnkiGetModelNames() { - return utilIsolate(await utilBackend().anki.getModelNames()); -} - -async function utilAnkiGetDeckNames() { - return utilIsolate(await utilBackend().anki.getDeckNames()); -} - -async function utilDatabaseGetDictionaryInfo() { - return utilIsolate(await utilBackend().translator.database.getDictionaryInfo()); -} - -async function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) { - return utilIsolate(await utilBackend().translator.database.getDictionaryCounts( - utilBackgroundIsolate(dictionaryNames), - utilBackgroundIsolate(getTotal) - )); -} - -async function utilAnkiGetModelFieldNames(modelName) { - return utilIsolate(await utilBackend().anki.getModelFieldNames( - utilBackgroundIsolate(modelName) - )); -} - -async function utilDatabasePurge() { - return utilIsolate(await utilBackend().translator.purgeDatabase()); -} - async function utilDatabaseDeleteDictionary(dictionaryName, onProgress) { return utilIsolate(await utilBackend().translator.database.deleteDictionary( utilBackgroundIsolate(dictionaryName), diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 50b285a5..56e6a715 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -120,6 +120,30 @@ function apiGetDefaultAnkiFieldTemplates() { return _apiInvoke('getDefaultAnkiFieldTemplates'); } +function apiGetAnkiDeckNames() { + return _apiInvoke('getAnkiDeckNames'); +} + +function apiGetAnkiModelNames() { + return _apiInvoke('getAnkiModelNames'); +} + +function apiGetAnkiModelFieldNames(modelName) { + return _apiInvoke('getAnkiModelFieldNames', {modelName}); +} + +function apiGetDictionaryInfo() { + return _apiInvoke('getDictionaryInfo'); +} + +function apiGetDictionaryCounts(dictionaryNames, getTotal) { + return _apiInvoke('getDictionaryCounts', {dictionaryNames, getTotal}); +} + +function apiPurgeDatabase() { + return _apiInvoke('purgeDatabase'); +} + function _apiInvoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { -- cgit v1.2.3 From cdd817a0e1c573b24114836a389236f49dd9b279 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 15:23:32 -0400 Subject: Move and rename utilStringHashCode options.js is the only place it's used. --- ext/bg/js/options.js | 23 ++++++++++++++++------- ext/bg/js/util.js | 13 ------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 20df2a68..d36b2bab 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -15,14 +15,23 @@ * along with this program. If not, see . */ -/* global - * utilStringHashCode - */ - /* * Generic options functions */ +function optionsGetStringHashCode(string) { + let hashCode = 0; + + if (typeof string !== 'string') { return hashCode; } + + for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { + hashCode = ((hashCode << 5) - hashCode) + charCode; + hashCode |= 0; + } + + return hashCode; +} + function optionsGenericApplyUpdates(options, updates) { const targetVersion = updates.length; const currentVersion = options.version; @@ -63,12 +72,12 @@ const profileOptionsVersionUpdates = [ options.anki.fieldTemplates = null; }, (options) => { - if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) { + if (optionsGetStringHashCode(options.anki.fieldTemplates) === 1285806040) { options.anki.fieldTemplates = null; } }, (options) => { - if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) { + if (optionsGetStringHashCode(options.anki.fieldTemplates) === -250091611) { options.anki.fieldTemplates = null; } }, @@ -87,7 +96,7 @@ const profileOptionsVersionUpdates = [ (options) => { // Version 12 changes: // The preferred default value of options.anki.fieldTemplates has been changed to null. - if (utilStringHashCode(options.anki.fieldTemplates) === 1444379824) { + if (optionsGetStringHashCode(options.anki.fieldTemplates) === 1444379824) { options.anki.fieldTemplates = null; } }, diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 106365ac..5edcc193 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -58,19 +58,6 @@ function utilBackgroundFunctionIsolate(func) { return backgroundPage.utilFunctionIsolate(func); } -function utilStringHashCode(string) { - let hashCode = 0; - - if (typeof string !== 'string') { return hashCode; } - - for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { - hashCode = ((hashCode << 5) - hashCode) + charCode; - hashCode |= 0; - } - - return hashCode; -} - function utilBackend() { const backend = chrome.extension.getBackgroundPage().yomichanBackend; if (!backend.isPrepared) { -- cgit v1.2.3 From ceb12ac41551aca11bc195e5fad9984a28a5e291 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 23:20:36 -0400 Subject: Add support for filtering frequency metadata based on readings --- .../data/dictionary-term-meta-bank-v3-schema.json | 26 ++++++++++++++++++++-- ext/bg/js/translator.js | 16 ++++++++++++- .../valid-dictionary1/term_meta_bank_1.json | 6 +++++ test/test-database.js | 16 ++++++------- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json index 8475db81..ffffb546 100644 --- a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json +++ b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json @@ -26,8 +26,30 @@ {}, {"enum": ["freq"]}, { - "type": ["string", "number"], - "description": "Frequency information for the term or expression." + "oneOf": [ + { + "type": ["string", "number"], + "description": "Frequency information for the term or expression." + }, + { + "type": ["object"], + "required": [ + "reading", + "frequency" + ], + "additionalProperties": false, + "properties": { + "reading": { + "type": "string", + "description": "Reading for the term or expression." + }, + "frequency": { + "type": ["string", "number"], + "description": "Frequency information for the term or expression." + } + } + } + ] } ] }, diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index e4441384..b6f8b8e5 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -469,7 +469,9 @@ class Translator { switch (mode) { case 'freq': for (const term of termsUnique[index]) { - term.frequencies.push({expression, frequency: data, dictionary}); + const frequencyData = this.getFrequencyData(expression, data, dictionary, term); + if (frequencyData === null) { continue; } + term.frequencies.push(frequencyData); } break; case 'pitch': @@ -562,6 +564,18 @@ class Translator { return tagMetaList; } + getFrequencyData(expression, data, dictionary, term) { + if (data !== null && typeof data === 'object') { + const {frequency, reading} = data; + + const termReading = term.reading || expression; + if (reading !== termReading) { return null; } + + return {expression, frequency, dictionary}; + } + return {expression, frequency: data, dictionary}; + } + async getPitchData(expression, data, dictionary, term) { const reading = data.reading; const termReading = term.reading || expression; diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json index 26922394..73d74e68 100644 --- a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json +++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json @@ -2,6 +2,12 @@ ["打", "freq", 1], ["打つ", "freq", 2], ["打ち込む", "freq", 3], + ["打", "freq", {"reading": "だ", "frequency": 4}], + ["打", "freq", {"reading": "ダース", "frequency": 5}], + ["打つ", "freq", {"reading": "うつ", "frequency": 6}], + ["打つ", "freq", {"reading": "ぶつ", "frequency": 7}], + ["打ち込む", "freq", {"reading": "うちこむ", "frequency": 8}], + ["打ち込む", "freq", {"reading": "ぶちこむ", "frequency": 9}], [ "打ち込む", "pitch", diff --git a/test/test-database.js b/test/test-database.js index d27f92e1..8b7a163a 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -235,8 +235,8 @@ async function testDatabase1() { true ); vm.assert.deepStrictEqual(counts, { - counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}], - total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14} + counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 12, tagMeta: 14}], + total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 12, tagMeta: 14} }); // Test find* functions @@ -626,9 +626,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 1, + total: 3, modes: [ - ['freq', 1] + ['freq', 3] ] } }, @@ -639,9 +639,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 1, + total: 3, modes: [ - ['freq', 1] + ['freq', 3] ] } }, @@ -652,9 +652,9 @@ async function testFindTermMetaBulk1(database, titles) { } ], expectedResults: { - total: 3, + total: 5, modes: [ - ['freq', 1], + ['freq', 3], ['pitch', 2] ] } -- cgit v1.2.3 From ade1b705d2370be9222ba4164f79bbdfae590bc1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:20:02 -0400 Subject: Mark internals as private --- ext/bg/js/anki.js | 70 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index c7f7c0cc..021cb4c4 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -25,82 +25,84 @@ class AnkiConnect { constructor(server) { - this.server = server; - this.localVersion = 2; - this.remoteVersion = 0; + this._server = server; + this._localVersion = 2; + this._remoteVersion = 0; } async addNote(note) { - await this.checkVersion(); - return await this.ankiInvoke('addNote', {note}); + await this._checkVersion(); + return await this._ankiInvoke('addNote', {note}); } async canAddNotes(notes) { - await this.checkVersion(); - return await this.ankiInvoke('canAddNotes', {notes}); + await this._checkVersion(); + return await this._ankiInvoke('canAddNotes', {notes}); } async getDeckNames() { - await this.checkVersion(); - return await this.ankiInvoke('deckNames'); + await this._checkVersion(); + return await this._ankiInvoke('deckNames'); } async getModelNames() { - await this.checkVersion(); - return await this.ankiInvoke('modelNames'); + await this._checkVersion(); + return await this._ankiInvoke('modelNames'); } async getModelFieldNames(modelName) { - await this.checkVersion(); - return await this.ankiInvoke('modelFieldNames', {modelName}); + await this._checkVersion(); + return await this._ankiInvoke('modelFieldNames', {modelName}); } async guiBrowse(query) { - await this.checkVersion(); - return await this.ankiInvoke('guiBrowse', {query}); + await this._checkVersion(); + return await this._ankiInvoke('guiBrowse', {query}); } async storeMediaFile(filename, dataBase64) { - await this.checkVersion(); - return await this.ankiInvoke('storeMediaFile', {filename, data: dataBase64}); - } - - async checkVersion() { - if (this.remoteVersion < this.localVersion) { - this.remoteVersion = await this.ankiInvoke('version'); - if (this.remoteVersion < this.localVersion) { - throw new Error('Extension and plugin versions incompatible'); - } - } + await this._checkVersion(); + return await this._ankiInvoke('storeMediaFile', {filename, data: dataBase64}); } async findNoteIds(notes) { - await this.checkVersion(); + await this._checkVersion(); const actions = notes.map((note) => ({ action: 'findNotes', params: { - query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}` + query: `deck:"${this._escapeQuery(note.deckName)}" ${this._fieldsToQuery(note.fields)}` } })); - return await this.ankiInvoke('multi', {actions}); + return await this._ankiInvoke('multi', {actions}); + } + + // Private + + async _checkVersion() { + if (this._remoteVersion < this._localVersion) { + this._remoteVersion = await this._ankiInvoke('version'); + if (this._remoteVersion < this._localVersion) { + throw new Error('Extension and plugin versions incompatible'); + } + } } - ankiInvoke(action, params) { - return requestJson(this.server, 'POST', {action, params, version: this.localVersion}); + _ankiInvoke(action, params) { + return requestJson(this._server, 'POST', {action, params, version: this._localVersion}); } - static escapeQuery(text) { + _escapeQuery(text) { return text.replace(/"/g, ''); } - static fieldsToQuery(fields) { + _fieldsToQuery(fields) { const fieldNames = Object.keys(fields); if (fieldNames.length === 0) { return ''; } const key = fieldNames[0]; - return `${key.toLowerCase()}:"${AnkiConnect.escapeQuery(fields[key])}"`; + return `${key.toLowerCase()}:"${this._escapeQuery(fields[key])}"`; } } -- cgit v1.2.3 From c41c7252aeb5f10fca7403a19740d869743a38a5 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:37:13 -0400 Subject: Add enabled checks --- ext/bg/js/anki.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index 021cb4c4..f7a24291 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -25,47 +25,74 @@ class AnkiConnect { constructor(server) { + this._enabled = true; this._server = server; this._localVersion = 2; this._remoteVersion = 0; } + setServer(server) { + this._server = server; + } + + getServer() { + return this._server; + } + + setEnabled(enabled) { + this._enabled = enabled; + } + + isEnabled() { + return this._enabled; + } + async addNote(note) { + if (!this._enabled) { return null; } await this._checkVersion(); return await this._ankiInvoke('addNote', {note}); } async canAddNotes(notes) { + if (!this._enabled) { return []; } await this._checkVersion(); return await this._ankiInvoke('canAddNotes', {notes}); } async getDeckNames() { + if (!this._enabled) { return []; } await this._checkVersion(); return await this._ankiInvoke('deckNames'); } async getModelNames() { + if (!this._enabled) { return []; } await this._checkVersion(); return await this._ankiInvoke('modelNames'); } async getModelFieldNames(modelName) { + if (!this._enabled) { return []; } await this._checkVersion(); return await this._ankiInvoke('modelFieldNames', {modelName}); } async guiBrowse(query) { + if (!this._enabled) { return []; } await this._checkVersion(); return await this._ankiInvoke('guiBrowse', {query}); } async storeMediaFile(filename, dataBase64) { + if (!this._enabled) { + return {result: null, error: 'AnkiConnect not enabled'}; + } await this._checkVersion(); return await this._ankiInvoke('storeMediaFile', {filename, data: dataBase64}); } async findNoteIds(notes) { + if (!this._enabled) { return []; } await this._checkVersion(); const actions = notes.map((note) => ({ action: 'findNotes', -- cgit v1.2.3 From 4c2ca82a2937fa4e1d0f3f6744f4e1a7b88692a1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:38:33 -0400 Subject: Use single instance of AnkiConnect --- ext/bg/js/anki.js | 2 +- ext/bg/js/backend.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index f7a24291..dd802424 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -25,7 +25,7 @@ class AnkiConnect { constructor(server) { - this._enabled = true; + this._enabled = false; this._server = server; this._localVersion = 2; this._remoteVersion = 0; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index be8ea322..24a16199 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -47,7 +47,7 @@ class Backend { this.database = new Database(); this.dictionaryImporter = new DictionaryImporter(); this.translator = new Translator(this.database); - this.anki = new AnkiNull(); + this.anki = new AnkiConnect(); this.mecab = new Mecab(); this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); this.options = null; @@ -210,7 +210,8 @@ class Backend { this.setExtensionBadgeText(''); } - this.anki = options.anki.enable ? new AnkiConnect(options.anki.server) : new AnkiNull(); + this.anki.setServer(options.anki.server); + this.anki.setEnabled(options.anki.enable); if (options.parsing.enableMecabParser) { this.mecab.startListener(); -- cgit v1.2.3 From cc5e4294223f9d0106ddec1d561b29ac449b1115 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:38:52 -0400 Subject: Remove AnkiNull and redundant comment --- ext/bg/js/anki.js | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index dd802424..27590311 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -19,10 +19,6 @@ * requestJson */ -/* - * AnkiConnect - */ - class AnkiConnect { constructor(server) { this._enabled = false; @@ -132,38 +128,3 @@ class AnkiConnect { return `${key.toLowerCase()}:"${this._escapeQuery(fields[key])}"`; } } - - -/* - * AnkiNull - */ - -class AnkiNull { - async addNote() { - return null; - } - - async canAddNotes() { - return []; - } - - async getDeckNames() { - return []; - } - - async getModelNames() { - return []; - } - - async getModelFieldNames() { - return []; - } - - async guiBrowse() { - return []; - } - - async findNoteIds() { - return []; - } -} -- cgit v1.2.3 From 3c335e68cdd41860d791ffe85dd07abf8932d3ce Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:43:24 -0400 Subject: Throw errors in returned by invocation --- ext/bg/js/anki.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index 27590311..c07af462 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -81,7 +81,7 @@ class AnkiConnect { async storeMediaFile(filename, dataBase64) { if (!this._enabled) { - return {result: null, error: 'AnkiConnect not enabled'}; + throw new Error('AnkiConnect not enabled'); } await this._checkVersion(); return await this._ankiInvoke('storeMediaFile', {filename, data: dataBase64}); @@ -110,8 +110,19 @@ class AnkiConnect { } } - _ankiInvoke(action, params) { - return requestJson(this._server, 'POST', {action, params, version: this._localVersion}); + async _ankiInvoke(action, params) { + const result = await requestJson(this._server, 'POST', {action, params, version: this._localVersion}); + if ( + result !== null && + typeof result === 'object' && + !Array.isArray(result) + ) { + const error = result.error; + if (typeof error !== 'undefined') { + throw new Error(`AnkiConnect error: ${error}`); + } + } + return result; } _escapeQuery(text) { -- cgit v1.2.3 From 37c374fb633a5b2f224348a8e5490f0275d348e7 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:44:33 -0400 Subject: Rename _ankiInvoke to _invoke to remove redundancy --- ext/bg/js/anki.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index c07af462..928b5159 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -46,37 +46,37 @@ class AnkiConnect { async addNote(note) { if (!this._enabled) { return null; } await this._checkVersion(); - return await this._ankiInvoke('addNote', {note}); + return await this._invoke('addNote', {note}); } async canAddNotes(notes) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._ankiInvoke('canAddNotes', {notes}); + return await this._invoke('canAddNotes', {notes}); } async getDeckNames() { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._ankiInvoke('deckNames'); + return await this._invoke('deckNames'); } async getModelNames() { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._ankiInvoke('modelNames'); + return await this._invoke('modelNames'); } async getModelFieldNames(modelName) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._ankiInvoke('modelFieldNames', {modelName}); + return await this._invoke('modelFieldNames', {modelName}); } async guiBrowse(query) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._ankiInvoke('guiBrowse', {query}); + return await this._invoke('guiBrowse', {query}); } async storeMediaFile(filename, dataBase64) { @@ -84,7 +84,7 @@ class AnkiConnect { throw new Error('AnkiConnect not enabled'); } await this._checkVersion(); - return await this._ankiInvoke('storeMediaFile', {filename, data: dataBase64}); + return await this._invoke('storeMediaFile', {filename, data: dataBase64}); } async findNoteIds(notes) { @@ -96,21 +96,21 @@ class AnkiConnect { query: `deck:"${this._escapeQuery(note.deckName)}" ${this._fieldsToQuery(note.fields)}` } })); - return await this._ankiInvoke('multi', {actions}); + return await this._invoke('multi', {actions}); } // Private async _checkVersion() { if (this._remoteVersion < this._localVersion) { - this._remoteVersion = await this._ankiInvoke('version'); + this._remoteVersion = await this._invoke('version'); if (this._remoteVersion < this._localVersion) { throw new Error('Extension and plugin versions incompatible'); } } } - async _ankiInvoke(action, params) { + async _invoke(action, params) { const result = await requestJson(this._server, 'POST', {action, params, version: this._localVersion}); if ( result !== null && -- cgit v1.2.3 From ebfc7ca945109c6700b3dbf6d45542ddbba94f3d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 12 Apr 2020 12:46:32 -0400 Subject: Pass anki reference to AnkiNoteBuilder constructor --- ext/bg/js/anki-note-builder.js | 7 ++++--- ext/bg/js/backend.js | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 8a707006..700d8237 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -16,7 +16,8 @@ */ class AnkiNoteBuilder { - constructor({audioSystem, renderTemplate}) { + constructor({anki, audioSystem, renderTemplate}) { + this._anki = anki; this._audioSystem = audioSystem; this._renderTemplate = renderTemplate; } @@ -101,7 +102,7 @@ class AnkiNoteBuilder { } } - async injectScreenshot(definition, fields, screenshot, anki) { + async injectScreenshot(definition, fields, screenshot) { if (!this._containsMarker(fields, 'screenshot')) { return; } const now = new Date(Date.now()); @@ -109,7 +110,7 @@ class AnkiNoteBuilder { const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, ''); try { - await anki.storeMediaFile(filename, data); + await this._anki.storeMediaFile(filename, data); } catch (e) { return; } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 24a16199..6a6819e9 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -56,6 +56,7 @@ class Backend { this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)}); this.audioUriBuilder = new AudioUriBuilder(); this.ankiNoteBuilder = new AnkiNoteBuilder({ + anki: this.anki, audioSystem: this.audioSystem, renderTemplate: this._renderTemplate.bind(this) }); @@ -482,8 +483,7 @@ class Backend { await this.ankiNoteBuilder.injectScreenshot( definition, options.anki.terms.fields, - details.screenshot, - this.anki + details.screenshot ); } -- cgit v1.2.3 From 018913d03fff627fb7d34f594340c47a607e8839 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 19:25:07 -0400 Subject: Use isObject --- ext/bg/js/anki.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index 928b5159..38823431 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -112,11 +112,7 @@ class AnkiConnect { async _invoke(action, params) { const result = await requestJson(this._server, 'POST', {action, params, version: this._localVersion}); - if ( - result !== null && - typeof result === 'object' && - !Array.isArray(result) - ) { + if (isObject(result)) { const error = result.error; if (typeof error !== 'undefined') { throw new Error(`AnkiConnect error: ${error}`); -- cgit v1.2.3 From 06e95b8747e7222d3aa513cda28b0878a11921d9 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 22:16:08 -0400 Subject: Remove unused global --- ext/bg/js/backend.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 6a6819e9..1b922730 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -18,7 +18,6 @@ /* global * AnkiConnect * AnkiNoteBuilder - * AnkiNull * AudioSystem * AudioUriBuilder * BackendApiForwarder -- cgit v1.2.3 From 7fc3882607f48bb9371649ceacddf2fe278282d2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 13:44:31 -0400 Subject: Update the parameters passed to various audio-related functions --- ext/bg/js/anki-note-builder.js | 4 ++-- ext/bg/js/audio-uri-builder.js | 26 ++++++++++++-------------- ext/bg/js/backend.js | 22 +++++++--------------- ext/bg/js/settings/audio.js | 7 +------ ext/mixed/js/api.js | 4 ++-- ext/mixed/js/audio-system.js | 21 ++++++++++++--------- ext/mixed/js/display.js | 17 +++++++++-------- 7 files changed, 45 insertions(+), 56 deletions(-) diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 700d8237..9bab095d 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -85,14 +85,14 @@ class AnkiNoteBuilder { }); } - async injectAudio(definition, fields, sources, optionsContext) { + async injectAudio(definition, fields, sources, details) { if (!this._containsMarker(fields, 'audio')) { return; } try { const expressions = definition.expressions; const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition; - const {uri} = await this._audioSystem.getDefinitionAudio(audioSourceDefinition, sources, {tts: false, optionsContext}); + const {uri} = await this._audioSystem.getDefinitionAudio(audioSourceDefinition, sources, details); const filename = this._createInjectedAudioFileName(audioSourceDefinition); if (filename !== null) { definition.audio = {url: uri, filename}; diff --git a/ext/bg/js/audio-uri-builder.js b/ext/bg/js/audio-uri-builder.js index dfd195d8..27e97680 100644 --- a/ext/bg/js/audio-uri-builder.js +++ b/ext/bg/js/audio-uri-builder.js @@ -49,11 +49,11 @@ class AudioUriBuilder { return url; } - async getUri(definition, source, options) { + async getUri(definition, source, details) { const handler = this._getUrlHandlers.get(source); if (typeof handler === 'function') { try { - return await handler(definition, options); + return await handler(definition, details); } catch (e) { // NOP } @@ -132,26 +132,24 @@ class AudioUriBuilder { throw new Error('Failed to find audio URL'); } - async _getUriTextToSpeech(definition, options) { - const voiceURI = options.audio.textToSpeechVoice; - if (!voiceURI) { + async _getUriTextToSpeech(definition, {textToSpeechVoice}) { + if (!textToSpeechVoice) { throw new Error('No voice'); } - - return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(textToSpeechVoice)}`; } - async _getUriTextToSpeechReading(definition, options) { - const voiceURI = options.audio.textToSpeechVoice; - if (!voiceURI) { + async _getUriTextToSpeechReading(definition, {textToSpeechVoice}) { + if (!textToSpeechVoice) { throw new Error('No voice'); } - - return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(textToSpeechVoice)}`; } - async _getUriCustom(definition, options) { - const customSourceUrl = options.audio.customSourceUrl; + async _getUriCustom(definition, {customSourceUrl}) { + if (typeof customSourceUrl !== 'string') { + throw new Error('No custom URL defined'); + } return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); } } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index a1b788df..79402e67 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -51,8 +51,10 @@ class Backend { this.options = null; this.optionsSchema = null; this.defaultAnkiFieldTemplates = null; - this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)}); this.audioUriBuilder = new AudioUriBuilder(); + this.audioSystem = new AudioSystem({ + audioUriBuilder: this.audioUriBuilder + }); this.ankiNoteBuilder = new AnkiNoteBuilder({ anki: this.anki, audioSystem: this.audioSystem, @@ -494,11 +496,12 @@ class Backend { const templates = this.defaultAnkiFieldTemplates; if (mode !== 'kanji') { + const {customSourceUrl} = options.audio; await this.ankiNoteBuilder.injectAudio( definition, options.anki.terms.fields, options.audio.sources, - optionsContext + {textToSpeechVoice: null, customSourceUrl} ); } @@ -573,9 +576,8 @@ class Backend { return this._runCommand(command, params); } - async _onApiAudioGetUri({definition, source, optionsContext}) { - const options = this.getOptions(optionsContext); - return await this.audioUriBuilder.getUri(definition, source, options); + async _onApiAudioGetUri({definition, source, details}) { + return await this.audioUriBuilder.getUri(definition, source, details); } _onApiScreenshotGet({options}, sender) { @@ -861,16 +863,6 @@ class Backend { } } - async _getAudioUri(definition, source, details) { - let optionsContext = (typeof details === 'object' && details !== null ? details.optionsContext : null); - if (!(typeof optionsContext === 'object' && optionsContext !== null)) { - optionsContext = this.optionsContext; - } - - const options = this.getOptions(optionsContext); - return await this.audioUriBuilder.getUri(definition, source, options); - } - async _renderTemplate(template, data) { return handlebarsRenderDynamic(template, data); } diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 3c6e126c..e9aa72e1 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -28,12 +28,7 @@ let audioSourceUI = null; let audioSystem = null; async function audioSettingsInitialize() { - audioSystem = new AudioSystem({ - getAudioUri: async (definition, source) => { - const optionsContext = getOptionsContext(); - return await apiAudioGetUri(definition, source, optionsContext); - } - }); + audioSystem = new AudioSystem({audioUriBuilder: null}); const optionsContext = getOptionsContext(); const options = await getOptionsMutable(optionsContext); diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 7080d93a..c97dc687 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -64,8 +64,8 @@ function apiTemplateRender(template, data) { return _apiInvoke('templateRender', {data, template}); } -function apiAudioGetUri(definition, source, optionsContext) { - return _apiInvoke('audioGetUri', {definition, source, optionsContext}); +function apiAudioGetUri(definition, source, details) { + return _apiInvoke('audioGetUri', {definition, source, details}); } function apiCommandExec(command, params) { diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 45b733fc..574ad3dc 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -66,10 +66,10 @@ class TextToSpeechAudio { } class AudioSystem { - constructor({getAudioUri}) { + constructor({audioUriBuilder}) { this._cache = new Map(); this._cacheSizeMaximum = 32; - this._getAudioUri = getAudioUri; + this._audioUriBuilder = audioUriBuilder; if (typeof speechSynthesis !== 'undefined') { // speechSynthesis.getVoices() will not be populated unless some API call is made. @@ -90,7 +90,7 @@ class AudioSystem { if (uri === null) { continue; } try { - const audio = await this._createAudio(uri, details); + const audio = await this._createAudio(uri); this._cacheCheck(); this._cache.set(key, {audio, uri, source}); return {audio, uri, source}; @@ -114,20 +114,23 @@ class AudioSystem { // NOP } - async _createAudio(uri, details) { + async _createAudio(uri) { const ttsParameters = this._getTextToSpeechParameters(uri); if (ttsParameters !== null) { - if (typeof details === 'object' && details !== null) { - if (details.tts === false) { - throw new Error('Text-to-speech not permitted'); - } - } return this.createTextToSpeechAudio(ttsParameters); } return await this._createAudioFromUrl(uri); } + _getAudioUri(definition, source, details) { + return ( + this._audioUriBuilder !== null ? + this._audioUriBuilder.getUri(definition, source, details) : + null + ); + } + _createAudioFromUrl(url) { return new Promise((resolve, reject) => { const audio = new Audio(url); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 63687dc2..7f3ba859 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -45,7 +45,13 @@ class Display { this.index = 0; this.audioPlaying = null; this.audioFallback = null; - this.audioSystem = new AudioSystem({getAudioUri: this._getAudioUri.bind(this)}); + this.audioSystem = new AudioSystem({ + audioUriBuilder: { + async getUri(definition, source, details) { + return await apiAudioGetUri(definition, source, details); + } + } + }); this.styleNode = null; this.eventListeners = new EventListenerCollection(); @@ -789,10 +795,10 @@ class Display { this.audioPlaying = null; } - const sources = this.options.audio.sources; let audio, source, info; try { - ({audio, source} = await this.audioSystem.getDefinitionAudio(expression, sources)); + const {sources, textToSpeechVoice, customSourceUrl} = this.options.audio; + ({audio, source} = await this.audioSystem.getDefinitionAudio(expression, sources, {textToSpeechVoice, customSourceUrl})); info = `From source ${1 + sources.indexOf(source)}: ${source}`; } catch (e) { if (this.audioFallback === null) { @@ -947,9 +953,4 @@ class Display { } }; } - - async _getAudioUri(definition, source) { - const optionsContext = this.getOptionsContext(); - return await apiAudioGetUri(definition, source, optionsContext); - } } -- cgit v1.2.3 From 823c026533dcd758c2a93038fa526978a5fa9cc3 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 13:51:47 -0400 Subject: Remove de/structuring from public API --- ext/bg/js/settings/audio.js | 2 +- ext/mixed/js/audio-system.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index e9aa72e1..68dfe71e 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -110,7 +110,7 @@ function textToSpeechTest() { const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || ''; const voiceUri = document.querySelector('#text-to-speech-voice').value; - const audio = audioSystem.createTextToSpeechAudio({text, voiceUri}); + const audio = audioSystem.createTextToSpeechAudio(text, voiceUri); audio.volume = 1.0; audio.play(); } catch (e) { diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 574ad3dc..5366e3e0 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -102,7 +102,7 @@ class AudioSystem { throw new Error('Could not create audio'); } - createTextToSpeechAudio({text, voiceUri}) { + createTextToSpeechAudio(text, voiceUri) { const voice = this._getTextToSpeechVoiceFromVoiceUri(voiceUri); if (voice === null) { throw new Error('Invalid text-to-speech voice'); @@ -117,7 +117,8 @@ class AudioSystem { async _createAudio(uri) { const ttsParameters = this._getTextToSpeechParameters(uri); if (ttsParameters !== null) { - return this.createTextToSpeechAudio(ttsParameters); + const {text, voiceUri} = ttsParameters; + return this.createTextToSpeechAudio(text, voiceUri); } return await this._createAudioFromUrl(uri); -- cgit v1.2.3 From e1ebfb02f724518432b2e1c5ec2a80ff03b38fd8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 16:12:55 -0400 Subject: Disable cache on the backend and fix a bug with the cache key --- ext/bg/js/backend.js | 3 ++- ext/bg/js/settings/audio.js | 5 ++++- ext/mixed/js/audio-system.js | 22 ++++++++++++++-------- ext/mixed/js/display.js | 3 ++- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 79402e67..9d1fa6c1 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -53,7 +53,8 @@ class Backend { this.defaultAnkiFieldTemplates = null; this.audioUriBuilder = new AudioUriBuilder(); this.audioSystem = new AudioSystem({ - audioUriBuilder: this.audioUriBuilder + audioUriBuilder: this.audioUriBuilder, + useCache: false }); this.ankiNoteBuilder = new AnkiNoteBuilder({ anki: this.anki, diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 68dfe71e..98ed9b8b 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -28,7 +28,10 @@ let audioSourceUI = null; let audioSystem = null; async function audioSettingsInitialize() { - audioSystem = new AudioSystem({audioUriBuilder: null}); + audioSystem = new AudioSystem({ + audioUriBuilder: null, + useCache: true + }); const optionsContext = getOptionsContext(); const options = await getOptionsMutable(optionsContext); diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 5366e3e0..255a96de 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -66,8 +66,8 @@ class TextToSpeechAudio { } class AudioSystem { - constructor({audioUriBuilder}) { - this._cache = new Map(); + constructor({audioUriBuilder, useCache}) { + this._cache = useCache ? new Map() : null; this._cacheSizeMaximum = 32; this._audioUriBuilder = audioUriBuilder; @@ -79,10 +79,14 @@ class AudioSystem { async getDefinitionAudio(definition, sources, details) { const key = `${definition.expression}:${definition.reading}`; - const cacheValue = this._cache.get(definition); - if (typeof cacheValue !== 'undefined') { - const {audio, uri, source} = cacheValue; - return {audio, uri, source}; + const hasCache = (this._cache !== null); + + if (hasCache) { + const cacheValue = this._cache.get(key); + if (typeof cacheValue !== 'undefined') { + const {audio, uri, source} = cacheValue; + return {audio, uri, source}; + } } for (const source of sources) { @@ -91,8 +95,10 @@ class AudioSystem { try { const audio = await this._createAudio(uri); - this._cacheCheck(); - this._cache.set(key, {audio, uri, source}); + if (hasCache) { + this._cacheCheck(); + this._cache.set(key, {audio, uri, source}); + } return {audio, uri, source}; } catch (e) { // NOP diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 7f3ba859..8edae7c9 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -50,7 +50,8 @@ class Display { async getUri(definition, source, details) { return await apiAudioGetUri(definition, source, details); } - } + }, + useCache: true }); this.styleNode = null; -- cgit v1.2.3 From f50aee1021179411322f67c5951eb35de81c5174 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 16:35:43 -0400 Subject: Only return the cached value if it uses a valid source --- ext/mixed/js/audio-system.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 255a96de..0ded3490 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -85,7 +85,9 @@ class AudioSystem { const cacheValue = this._cache.get(key); if (typeof cacheValue !== 'undefined') { const {audio, uri, source} = cacheValue; - return {audio, uri, source}; + if (sources.includes(source)) { + return {audio, uri, source}; + } } } -- cgit v1.2.3 From 7eb7c88394ebb56936861b91e6b04525abb57490 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 16:38:53 -0400 Subject: Return index of the source instead of the source value --- ext/mixed/js/audio-system.js | 10 ++++++---- ext/mixed/js/display.js | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 0ded3490..94885d34 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -85,13 +85,15 @@ class AudioSystem { const cacheValue = this._cache.get(key); if (typeof cacheValue !== 'undefined') { const {audio, uri, source} = cacheValue; - if (sources.includes(source)) { - return {audio, uri, source}; + const index = sources.indexOf(source); + if (index >= 0) { + return {audio, uri, index}; } } } - for (const source of sources) { + for (let i = 0, ii = sources.length; i < ii; ++i) { + const source = sources[i]; const uri = await this._getAudioUri(definition, source, details); if (uri === null) { continue; } @@ -101,7 +103,7 @@ class AudioSystem { this._cacheCheck(); this._cache.set(key, {audio, uri, source}); } - return {audio, uri, source}; + return {audio, uri, index: i}; } catch (e) { // NOP } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 8edae7c9..5b8d3610 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -796,11 +796,12 @@ class Display { this.audioPlaying = null; } - let audio, source, info; + let audio, info; try { const {sources, textToSpeechVoice, customSourceUrl} = this.options.audio; - ({audio, source} = await this.audioSystem.getDefinitionAudio(expression, sources, {textToSpeechVoice, customSourceUrl})); - info = `From source ${1 + sources.indexOf(source)}: ${source}`; + let index; + ({audio, index} = await this.audioSystem.getDefinitionAudio(expression, sources, {textToSpeechVoice, customSourceUrl})); + info = `From source ${1 + index}: ${sources[index]}`; } catch (e) { if (this.audioFallback === null) { this.audioFallback = new Audio('/mixed/mp3/button.mp3'); -- cgit v1.2.3 From 5c2dff345eb9d4a25cf1022d14e28ba5925b0b10 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 10 Apr 2020 16:43:57 -0400 Subject: Fix button title text not updating correctly in merge mode --- ext/mixed/js/display.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 5b8d3610..d4481349 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -810,7 +810,7 @@ class Display { info = 'Could not find audio'; } - const button = this.audioButtonFindImage(entryIndex); + const button = this.audioButtonFindImage(entryIndex, expressionIndex); if (button !== null) { let titleDefault = button.dataset.titleDefault; if (!titleDefault) { @@ -909,9 +909,16 @@ class Display { viewerButton.dataset.noteId = noteId; } - audioButtonFindImage(index) { + audioButtonFindImage(index, expressionIndex) { const entry = this.getEntry(index); - return entry !== null ? entry.querySelector('.action-play-audio>img') : null; + if (entry === null) { return null; } + + const container = ( + expressionIndex >= 0 ? + entry.querySelector(`.term-expression:nth-of-type(${expressionIndex + 1})`) : + entry + ); + return container !== null ? container.querySelector('.action-play-audio>img') : null; } async getDefinitionsAddable(definitions, modes) { -- cgit v1.2.3 From 92790763d19a5259e4b091b72a51e67f45548685 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 14 Apr 2020 18:22:51 -0400 Subject: Update style --- ext/mixed/js/display.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index d4481349..f30a65e6 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -47,7 +47,7 @@ class Display { this.audioFallback = null; this.audioSystem = new AudioSystem({ audioUriBuilder: { - async getUri(definition, source, details) { + getUri: async (definition, source, details) => { return await apiAudioGetUri(definition, source, details); } }, -- cgit v1.2.3 From 9fe7b9ad29958b148162bfc2d065a7e32a986291 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 14 Apr 2020 18:26:24 -0400 Subject: Remove unused global --- ext/bg/js/settings/audio.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 98ed9b8b..ac2d82f3 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -18,7 +18,6 @@ /* global * AudioSourceUI * AudioSystem - * apiAudioGetUri * getOptionsContext * getOptionsMutable * settingsSaveOptions -- cgit v1.2.3 From fcbfde506abf6ca3474d2dfdf4f337b86b0bb579 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 17:48:55 -0400 Subject: Await and handle errors from audio.play() --- ext/mixed/js/audio-system.js | 2 +- ext/mixed/js/display.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js index 94885d34..3273f982 100644 --- a/ext/mixed/js/audio-system.js +++ b/ext/mixed/js/audio-system.js @@ -40,7 +40,7 @@ class TextToSpeechAudio { } } - play() { + async play() { try { if (this._utterance === null) { this._utterance = new SpeechSynthesisUtterance(this.text || ''); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index f30a65e6..b4a93d99 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -823,7 +823,14 @@ class Display { this.audioPlaying = audio; audio.currentTime = 0; audio.volume = this.options.audio.volume / 100.0; - audio.play(); + const playPromise = audio.play(); + if (typeof playPromise !== 'undefined') { + try { + await playPromise; + } catch (e2) { + // NOP + } + } } catch (e) { this.onError(e); } finally { -- cgit v1.2.3 From 320852f2d01d72c1039d098033081e8266d02be7 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 18:00:28 -0400 Subject: Fix overlapping audio.play calls due to await --- ext/mixed/js/display.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index b4a93d99..c2284ffe 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -791,10 +791,7 @@ class Display { const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex]; - if (this.audioPlaying !== null) { - this.audioPlaying.pause(); - this.audioPlaying = null; - } + this._stopPlayingAudio(); let audio, info; try { @@ -820,6 +817,8 @@ class Display { button.title = `${titleDefault}\n${info}`; } + this._stopPlayingAudio(); + this.audioPlaying = audio; audio.currentTime = 0; audio.volume = this.options.audio.volume / 100.0; @@ -838,6 +837,13 @@ class Display { } } + _stopPlayingAudio() { + if (this.audioPlaying !== null) { + this.audioPlaying.pause(); + this.audioPlaying = null; + } + } + noteUsesScreenshot(mode) { const optionsAnki = this.options.anki; const fields = (mode === 'kanji' ? optionsAnki.kanji : optionsAnki.terms).fields; -- cgit v1.2.3 From 4fdc300b61ebc3d36c3f5a511df92248453f8d55 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Fri, 17 Apr 2020 23:09:55 +0300 Subject: disable root frame popup when iframe is fullscreen --- ext/fg/js/frontend-initialize.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 2b942258..83c0e606 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -88,7 +88,7 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame) { + if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen) { popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder); popups.iframe = popup; } else if (proxy) { @@ -117,6 +117,7 @@ async function main() { }; yomichan.on('optionsUpdated', applyOptions); + window.addEventListener('fullscreenchange', applyOptions, false); await applyOptions(); } -- cgit v1.2.3 From fbaf50def1934ef6fe0967233f4419efc44f1c30 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:33:49 +0300 Subject: support iframes inside open shadow dom --- ext/fg/js/frame-offset-forwarder.js | 23 +++++++++++++++++++++-- test/data/html/test-document2.html | 19 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index c658c55a..ac6e617d 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -79,9 +79,28 @@ class FrameOffsetForwarder { sourceFrame = frame; break; } + if (sourceFrame === null) { - this._forwardFrameOffsetOrigin(null, uniqueId); - return; + const getShadowRootElements = (documentOrElement) => { + const elements = Array.from(documentOrElement.querySelectorAll('*')) + .filter((el) => !!el.shadowRoot); + const childElements = elements + .map((el) => el.shadowRoot) + .map(getShadowRootElements); + elements.push(childElements.flat()); + + return elements.flat(); + }; + + sourceFrame = getShadowRootElements(document) + .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) + .flat() + .find((el) => el.contentWindow === e.source); + + if (!sourceFrame) { + this._forwardFrameOffsetOrigin(null, uniqueId); + return; + } } const [forwardedX, forwardedY] = offset; diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html index 3a22a5bf..b2046dfd 100644 --- a/test/data/html/test-document2.html +++ b/test/data/html/test-document2.html @@ -77,5 +77,22 @@ document.querySelector('#fullscreen-link1').addEventListener('click', () => togg +
+
<iframe> element inside of an open shadow DOM.
+
+ + +
+ - \ No newline at end of file + -- cgit v1.2.3 From 85706c421b7496d2d73a0f3d1f7721d39d5d0b3f Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:50:05 +0300 Subject: show popup inside iframe for closed shadow dom --- ext/fg/js/frontend-initialize.js | 15 +++++++++++---- ext/fg/js/popup-proxy.js | 7 ++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 83c0e606..2e63c29f 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -24,7 +24,7 @@ * apiOptionsGet */ -async function createIframePopupProxy(url, frameOffsetForwarder) { +async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( chrome.runtime.onMessage, ({action, params}, {resolve}) => { @@ -38,7 +38,7 @@ async function createIframePopupProxy(url, frameOffsetForwarder) { const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); - const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset); + const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled); await popup.prepare(); return popup; @@ -78,6 +78,13 @@ async function main() { let frontendPreparePromise = null; let frameOffsetForwarder = null; + let iframePopupsInRootFrameAvailable = true; + + const disableIframePopupsInRootFrame = () => { + iframePopupsInRootFrameAvailable = false; + applyOptions(); + }; + const applyOptions = async () => { const optionsContext = {depth: isSearchPage ? 0 : depth, url}; const options = await apiOptionsGet(optionsContext); @@ -88,8 +95,8 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen) { - popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder); + if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen && iframePopupsInRootFrameAvailable) { + popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); popups.iframe = popup; } else if (proxy) { popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 82ad9a8f..3af83db2 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -20,7 +20,7 @@ */ class PopupProxy { - constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) { + constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null, setDisabled=null) { this._parentId = parentId; this._parentFrameId = parentFrameId; this._id = id; @@ -28,6 +28,7 @@ class PopupProxy { this._url = url; this._apiSender = new FrontendApiSender(); this._getFrameOffset = getFrameOffset; + this._setDisabled = setDisabled; this._frameOffset = null; this._frameOffsetPromise = null; @@ -142,6 +143,10 @@ class PopupProxy { try { const offset = await this._frameOffsetPromise; this._frameOffset = offset !== null ? offset : [0, 0]; + if (offset === null && this._setDisabled !== null) { + this._setDisabled(); + return; + } this._frameOffsetUpdatedAt = now; } catch (e) { logError(e); -- cgit v1.2.3 From b786e2da1912dfa7d707db628d54fb914189f7d1 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:55:16 +0300 Subject: move open shadow root iframe finder to a function --- ext/fg/js/frame-offset-forwarder.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index ac6e617d..2b48ba26 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -81,23 +81,9 @@ class FrameOffsetForwarder { } if (sourceFrame === null) { - const getShadowRootElements = (documentOrElement) => { - const elements = Array.from(documentOrElement.querySelectorAll('*')) - .filter((el) => !!el.shadowRoot); - const childElements = elements - .map((el) => el.shadowRoot) - .map(getShadowRootElements); - elements.push(childElements.flat()); - - return elements.flat(); - }; - - sourceFrame = getShadowRootElements(document) - .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) - .flat() - .find((el) => el.contentWindow === e.source); - + sourceFrame = this._getOpenShadowRootSourceFrame(e.source); if (!sourceFrame) { + // closed shadow root etc. this._forwardFrameOffsetOrigin(null, uniqueId); return; } @@ -110,6 +96,24 @@ class FrameOffsetForwarder { this._forwardFrameOffset(offset, uniqueId); } + _getOpenShadowRootSourceFrame(sourceWindow) { + const getShadowRootElements = (documentOrElement) => { + const elements = Array.from(documentOrElement.querySelectorAll('*')) + .filter((el) => !!el.shadowRoot); + const childElements = elements + .map((el) => el.shadowRoot) + .map(getShadowRootElements); + elements.push(childElements.flat()); + + return elements.flat(); + }; + + return getShadowRootElements(document) + .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) + .flat() + .find((el) => el.contentWindow === sourceWindow); + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } -- cgit v1.2.3 From 350a1139968ec3db4da95cd27c4ce8b5be45c56a Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 02:05:18 +0300 Subject: use getFullscreenElement to check fullscreen --- ext/fg/js/frontend-initialize.js | 3 ++- ext/fg/js/popup.js | 13 ++----------- ext/mixed/js/dom.js | 10 ++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 2e63c29f..2df59e20 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -16,6 +16,7 @@ */ /* global + * DOM * FrameOffsetForwarder * Frontend * PopupProxy @@ -95,7 +96,7 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen && iframePopupsInRootFrameAvailable) { + if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); popups.iframe = popup; } else if (proxy) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 99610e17..ae158263 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -16,6 +16,7 @@ */ /* global + * DOM * apiGetMessageToken * apiInjectStylesheet */ @@ -271,7 +272,7 @@ class Popup { } _onFullscreenChanged() { - const parent = (Popup._getFullscreenElement() || document.body || null); + const parent = (DOM.getFullscreenElement() || document.body || null); if (parent !== null && this._container.parentNode !== parent) { parent.appendChild(this._container); } @@ -365,16 +366,6 @@ class Popup { contentWindow.postMessage({action, params, token}, this._targetOrigin); } - static _getFullscreenElement() { - return ( - document.fullscreenElement || - document.msFullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement || - null - ); - } - static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 03acbb80..31ba33d6 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -62,4 +62,14 @@ class DOM { default: return false; } } + + static getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + null + ); + } } -- cgit v1.2.3 From c992e7f920f20c0c7cc55fddf6aba61e0f8b1641 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 03:35:00 +0300 Subject: add manual performance tests --- test/data/html/test-document3-frame1.html | 44 ++++++++++++++++++++++ test/data/html/test-document3-frame2.html | 62 +++++++++++++++++++++++++++++++ test/data/html/test-document3.html | 26 +++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 test/data/html/test-document3-frame1.html create mode 100644 test/data/html/test-document3-frame2.html create mode 100644 test/data/html/test-document3.html diff --git a/test/data/html/test-document3-frame1.html b/test/data/html/test-document3-frame1.html new file mode 100644 index 00000000..2ae906d2 --- /dev/null +++ b/test/data/html/test-document3-frame1.html @@ -0,0 +1,44 @@ + + + + + + Yomichan Manual Performance Tests + + +
+ +
Add elements
+ +
+ 1000 + 10000 + 100000 + 1000000 + +
+ +
+
+ +
+ diff --git a/test/data/html/test-document3-frame2.html b/test/data/html/test-document3-frame2.html new file mode 100644 index 00000000..c486e04b --- /dev/null +++ b/test/data/html/test-document3-frame2.html @@ -0,0 +1,62 @@ + + + + + + Yomichan Manual Performance Tests + + +
+ +
<iframe> element inside of an open shadow DOM.
+ +
+ + + +
Add elements
+ +
+ 1000 + 10000 + 100000 + 1000000 +
+ +
+
+ + +
+ diff --git a/test/data/html/test-document3.html b/test/data/html/test-document3.html new file mode 100644 index 00000000..3e7d5236 --- /dev/null +++ b/test/data/html/test-document3.html @@ -0,0 +1,26 @@ + + + + + + Yomichan Manual Performance Tests + + + + + +

Yomichan Manual Performance Tests

+

Testing Yomichan performance with artificially demanding cases in a real browser

+ +
+
<iframe> element.
+ +
+ +
+
<iframe> element containing an <iframe> element inside of an open shadow DOM.
+ +
+ + + -- cgit v1.2.3 From bb3ad78e373b01b64a24fc46712f24964528a24f Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 16:48:49 +0300 Subject: optimize source frame finding --- ext/fg/js/frame-offset-forwarder.js | 66 +++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 2b48ba26..c2df7581 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -22,6 +22,7 @@ class FrameOffsetForwarder { constructor() { this._started = false; + this._frameCache = new Set(); this._forwardFrameOffset = ( window !== window.parent ? @@ -73,20 +74,11 @@ class FrameOffsetForwarder { } _onGetFrameOffset(offset, uniqueId, e) { - let sourceFrame = null; - for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) { - if (frame.contentWindow !== e.source) { continue; } - sourceFrame = frame; - break; - } - + const sourceFrame = this._findFrameWithContentWindow(e.source); if (sourceFrame === null) { - sourceFrame = this._getOpenShadowRootSourceFrame(e.source); - if (!sourceFrame) { - // closed shadow root etc. - this._forwardFrameOffsetOrigin(null, uniqueId); - return; - } + // closed shadow root etc. + this._forwardFrameOffsetOrigin(null, uniqueId); + return; } const [forwardedX, forwardedY] = offset; @@ -96,22 +88,38 @@ class FrameOffsetForwarder { this._forwardFrameOffset(offset, uniqueId); } - _getOpenShadowRootSourceFrame(sourceWindow) { - const getShadowRootElements = (documentOrElement) => { - const elements = Array.from(documentOrElement.querySelectorAll('*')) - .filter((el) => !!el.shadowRoot); - const childElements = elements - .map((el) => el.shadowRoot) - .map(getShadowRootElements); - elements.push(childElements.flat()); - - return elements.flat(); - }; - - return getShadowRootElements(document) - .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) - .flat() - .find((el) => el.contentWindow === sourceWindow); + _findFrameWithContentWindow(contentWindow) { + const elements = [ + ...this._frameCache, + // will contain duplicates, but frame elements are cheap to handle + ...document.querySelectorAll('frame, iframe:not(.yomichan-float)'), + document.documentElement + ]; + const ELEMENT_NODE = Node.ELEMENT_NODE; + while (elements.length > 0) { + const element = elements.shift(); + if (element.contentWindow === contentWindow) { + this._frameCache.add(element); + return element; + } + + const shadowRoot = element.shadowRoot; + if (shadowRoot) { + for (const child of shadowRoot.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } + + for (const child of element.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } + + return null; } _forwardFrameOffsetParent(offset, uniqueId) { -- cgit v1.2.3 From 66354f1f9e866fd31f6bb0365024a39697a54079 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 17:18:33 +0300 Subject: lazy load element sources --- ext/fg/js/frame-offset-forwarder.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index c2df7581..4b77d5ed 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -89,14 +89,27 @@ class FrameOffsetForwarder { } _findFrameWithContentWindow(contentWindow) { - const elements = [ - ...this._frameCache, + const elementSources = [ + () => [...this._frameCache], // will contain duplicates, but frame elements are cheap to handle - ...document.querySelectorAll('frame, iframe:not(.yomichan-float)'), - document.documentElement + () => [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')], + () => [document.documentElement] ]; + const getMoreElements = () => { + while (true) { + const source = elementSources.shift(); + if (source) { + const elements = source(); + if (elements.length === 0) { continue; } + return elements; + } + return []; + } + }; + + const elements = []; const ELEMENT_NODE = Node.ELEMENT_NODE; - while (elements.length > 0) { + while (elements.length > 0 || elements.push(...getMoreElements())) { const element = elements.shift(); if (element.contentWindow === contentWindow) { this._frameCache.add(element); -- cgit v1.2.3 From 691b7398490bbf247070cd38603e51c7a6b66121 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 17:54:49 +0300 Subject: cache closed shadow dom content windows --- ext/fg/js/frame-offset-forwarder.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 4b77d5ed..72731605 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -23,6 +23,7 @@ class FrameOffsetForwarder { constructor() { this._started = false; this._frameCache = new Set(); + this._unreachableContentWindowCache = new Set(); this._forwardFrameOffset = ( window !== window.parent ? @@ -74,9 +75,13 @@ class FrameOffsetForwarder { } _onGetFrameOffset(offset, uniqueId, e) { - const sourceFrame = this._findFrameWithContentWindow(e.source); + let sourceFrame = null; + if (!this._unreachableContentWindowCache.has(e.source)) { + sourceFrame = this._findFrameWithContentWindow(e.source); + } if (sourceFrame === null) { // closed shadow root etc. + this._unreachableContentWindowCache.add(e.source); this._forwardFrameOffsetOrigin(null, uniqueId); return; } -- cgit v1.2.3 From a81c33b60aac0752ccca06f5183632146f6c6bf0 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 22:08:38 +0300 Subject: simplify element source lazy load --- ext/fg/js/frame-offset-forwarder.js | 56 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 72731605..f40c642d 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -94,52 +94,42 @@ class FrameOffsetForwarder { } _findFrameWithContentWindow(contentWindow) { - const elementSources = [ - () => [...this._frameCache], - // will contain duplicates, but frame elements are cheap to handle - () => [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')], - () => [document.documentElement] - ]; - const getMoreElements = () => { - while (true) { - const source = elementSources.shift(); - if (source) { - const elements = source(); - if (elements.length === 0) { continue; } - return elements; + const ELEMENT_NODE = Node.ELEMENT_NODE; + for (const elements of this._getFrameElementSources()) { + while (elements.length > 0) { + const element = elements.shift(); + if (element.contentWindow === contentWindow) { + this._frameCache.add(element); + return element; } - return []; - } - }; - const elements = []; - const ELEMENT_NODE = Node.ELEMENT_NODE; - while (elements.length > 0 || elements.push(...getMoreElements())) { - const element = elements.shift(); - if (element.contentWindow === contentWindow) { - this._frameCache.add(element); - return element; - } + const shadowRoot = element.shadowRoot; + if (shadowRoot) { + for (const child of shadowRoot.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } - const shadowRoot = element.shadowRoot; - if (shadowRoot) { - for (const child of shadowRoot.children) { + for (const child of element.children) { if (child.nodeType === ELEMENT_NODE) { elements.push(child); } } } - - for (const child of element.children) { - if (child.nodeType === ELEMENT_NODE) { - elements.push(child); - } - } } return null; } + *_getFrameElementSources() { + yield [...this._frameCache]; + // will contain duplicates, but frame elements are cheap to handle + yield [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')]; + yield [document.documentElement]; + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } -- cgit v1.2.3 From d66ca93ce4d6a4c9814bac4cc508c24ff87b8f69 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 22:26:11 +0300 Subject: cache invalidation --- ext/fg/js/frame-offset-forwarder.js | 29 ++++++++++++++++++++++++++--- ext/manifest.json | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index f40c642d..1a2f3e1e 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -22,6 +22,8 @@ class FrameOffsetForwarder { constructor() { this._started = false; + + this._cacheMaxSize = 1000; this._frameCache = new Set(); this._unreachableContentWindowCache = new Set(); @@ -81,7 +83,7 @@ class FrameOffsetForwarder { } if (sourceFrame === null) { // closed shadow root etc. - this._unreachableContentWindowCache.add(e.source); + this._addToCache(this._unreachableContentWindowCache, e.source); this._forwardFrameOffsetOrigin(null, uniqueId); return; } @@ -99,7 +101,7 @@ class FrameOffsetForwarder { while (elements.length > 0) { const element = elements.shift(); if (element.contentWindow === contentWindow) { - this._frameCache.add(element); + this._addToCache(this._frameCache, element); return element; } @@ -124,12 +126,33 @@ class FrameOffsetForwarder { } *_getFrameElementSources() { - yield [...this._frameCache]; + const frameCache = []; + for (const frame of this._frameCache) { + // removed from DOM + if (!frame.isConnected) { + this._frameCache.delete(frame); + continue; + } + frameCache.push(frame); + } + yield frameCache; // will contain duplicates, but frame elements are cheap to handle yield [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')]; yield [document.documentElement]; } + _addToCache(cache, value) { + let freeSlots = this._cacheMaxSize - cache.size; + if (freeSlots <= 0) { + for (const cachedValue of cache) { + cache.delete(cachedValue); + ++freeSlots; + if (freeSlots > 0) { break; } + } + } + cache.add(value); + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } diff --git a/ext/manifest.json b/ext/manifest.json index 452b642c..d383dab0 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -71,7 +71,7 @@ "applications": { "gecko": { "id": "alex@foosoft.net", - "strict_min_version": "52.0" + "strict_min_version": "53.0" } } } -- cgit v1.2.3 From 6498556ec7ddd3e0896ef47cce297bcbf938defb Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 20:45:23 -0400 Subject: Update isPrepared to be consistent with DisplaySearch's isPrepared --- ext/bg/js/backend.js | 12 ++++++++---- ext/bg/js/util.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9d1fa6c1..f5bd36f5 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -67,8 +67,6 @@ class Backend { url: window.location.href }; - this.isPrepared = false; - this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target'); this.popupWindow = null; @@ -77,6 +75,8 @@ class Backend { this.messageToken = yomichan.generateId(16); + this._isPrepared = false; + this._messageHandlers = new Map([ ['yomichanCoreReady', {handler: this._onApiYomichanCoreReady.bind(this), async: false}], ['optionsSchemaGet', {handler: this._onApiOptionsSchemaGet.bind(this), async: false}], @@ -144,8 +144,6 @@ class Backend { } chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); - this.isPrepared = true; - const options = this.getOptions(this.optionsContext); if (options.general.showGuide) { chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')}); @@ -156,6 +154,12 @@ class Backend { this._sendMessageAllTabs('backendPrepared'); const callback = () => this.checkLastError(chrome.runtime.lastError); chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); + + this._isPrepared = true; + } + + isPrepared() { + return this._isPrepared; } _sendMessageAllTabs(action, params={}) { diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 5edcc193..d2fb0e49 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -60,7 +60,7 @@ function utilBackgroundFunctionIsolate(func) { function utilBackend() { const backend = chrome.extension.getBackgroundPage().yomichanBackend; - if (!backend.isPrepared) { + if (!backend.isPrepared()) { throw new Error('Backend not ready yet'); } return backend; -- cgit v1.2.3 From c9704b5c5e3b8f78888adaa2dcd4fa54f28c9db1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 20:46:11 -0400 Subject: Update when/how badge state is changed --- ext/bg/js/backend.js | 96 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index f5bd36f5..1589524b 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -75,6 +75,7 @@ class Backend { this.messageToken = yomichan.generateId(16); + this._defaultBrowserActionTitle = null; this._isPrepared = false; this._messageHandlers = new Map([ @@ -121,6 +122,8 @@ class Backend { } async prepare() { + this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); + this._updateBadge(); await this.database.prepare(); await this.translator.prepare(); @@ -156,6 +159,7 @@ class Backend { chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); this._isPrepared = true; + this._updateBadge(); } isPrepared() { @@ -211,15 +215,7 @@ class Backend { applyOptions() { const options = this.getOptions(this.optionsContext); - if (!options.general.enable) { - this.setExtensionBadgeBackgroundColor('#555555'); - this.setExtensionBadgeText('off'); - } else if (!dictConfigured(options)) { - this.setExtensionBadgeBackgroundColor('#f0ad4e'); - this.setExtensionBadgeText('!'); - } else { - this.setExtensionBadgeText(''); - } + this._updateBadge(); this.anki.setServer(options.anki.server); this.anki.setEnabled(options.anki.enable); @@ -299,18 +295,6 @@ class Backend { return true; } - setExtensionBadgeBackgroundColor(color) { - if (typeof chrome.browserAction.setBadgeBackgroundColor === 'function') { - chrome.browserAction.setBadgeBackgroundColor({color}); - } - } - - setExtensionBadgeText(text) { - if (typeof chrome.browserAction.setBadgeText === 'function') { - chrome.browserAction.setBadgeText({text}); - } - } - checkLastError() { // NOP } @@ -868,6 +852,76 @@ class Backend { } } + _getBrowserIconTitle() { + return ( + chrome.browserAction !== null && + typeof chrome.browserAction === 'object' && + typeof chrome.browserAction.getTitle === 'function' ? + new Promise((resolve) => chrome.browserAction.getTitle({}, resolve)) : + Promise.resolve('') + ); + } + + _updateBadge() { + let title = this._defaultBrowserActionTitle; + if ( + title === null || + chrome.browserAction === null || + typeof chrome.browserAction !== 'object' + ) { + // Not ready or invalid + return; + } + + let text = ''; + let color = null; + let status = null; + + if (!this._isPrepared) { + text = '...'; + color = '#f0ad4e'; + status = 'Loading'; + } else if (!this._anyOptionsMatches((options) => options.general.enable)) { + text = 'off'; + color = '#555555'; + status = 'Disabled'; + } else if (!this._anyOptionsMatches((options) => this._isAnyDictionaryEnabled(options))) { + text = '!'; + color = '#f0ad4e'; + status = 'No dictionaries installed'; + } + + if (color !== null && typeof chrome.browserAction.setBadgeBackgroundColor === 'function') { + chrome.browserAction.setBadgeBackgroundColor({color}); + } + if (text !== null && typeof chrome.browserAction.setBadgeText === 'function') { + chrome.browserAction.setBadgeText({text}); + } + if (typeof chrome.browserAction.setTitle === 'function') { + if (status !== null) { + title = `${title} - ${status}`; + } + chrome.browserAction.setTitle({title}); + } + } + + _isAnyDictionaryEnabled(options) { + for (const {enabled} of Object.values(options.dictionaries)) { + if (enabled) { + return true; + } + } + return false; + } + + _anyOptionsMatches(predicate) { + for (const {options} of this.options.profiles) { + const value = predicate(options); + if (value) { return value; } + } + return false; + } + async _renderTemplate(template, data) { return handlebarsRenderDynamic(template, data); } -- cgit v1.2.3 From 5c5c70326d797b819cff33390e04dde93c63669c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 20:53:18 -0400 Subject: Add a delay before showing the loading state Intended to prevent flickering when startup is quick --- ext/bg/js/backend.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 1589524b..9cfa621a 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -77,6 +77,7 @@ class Backend { this._defaultBrowserActionTitle = null; this._isPrepared = false; + this._badgePrepareDelayTimer = null; this._messageHandlers = new Map([ ['yomichanCoreReady', {handler: this._onApiYomichanCoreReady.bind(this), async: false}], @@ -123,6 +124,10 @@ class Backend { async prepare() { this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); + this._badgePrepareDelayTimer = setTimeout(() => { + this._badgePrepareDelayTimer = null; + this._updateBadge(); + }, 1000); this._updateBadge(); await this.database.prepare(); await this.translator.prepare(); @@ -158,6 +163,11 @@ class Backend { const callback = () => this.checkLastError(chrome.runtime.lastError); chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); + if (this._badgePrepareDelayTimer !== null) { + clearTimeout(this._badgePrepareDelayTimer); + this._badgePrepareDelayTimer = null; + } + this._isPrepared = true; this._updateBadge(); } @@ -878,9 +888,11 @@ class Backend { let status = null; if (!this._isPrepared) { - text = '...'; - color = '#f0ad4e'; - status = 'Loading'; + if (this._badgePrepareDelayTimer === null) { + text = '...'; + color = '#f0ad4e'; + status = 'Loading'; + } } else if (!this._anyOptionsMatches((options) => options.general.enable)) { text = 'off'; color = '#555555'; -- cgit v1.2.3 From dee7d924a8555226721bbbdb045b86a21426f60b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 20:58:52 -0400 Subject: Show error status for prepare() errors --- ext/bg/js/backend.js | 84 ++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9cfa621a..9f466647 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -77,6 +77,7 @@ class Backend { this._defaultBrowserActionTitle = null; this._isPrepared = false; + this._prepareError = false; this._badgePrepareDelayTimer = null; this._messageHandlers = new Map([ @@ -123,53 +124,56 @@ class Backend { } async prepare() { - this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); - this._badgePrepareDelayTimer = setTimeout(() => { - this._badgePrepareDelayTimer = null; + try { + this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); + this._badgePrepareDelayTimer = setTimeout(() => { + this._badgePrepareDelayTimer = null; + this._updateBadge(); + }, 1000); this._updateBadge(); - }, 1000); - this._updateBadge(); - await this.database.prepare(); - await this.translator.prepare(); - this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); - this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET'); - this.options = await optionsLoad(); - try { + await this.database.prepare(); + await this.translator.prepare(); + + this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); + this.defaultAnkiFieldTemplates = await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET'); + this.options = await optionsLoad(); this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, this.options); - } catch (e) { - // This shouldn't happen, but catch errors just in case of bugs - logError(e); - } - this.onOptionsUpdated('background'); + this.onOptionsUpdated('background'); - if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) { - chrome.commands.onCommand.addListener(this._runCommand.bind(this)); - } - if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) { - chrome.tabs.onZoomChange.addListener(this._onZoomChange.bind(this)); - } - chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); + if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) { + chrome.commands.onCommand.addListener(this._runCommand.bind(this)); + } + if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) { + chrome.tabs.onZoomChange.addListener(this._onZoomChange.bind(this)); + } + chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); - const options = this.getOptions(this.optionsContext); - if (options.general.showGuide) { - chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')}); - } + const options = this.getOptions(this.optionsContext); + if (options.general.showGuide) { + chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')}); + } - this.clipboardMonitor.on('change', this._onClipboardText.bind(this)); + this.clipboardMonitor.on('change', this._onClipboardText.bind(this)); - this._sendMessageAllTabs('backendPrepared'); - const callback = () => this.checkLastError(chrome.runtime.lastError); - chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); + this._sendMessageAllTabs('backendPrepared'); + const callback = () => this.checkLastError(chrome.runtime.lastError); + chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); - if (this._badgePrepareDelayTimer !== null) { - clearTimeout(this._badgePrepareDelayTimer); - this._badgePrepareDelayTimer = null; - } + this._isPrepared = true; + } catch (e) { + this._prepareError = true; + logError(e); + throw e; + } finally { + if (this._badgePrepareDelayTimer !== null) { + clearTimeout(this._badgePrepareDelayTimer); + this._badgePrepareDelayTimer = null; + } - this._isPrepared = true; - this._updateBadge(); + this._updateBadge(); + } } isPrepared() { @@ -888,7 +892,11 @@ class Backend { let status = null; if (!this._isPrepared) { - if (this._badgePrepareDelayTimer === null) { + if (this._prepareError !== null) { + text = '!!'; + color = '#f04e4e'; + status = 'Error'; + } else if (this._badgePrepareDelayTimer === null) { text = '...'; color = '#f0ad4e'; status = 'Loading'; -- cgit v1.2.3 From 66ef7301198e4baa827fafe002818896a8bb6483 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 19:19:38 -0400 Subject: Update style, use isObject --- ext/bg/js/backend.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9f466647..aacebd2c 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -868,21 +868,16 @@ class Backend { _getBrowserIconTitle() { return ( - chrome.browserAction !== null && - typeof chrome.browserAction === 'object' && + isObject(chrome.browserAction) && typeof chrome.browserAction.getTitle === 'function' ? - new Promise((resolve) => chrome.browserAction.getTitle({}, resolve)) : - Promise.resolve('') + new Promise((resolve) => chrome.browserAction.getTitle({}, resolve)) : + Promise.resolve('') ); } _updateBadge() { let title = this._defaultBrowserActionTitle; - if ( - title === null || - chrome.browserAction === null || - typeof chrome.browserAction !== 'object' - ) { + if (title === null || !isObject(chrome.browserAction)) { // Not ready or invalid return; } -- cgit v1.2.3 From 4638985b16951b1b1b3895ca0bf52575f1f0bd6b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 17 Apr 2020 22:16:51 -0400 Subject: Remove unused global --- ext/bg/js/backend.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index aacebd2c..8c0b531f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -28,7 +28,6 @@ * Mecab * Translator * conditionsTestValue - * dictConfigured * dictTermsSort * handlebarsRenderDynamic * jp -- cgit v1.2.3 From 51d756eefc26402e4d6f9635acc77ba7f642d15b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 18 Apr 2020 21:15:15 -0400 Subject: Fix _prepareError check --- ext/bg/js/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 8c0b531f..e0814c17 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -886,7 +886,7 @@ class Backend { let status = null; if (!this._isPrepared) { - if (this._prepareError !== null) { + if (this._prepareError) { text = '!!'; color = '#f04e4e'; status = 'Error'; -- cgit v1.2.3 From 8106f4744b07833526d16acf656eda11d29b99ad Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 1 Mar 2020 22:36:42 -0500 Subject: Add support for importing and storing media files --- ext/bg/background.html | 1 + ext/bg/data/dictionary-term-bank-v3-schema.json | 81 +++++++++++++++++++++- ext/bg/js/database.js | 11 ++- ext/bg/js/dictionary-importer.js | 90 +++++++++++++++++++++++++ ext/bg/js/media-utility.js | 75 +++++++++++++++++++++ 5 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 ext/bg/js/media-utility.js diff --git a/ext/bg/background.html b/ext/bg/background.html index afe9c5d1..f1006f8d 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -36,6 +36,7 @@ + diff --git a/ext/bg/data/dictionary-term-bank-v3-schema.json b/ext/bg/data/dictionary-term-bank-v3-schema.json index bb982e36..4790e561 100644 --- a/ext/bg/data/dictionary-term-bank-v3-schema.json +++ b/ext/bg/data/dictionary-term-bank-v3-schema.json @@ -31,8 +31,85 @@ "type": "array", "description": "Array of definitions for the term/expression.", "items": { - "type": "string", - "description": "Single definition for the term/expression." + "oneOf": [ + { + "type": "string", + "description": "Single definition for the term/expression." + }, + { + "type": "object", + "description": "Single detailed definition for the term/expression.", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "description": "The type of the data for this definition.", + "enum": ["text", "image"] + } + }, + "oneOf": [ + { + "required": [ + "type", + "text" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string", + "description": "Single definition for the term/expression." + } + } + }, + { + "required": [ + "type", + "path" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["image"] + }, + "path": { + "type": "string", + "description": "Path to the image file in the archive." + }, + "width": { + "type": "integer", + "description": "Preferred width of the image.", + "minimum": 1 + }, + "height": { + "type": "integer", + "description": "Preferred width of the image.", + "minimum": 1 + }, + "title": { + "type": "string", + "description": "Hover text for the image." + }, + "description": { + "type": "string", + "description": "Description of the image." + }, + "pixelated": { + "type": "boolean", + "description": "Whether or not the image should appear pixelated at sizes larger than the image's native resolution.", + "default": false + } + } + } + ] + } + ] } }, { diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 260c815a..0c7eee6a 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -33,7 +33,7 @@ class Database { } try { - this.db = await Database._open('dict', 5, (db, transaction, oldVersion) => { + this.db = await Database._open('dict', 6, (db, transaction, oldVersion) => { Database._upgrade(db, transaction, oldVersion, [ { version: 2, @@ -90,6 +90,15 @@ class Database { indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse'] } } + }, + { + version: 6, + stores: { + media: { + primaryKey: {keyPath: 'id', autoIncrement: true}, + indices: ['dictionary', 'path'] + } + } } ]); }); diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js index bf6809ec..8a4497a3 100644 --- a/ext/bg/js/dictionary-importer.js +++ b/ext/bg/js/dictionary-importer.js @@ -18,6 +18,7 @@ /* global * JSZip * JsonSchema + * mediaUtility * requestJson */ @@ -148,6 +149,22 @@ class DictionaryImporter { } } + // Extended data support + const extendedDataContext = { + archive, + media: new Map() + }; + for (const entry of termList) { + const glossaryList = entry.glossary; + for (let i = 0, ii = glossaryList.length; i < ii; ++i) { + const glossary = glossaryList[i]; + if (typeof glossary !== 'object' || glossary === null) { continue; } + glossaryList[i] = await this._formatDictionaryTermGlossaryObject(glossary, extendedDataContext, entry); + } + } + + const media = [...extendedDataContext.media.values()]; + // Add dictionary const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported}); @@ -188,6 +205,7 @@ class DictionaryImporter { await bulkAdd('kanji', kanjiList); await bulkAdd('kanjiMeta', kanjiMetaList); await bulkAdd('tagMeta', tagList); + await bulkAdd('media', media); return {result: summary, errors}; } @@ -275,4 +293,76 @@ class DictionaryImporter { return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank]; } + + async _formatDictionaryTermGlossaryObject(data, context, entry) { + switch (data.type) { + case 'text': + return data.text; + case 'image': + return await this._formatDictionaryTermGlossaryImage(data, context, entry); + default: + throw new Error(`Unhandled data type: ${data.type}`); + } + } + + async _formatDictionaryTermGlossaryImage(data, context, entry) { + const dictionary = entry.dictionary; + const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated} = data; + if (context.media.has(path)) { + // Already exists + return data; + } + + let errorSource = entry.expression; + if (entry.reading.length > 0) { + errorSource += ` (${entry.reading});`; + } + + const file = context.archive.file(path); + if (file === null) { + throw new Error(`Could not find image at path ${JSON.stringify(path)} for ${errorSource}`); + } + + const source = await file.async('base64'); + const mediaType = mediaUtility.getImageMediaTypeFromFileName(path); + if (mediaType === null) { + throw new Error(`Could not determine media type for image at path ${JSON.stringify(path)} for ${errorSource}`); + } + + let image; + try { + image = await mediaUtility.loadImage(mediaType, source); + } catch (e) { + throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`); + } + + const width = image.naturalWidth; + const height = image.naturalHeight; + + // Create image data + const mediaData = { + dictionary, + path, + mediaType, + width, + height, + source + }; + context.media.set(path, mediaData); + + // Create new data + const newData = { + type: 'image', + path, + width, + height + }; + if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; } + if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; } + if (typeof title === 'string') { newData.title = title; } + if (typeof description === 'string') { newData.description = description; } + if (typeof pixelated === 'boolean') { newData.pixelated = pixelated; } + + return newData; + } } diff --git a/ext/bg/js/media-utility.js b/ext/bg/js/media-utility.js new file mode 100644 index 00000000..24686838 --- /dev/null +++ b/ext/bg/js/media-utility.js @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +const mediaUtility = (() => { + function getFileNameExtension(fileName) { + const match = /\.[^.]*$/.exec(fileName); + return match !== null ? match[0] : ''; + } + + function getImageMediaTypeFromFileName(fileName) { + switch (getFileNameExtension(fileName).toLowerCase()) { + case '.apng': + return 'image/apng'; + case '.bmp': + return 'image/bmp'; + case '.gif': + return 'image/gif'; + case '.ico': + case '.cur': + return 'image/x-icon'; + case '.jpg': + case '.jpeg': + case '.jfif': + case '.pjpeg': + case '.pjp': + return 'image/jpeg'; + case '.png': + return 'image/png'; + case '.svg': + return 'image/svg+xml'; + case '.tif': + case '.tiff': + return 'image/tiff'; + case '.webp': + return 'image/webp'; + default: + return null; + } + } + + function loadImage(mediaType, base64Source) { + return new Promise((resolve, reject) => { + const image = new Image(); + const eventListeners = new EventListenerCollection(); + eventListeners.addEventListener(image, 'load', () => { + eventListeners.removeAllEventListeners(); + resolve(image); + }, false); + eventListeners.addEventListener(image, 'error', () => { + eventListeners.removeAllEventListeners(); + reject(new Error('Image failed to load')); + }, false); + image.src = `data:${mediaType};base64,${base64Source}`; + }); + } + + return { + getImageMediaTypeFromFileName, + loadImage + }; +})(); -- cgit v1.2.3 From fd6ea0e404da2657f110599061af4034a524283a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 14:23:02 -0400 Subject: Add API for getting media data --- ext/bg/js/backend.js | 7 ++++++- ext/bg/js/database.js | 32 ++++++++++++++++++++++++++++++++ ext/mixed/js/api.js | 4 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index e0814c17..8a19203f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -111,7 +111,8 @@ class Backend { ['getAnkiModelFieldNames', {handler: this._onApiGetAnkiModelFieldNames.bind(this), async: true}], ['getDictionaryInfo', {handler: this._onApiGetDictionaryInfo.bind(this), async: true}], ['getDictionaryCounts', {handler: this._onApiGetDictionaryCounts.bind(this), async: true}], - ['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}] + ['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}], + ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}] ]); this._commandHandlers = new Map([ @@ -762,6 +763,10 @@ class Backend { return await this.translator.purgeDatabase(); } + async _onApiGetMedia({targets}) { + return await this.database.getMedia(targets); + } + // Command handlers async _onCommandSearch(params) { diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 0c7eee6a..16612403 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -277,6 +277,34 @@ class Database { return result; } + async getMedia(targets) { + this._validate(); + + const count = targets.length; + const promises = []; + const results = new Array(count).fill(null); + const createResult = Database._createMedia; + const processRow = (row, [index, dictionaryName]) => { + if (row.dictionary === dictionaryName) { + results[index] = createResult(row, index); + } + }; + + const transaction = this.db.transaction(['media'], 'readonly'); + const objectStore = transaction.objectStore('media'); + const index = objectStore.index('path'); + + for (let i = 0; i < count; ++i) { + const {path, dictionaryName} = targets[i]; + const only = IDBKeyRange.only(path); + promises.push(Database._getAll(index, only, [i, dictionaryName], processRow)); + } + + await Promise.all(promises); + + return results; + } + async getDictionaryInfo() { this._validate(); @@ -441,6 +469,10 @@ class Database { return {character, mode, data, dictionary, index}; } + static _createMedia(row, index) { + return Object.assign({}, row, {index}); + } + static _getAll(dbIndex, query, context, processRow) { const fn = typeof dbIndex.getAll === 'function' ? Database._getAllFast : Database._getAllUsingCursor; return fn(dbIndex, query, context, processRow); diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index c97dc687..52f41646 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -140,6 +140,10 @@ function apiPurgeDatabase() { return _apiInvoke('purgeDatabase'); } +function apiGetMedia(targets) { + return _apiInvoke('getMedia', {targets}); +} + function _apiInvoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { -- cgit v1.2.3 From ac603d54a3a53bec2881199756f3dd6a1aa44057 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 14:23:49 -0400 Subject: Add support for displaying images --- ext/bg/search.html | 1 + ext/fg/float.html | 1 + ext/mixed/css/display-dark.css | 7 +++ ext/mixed/css/display-default.css | 7 +++ ext/mixed/css/display.css | 96 ++++++++++++++++++++++++++++++++++ ext/mixed/display-templates.html | 1 + ext/mixed/js/display-generator.js | 85 ++++++++++++++++++++++++++++-- ext/mixed/js/display.js | 6 ++- ext/mixed/js/media-loader.js | 107 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 ext/mixed/js/media-loader.js diff --git a/ext/bg/search.html b/ext/bg/search.html index eacc1893..fe88e264 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -85,6 +85,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index 3ccf68eb..c8ea9b67 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -51,6 +51,7 @@ + diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css index e4549bbf..acfa2782 100644 --- a/ext/mixed/css/display-dark.css +++ b/ext/mixed/css/display-dark.css @@ -94,3 +94,10 @@ h2 { border-bottom-color: #2f2f2f; } #term-pitch-accent-graph-dot-downstep>circle:last-of-type { fill: #ffffff; } + +.term-glossary-image-container { + background-color: #2f2f2f; +} +.term-glossary-image-container-overlay { + color: #888888; +} diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css index 7bcb1014..70f81eb6 100644 --- a/ext/mixed/css/display-default.css +++ b/ext/mixed/css/display-default.css @@ -94,3 +94,10 @@ h2 { border-bottom-color: #eeeeee; } #term-pitch-accent-graph-dot-downstep>circle:last-of-type { fill: #000000; } + +.term-glossary-image-container { + background-color: #eeeeee; +} +.term-glossary-image-container-overlay { + color: #777777; +} diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index d1a54064..ca1fa371 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -611,6 +611,102 @@ button.action-button { stroke-width: 5; } +.term-glossary-image-container { + display: inline-block; + white-space: nowrap; + max-width: 100%; + position: relative; + vertical-align: top; + line-height: 0; + font-size: 0.07142857em; /* 14px => 1px */ + overflow: hidden; +} + +.term-glossary-image-link { + cursor: inherit; + color: inherit; +} + +.term-glossary-image-link[href]:hover { + cursor: pointer; +} + +.term-glossary-image-container-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + font-size: 14em; /* 1px => 14px; */ + line-height: 1.42857143; /* 14px => 20px */ + display: table; + table-layout: fixed; + white-space: normal; +} + +.term-glossary-item[data-has-image=true][data-image-load-state=load-error] .term-glossary-image-container-overlay:after { + content: "Image failed to load"; + display: table-cell; + width: 100%; + height: 100%; + vertical-align: middle; + text-align: center; + padding: 0.25em; +} + +.term-glossary-image { + display: inline-block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + vertical-align: top; + object-fit: contain; + border: none; + outline: none; +} + +.term-glossary-image:not([src]) { + display: none; +} + +.term-glossary-image[data-pixelated=true] { + image-rendering: auto; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: pixelated; + image-rendering: crisp-edges; +} + +.term-glossary-image-aspect-ratio-sizer { + content: ""; + display: inline-block; + width: 0; + vertical-align: top; + font-size: 0; +} + +.term-glossary-image-link-text:before { + content: "["; +} + +.term-glossary-image-link-text:after { + content: "]"; +} + +:root[data-compact-glossaries=true] .term-glossary-image-container { + display: none; +} + +:root:not([data-compact-glossaries=true]) .term-glossary-image-link-text { + display: none; +} + +:root:not([data-compact-glossaries=true]) .term-glossary-image-description { + display: block; +} + /* * Kanji diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html index 3baa8293..5ecf2240 100644 --- a/ext/mixed/display-templates.html +++ b/ext/mixed/display-templates.html @@ -35,6 +35,7 @@ + - +