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/mixed/js/core.js | 9 +++++++++
1 file changed, 9 insertions(+)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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 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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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 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(-)
(limited to 'ext/mixed')
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(-)
(limited to 'ext/mixed')
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 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(-)
(limited to 'ext/mixed')
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 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(-)
(limited to 'ext/mixed')
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
(limited to 'ext/mixed')
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 @@
+ Image
- Image
+ Image
- - Click on the icon in the browser toolbar to open the Yomichan actions dialog.
+ - Click on the icon in the browser toolbar to open the Yomichan actions dialog.
- Click on the monkey wrench icon in the middle to open the options page.
- Import the dictionaries you wish to use for term and Kanji searches.
- Hold down Shift key or the middle mouse button as you move your mouse over text to display definitions.
diff --git a/ext/bg/legal.html b/ext/bg/legal.html
index 78acf79a..1ee9a28c 100644
--- a/ext/bg/legal.html
+++ b/ext/bg/legal.html
@@ -6,6 +6,7 @@
Yomichan Legal
+
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 9a824776..8ed6c838 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -6,6 +6,7 @@
Yomichan Search
+
diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html
index 66475b7c..a332fb22 100644
--- a/ext/bg/settings-popup-preview.html
+++ b/ext/bg/settings-popup-preview.html
@@ -6,6 +6,7 @@
Yomichan Popup Preview
+
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 96c1db82..f0236193 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -6,6 +6,7 @@
Yomichan Options
+
diff --git a/ext/fg/float.html b/ext/fg/float.html
index 07c3c9e6..deb9e9d2 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -6,6 +6,7 @@
+
diff --git a/ext/manifest.json b/ext/manifest.json
index 4f35b03c..3cb634f0 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -4,9 +4,26 @@
"version": "20.4.18.0",
"description": "Japanese dictionary with Anki integration",
- "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
+ "icons": {
+ "16": "mixed/img/icon16.png",
+ "19": "mixed/img/icon19.png",
+ "32": "mixed/img/icon32.png",
+ "38": "mixed/img/icon38.png",
+ "48": "mixed/img/icon48.png",
+ "64": "mixed/img/icon48.png",
+ "128": "mixed/img/icon128.png"
+ },
"browser_action": {
- "default_icon": {"19": "mixed/img/icon19.png", "38": "mixed/img/icon38.png"},
+ "default_icon": {
+ "16": "mixed/img/icon16.png",
+ "19": "mixed/img/icon19.png",
+ "32": "mixed/img/icon32.png",
+ "38": "mixed/img/icon38.png",
+ "48": "mixed/img/icon48.png",
+ "64": "mixed/img/icon48.png",
+ "128": "mixed/img/icon128.png"
+ },
+ "default_title": "Yomichan",
"default_popup": "bg/context.html"
},
diff --git a/ext/mixed/img/icon32.png b/ext/mixed/img/icon32.png
new file mode 100644
index 00000000..05f2f064
Binary files /dev/null and b/ext/mixed/img/icon32.png differ
diff --git a/ext/mixed/img/yomichan-icon.svg b/ext/mixed/img/yomichan-icon.svg
new file mode 100644
index 00000000..f15ab0aa
--- /dev/null
+++ b/ext/mixed/img/yomichan-icon.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/resources/icons.svg b/resources/icons.svg
index 4bc46c02..f096947b 100644
--- a/resources/icons.svg
+++ b/resources/icons.svg
@@ -15,10 +15,11 @@
viewBox="0 0 16 16"
version="1.1"
id="svg8"
- inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="icons.svg"
- inkscape:export-xdpi="96"
- inkscape:export-ydpi="96">
+ inkscape:export-xdpi="192"
+ inkscape:export-ydpi="192"
+ inkscape:export-filename="../ext/mixed/img/icon32.png">
image/svg+xml
-
+
@@ -520,7 +521,7 @@
style="image-rendering:optimizeSpeed"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlko qCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94V TUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpK tE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw 9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0Hgl WNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1V MzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8 w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9 XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVP C4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnj ahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQy u/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg== "
id="image4539"
- x="4.7683716e-007"
+ x="4.7683716e-07"
y="292.76669" />
+
+
+
+
+
+
+
+
--
cgit v1.2.3
From 48c7010f4ea8daafd30e5650625c377affa0cecd Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Mon, 27 Apr 2020 18:10:37 -0400
Subject: Frontend refactor (part 1) (#484)
* Remove _getVisualViewportScale
* Use super's mouse event listener definitions
* Remove redundant override
* Remove getTouchEventListeners override
* Rename Display.onSearchClear to onEscape
* Change onSearchClear to clearSelection and use an event
* Update how text is marked for selection and deselection
* Replace onError with yomichan.logError
* Update setEnabled to refresh all event listeners
---
ext/bg/js/search-query-parser.js | 25 ++---------------
ext/bg/js/search.js | 2 +-
ext/bg/js/settings/popup-preview-frame.js | 2 +-
ext/fg/js/float.js | 2 +-
ext/fg/js/frontend.js | 26 ++++++++---------
ext/mixed/js/display.js | 4 +--
ext/mixed/js/text-scanner.js | 46 +++++++++++++++----------------
7 files changed, 42 insertions(+), 65 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 0001c9ff..3215f8e4 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -44,12 +44,7 @@ class QueryParser extends TextScanner {
await this.queryParserGenerator.prepare();
}
- onError(error) {
- yomichan.logError(error);
- }
-
- onClick(e) {
- super.onClick(e);
+ onClick2(e) {
this.searchAt(e.clientX, e.clientY, 'click');
}
@@ -84,22 +79,8 @@ class QueryParser extends TextScanner {
getMouseEventListeners() {
return [
- [this.node, 'click', this.onClick.bind(this)],
- [this.node, 'mousedown', this.onMouseDown.bind(this)],
- [this.node, 'mousemove', this.onMouseMove.bind(this)],
- [this.node, 'mouseover', this.onMouseOver.bind(this)],
- [this.node, 'mouseout', this.onMouseOut.bind(this)]
- ];
- }
-
- getTouchEventListeners() {
- return [
- [this.node, 'auxclick', this.onAuxClick.bind(this)],
- [this.node, 'touchstart', this.onTouchStart.bind(this)],
- [this.node, 'touchend', this.onTouchEnd.bind(this)],
- [this.node, 'touchcancel', this.onTouchCancel.bind(this)],
- [this.node, 'touchmove', this.onTouchMove.bind(this), {passive: false}],
- [this.node, 'contextmenu', this.onContextMenu.bind(this)]
+ ...super.getMouseEventListeners(),
+ [this.node, 'click', this.onClick2.bind(this)]
];
}
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index cbd7b562..b7d2eed8 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -125,7 +125,7 @@ class DisplaySearch extends Display {
yomichan.logError(error);
}
- onSearchClear() {
+ onEscape() {
if (this.query === null) {
return;
}
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index 05a2a41b..e73c04a0 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -69,7 +69,7 @@ class SettingsPopupPreview {
this.frontend.getOptionsContext = async () => this.optionsContext;
this.frontend.setEnabled = () => {};
- this.frontend.onSearchClear = () => {};
+ this.frontend.clearSelection = () => {};
await this.frontend.prepare();
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index fd3b92cc..294093cd 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -92,7 +92,7 @@ class DisplayFloat extends Display {
this._orphaned = true;
}
- onSearchClear() {
+ onEscape() {
window.parent.postMessage('popupClose', '*');
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 46921d36..50f52724 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -49,7 +49,7 @@ class Frontend extends TextScanner {
this._lastShowPromise = Promise.resolve();
this._windowMessageHandlers = new Map([
- ['popupClose', () => this.onSearchClear(true)],
+ ['popupClose', () => this.clearSelection(false)],
['selectionCopy', () => document.execCommand('copy')]
]);
@@ -79,10 +79,12 @@ class Frontend extends TextScanner {
yomichan.on('zoomChanged', this.onZoomChanged.bind(this));
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+ this.on('clearSelection', this.onClearSelection.bind(this));
+
this._updateContentScale();
this._broadcastRootPopupInformation();
} catch (e) {
- this.onError(e);
+ yomichan.logError(e);
}
}
@@ -140,7 +142,7 @@ class Frontend extends TextScanner {
}
async setPopup(popup) {
- this.onSearchClear(false);
+ this.clearSelection(true);
this.popup = popup;
await popup.setOptionsContext(await this.getOptionsContext(), this._id);
}
@@ -186,11 +188,11 @@ class Frontend extends TextScanner {
this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned');
}
} else {
- this.onError(e);
+ yomichan.logError(e);
}
} finally {
if (results === null && this.options.scanning.autoHideResults) {
- this.onSearchClear(true);
+ this.clearSelection(false);
}
}
@@ -238,10 +240,9 @@ class Frontend extends TextScanner {
return {definitions, type: 'kanji'};
}
- onSearchClear(changeFocus) {
- this.popup.hide(changeFocus);
+ onClearSelection({passive}) {
+ this.popup.hide(!passive);
this.popup.clearAutoPlayTimer();
- super.onSearchClear(changeFocus);
}
async getOptionsContext() {
@@ -269,7 +270,9 @@ class Frontend extends TextScanner {
contentScale /= this._pageZoomFactor;
}
if (popupScaleRelativeToVisualViewport) {
- contentScale /= Frontend._getVisualViewportScale();
+ const visualViewport = window.visualViewport;
+ const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0);
+ contentScale /= visualViewportScale;
}
if (contentScale === this._contentScale) { return; }
@@ -302,9 +305,4 @@ class Frontend extends TextScanner {
this._showPopupContent(textSource, await this.getOptionsContext());
}
}
-
- static _getVisualViewportScale() {
- const visualViewport = window.visualViewport;
- return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0;
- }
}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 70b7fcd3..32081c70 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -69,7 +69,7 @@ class Display {
this._onKeyDownHandlers = new Map([
['Escape', () => {
- this.onSearchClear();
+ this.onEscape();
return true;
}],
['PageUp', (e) => {
@@ -183,7 +183,7 @@ class Display {
throw new Error('Override me');
}
- onSearchClear() {
+ onEscape() {
throw new Error('Override me');
}
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 1c32714b..c582ccd8 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -21,8 +21,9 @@
* docRangeFromPoint
*/
-class TextScanner {
+class TextScanner extends EventDispatcher {
constructor(node, ignoreElements, ignorePoints) {
+ super();
this.node = node;
this.ignoreElements = ignoreElements;
this.ignorePoints = ignorePoints;
@@ -32,6 +33,7 @@ class TextScanner {
this.scanTimerPromise = null;
this.causeCurrent = null;
this.textSourceCurrent = null;
+ this.textSourceCurrentSelected = false;
this.pendingLookup = false;
this.options = null;
@@ -92,7 +94,7 @@ class TextScanner {
if (DOM.isMouseButtonDown(e, 'primary')) {
this.scanTimerClear();
- this.onSearchClear(true);
+ this.clearSelection(false);
}
}
@@ -200,10 +202,6 @@ class TextScanner {
throw new Error('Override me');
}
- onError(error) {
- yomichan.logError(error);
- }
-
async scanTimerWait() {
const delay = this.options.scanning.delay;
const promise = promiseTimeout(delay, true);
@@ -225,17 +223,12 @@ class TextScanner {
}
setEnabled(enabled, canEnable) {
- if (enabled && canEnable) {
- if (!this.enabled) {
- this.hookEvents();
- this.enabled = true;
- }
+ this.eventListeners.removeAllEventListeners();
+ this.enabled = enabled && canEnable;
+ if (this.enabled) {
+ this.hookEvents();
} else {
- if (this.enabled) {
- this.eventListeners.removeAllEventListeners();
- this.enabled = false;
- }
- this.onSearchClear(false);
+ this.clearSelection(true);
}
}
@@ -300,10 +293,7 @@ class TextScanner {
const result = await this.onSearchSource(textSource, cause);
if (result !== null) {
this.causeCurrent = cause;
- this.textSourceCurrent = textSource;
- if (this.options.scanning.selectText) {
- textSource.select();
- }
+ this.setCurrentTextSource(textSource);
}
this.pendingLookup = false;
} finally {
@@ -312,7 +302,7 @@ class TextScanner {
}
}
} catch (e) {
- this.onError(e);
+ yomichan.logError(e);
}
}
@@ -333,13 +323,15 @@ class TextScanner {
}
}
- onSearchClear(_) {
+ clearSelection(passive) {
if (this.textSourceCurrent !== null) {
- if (this.options.scanning.selectText) {
+ if (this.textSourceCurrentSelected) {
this.textSourceCurrent.deselect();
}
this.textSourceCurrent = null;
+ this.textSourceCurrentSelected = false;
}
+ this.trigger('clearSelection', {passive});
}
getCurrentTextSource() {
@@ -347,7 +339,13 @@ class TextScanner {
}
setCurrentTextSource(textSource) {
- return this.textSourceCurrent = textSource;
+ this.textSourceCurrent = textSource;
+ if (this.options.scanning.selectText) {
+ this.textSourceCurrent.select();
+ this.textSourceCurrentSelected = true;
+ } else {
+ this.textSourceCurrentSelected = false;
+ }
}
static isScanningModifierPressed(scanningModifier, mouseEvent) {
--
cgit v1.2.3
From 08ada6844af424e8ff28e592fc6b9dbc1a9a97eb Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 12:47:15 -0400
Subject: Remove Frontend inheritance (#486)
* Make Frontend use composition instead of inheritance for TextScanner
* Use push instead of concat
* Update setOptions and setEnabled APIs
* Update how onWindowMessage event listener is added/removed
* Rename options to _options
* Use bind instead of arrow function
* Fix selection being cleared due to settings changes
---
ext/bg/js/search-query-parser.js | 1 +
ext/bg/js/settings/popup-preview-frame.js | 9 +--
ext/fg/js/frontend.js | 94 +++++++++++++++++++------------
ext/mixed/js/text-scanner.js | 22 ++++++--
4 files changed, 77 insertions(+), 49 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 3215f8e4..137234e8 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -86,6 +86,7 @@ class QueryParser extends TextScanner {
setOptions(options) {
super.setOptions(options);
+ super.setEnabled(true);
this.queryParser.dataset.termSpacing = `${options.parsing.termSpacing}`;
}
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index e73c04a0..cb548ed7 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -66,12 +66,10 @@ class SettingsPopupPreview {
this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this);
this.frontend = new Frontend(this.popup);
-
this.frontend.getOptionsContext = async () => this.optionsContext;
- this.frontend.setEnabled = () => {};
- this.frontend.clearSelection = () => {};
-
await this.frontend.prepare();
+ this.frontend.setDisabledOverride(true);
+ this.frontend.canClearSelection = false;
// Update search
this.updateSearch();
@@ -169,8 +167,7 @@ class SettingsPopupPreview {
const source = new TextSourceRange(range, range.toString(), null, null);
try {
- await this.frontend.onSearchSource(source, 'script');
- this.frontend.setCurrentTextSource(source);
+ await this.frontend.setTextSource(source);
} finally {
source.cleanup();
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 50f52724..76ad27e0 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -25,14 +25,8 @@
* docSentenceExtract
*/
-class Frontend extends TextScanner {
+class Frontend {
constructor(popup, getUrl=null) {
- super(
- window,
- () => this.popup.isProxy() ? [] : [this.popup.getContainer()],
- [(x, y) => this.popup.containsPoint(x, y)]
- );
-
this._id = yomichan.generateId(16);
this.popup = popup;
@@ -41,15 +35,23 @@ class Frontend extends TextScanner {
this._disabledOverride = false;
- this.options = null;
+ this._options = null;
this._pageZoomFactor = 1.0;
this._contentScale = 1.0;
this._orphaned = false;
this._lastShowPromise = Promise.resolve();
+ this._enabledEventListeners = new EventListenerCollection();
+ this._textScanner = new TextScanner(
+ window,
+ () => this.popup.isProxy() ? [] : [this.popup.getContainer()],
+ [(x, y) => this.popup.containsPoint(x, y)]
+ );
+ this._textScanner.onSearchSource = this.onSearchSource.bind(this);
+
this._windowMessageHandlers = new Map([
- ['popupClose', () => this.clearSelection(false)],
+ ['popupClose', () => this._textScanner.clearSelection(false)],
['selectionCopy', () => document.execCommand('copy')]
]);
@@ -60,6 +62,14 @@ class Frontend extends TextScanner {
]);
}
+ get canClearSelection() {
+ return this._textScanner.canClearSelection;
+ }
+
+ set canClearSelection(value) {
+ this._textScanner.canClearSelection = value;
+ }
+
async prepare() {
try {
await this.updateOptions();
@@ -79,7 +89,7 @@ class Frontend extends TextScanner {
yomichan.on('zoomChanged', this.onZoomChanged.bind(this));
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
- this.on('clearSelection', this.onClearSelection.bind(this));
+ this._textScanner.on('clearSelection', this.onClearSelection.bind(this));
this._updateContentScale();
this._broadcastRootPopupInformation();
@@ -129,44 +139,45 @@ class Frontend extends TextScanner {
this._updateContentScale();
}
- getMouseEventListeners() {
- return [
- ...super.getMouseEventListeners(),
- [window, 'message', this.onWindowMessage.bind(this)]
- ];
- }
-
setDisabledOverride(disabled) {
this._disabledOverride = disabled;
- this.setEnabled(this.options.general.enable, this._canEnable());
+ this._updateTextScannerEnabled();
}
async setPopup(popup) {
- this.clearSelection(true);
+ this._textScanner.clearSelection(true);
this.popup = popup;
await popup.setOptionsContext(await this.getOptionsContext(), this._id);
}
async updateOptions() {
const optionsContext = await this.getOptionsContext();
- this.options = await apiOptionsGet(optionsContext);
- this.setOptions(this.options, this._canEnable());
+ this._options = await apiOptionsGet(optionsContext);
+ this._textScanner.setOptions(this._options);
+ this._updateTextScannerEnabled();
const ignoreNodes = ['.scan-disable', '.scan-disable *'];
- if (!this.options.scanning.enableOnPopupExpressions) {
+ if (!this._options.scanning.enableOnPopupExpressions) {
ignoreNodes.push('.source-text', '.source-text *');
}
- this.ignoreNodes = ignoreNodes.join(',');
+ this._textScanner.ignoreNodes = ignoreNodes.join(',');
await this.popup.setOptionsContext(optionsContext, this._id);
this._updateContentScale();
- if (this.textSourceCurrent !== null && this.causeCurrent !== null) {
- await this.onSearchSource(this.textSourceCurrent, this.causeCurrent);
+ const textSourceCurrent = this._textScanner.getCurrentTextSource();
+ const causeCurrent = this._textScanner.causeCurrent;
+ if (textSourceCurrent !== null && causeCurrent !== null) {
+ await this.onSearchSource(textSourceCurrent, causeCurrent);
}
}
+ async setTextSource(textSource) {
+ await this.onSearchSource(textSource, 'script');
+ this._textScanner.setCurrentTextSource(textSource);
+ }
+
async onSearchSource(textSource, cause) {
let results = null;
@@ -184,15 +195,15 @@ class Frontend extends TextScanner {
}
} catch (e) {
if (this._orphaned) {
- if (textSource !== null && this.options.scanning.modifier !== 'none') {
+ if (textSource !== null && this._options.scanning.modifier !== 'none') {
this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned');
}
} else {
yomichan.logError(e);
}
} finally {
- if (results === null && this.options.scanning.autoHideResults) {
- this.clearSelection(false);
+ if (results === null && this._options.scanning.autoHideResults) {
+ this._textScanner.clearSelection(false);
}
}
@@ -201,7 +212,7 @@ class Frontend extends TextScanner {
showContent(textSource, focus, definitions, type, optionsContext) {
const {url} = optionsContext;
- const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt);
+ const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt);
this._showPopupContent(
textSource,
optionsContext,
@@ -215,7 +226,7 @@ class Frontend extends TextScanner {
}
async findTerms(textSource, optionsContext) {
- this.setTextSourceScanLength(textSource, this.options.scanning.length);
+ this._textScanner.setTextSourceScanLength(textSource, this._options.scanning.length);
const searchText = textSource.text();
if (searchText.length === 0) { return null; }
@@ -229,7 +240,7 @@ class Frontend extends TextScanner {
}
async findKanji(textSource, optionsContext) {
- this.setTextSourceScanLength(textSource, 1);
+ this._textScanner.setTextSourceScanLength(textSource, 1);
const searchText = textSource.text();
if (searchText.length === 0) { return null; }
@@ -263,8 +274,21 @@ class Frontend extends TextScanner {
return this._lastShowPromise;
}
+ _updateTextScannerEnabled() {
+ const enabled = (
+ this._options.general.enable &&
+ this.popup.depth <= this._options.scanning.popupNestingMaxDepth &&
+ !this._disabledOverride
+ );
+ this._enabledEventListeners.removeAllEventListeners();
+ this._textScanner.setEnabled(enabled);
+ if (enabled) {
+ this._enabledEventListeners.addEventListener(window, 'message', this.onWindowMessage.bind(this));
+ }
+ }
+
_updateContentScale() {
- const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this.options.general;
+ const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general;
let contentScale = popupScalingFactor;
if (popupScaleRelativeToPageZoom) {
contentScale /= this._pageZoomFactor;
@@ -295,12 +319,8 @@ class Frontend extends TextScanner {
});
}
- _canEnable() {
- return this.popup.depth <= this.options.scanning.popupNestingMaxDepth && !this._disabledOverride;
- }
-
async _updatePopupPosition() {
- const textSource = this.getCurrentTextSource();
+ const textSource = this._textScanner.getCurrentTextSource();
if (textSource !== null && await this.popup.isVisible()) {
this._showPopupContent(textSource, await this.getOptionsContext());
}
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index c582ccd8..774eef44 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -45,6 +45,16 @@ class TextScanner extends EventDispatcher {
this.preventNextMouseDown = false;
this.preventNextClick = false;
this.preventScroll = false;
+
+ this._canClearSelection = true;
+ }
+
+ get canClearSelection() {
+ return this._canClearSelection;
+ }
+
+ set canClearSelection(value) {
+ this._canClearSelection = value;
}
onMouseOver(e) {
@@ -222,9 +232,9 @@ class TextScanner extends EventDispatcher {
}
}
- setEnabled(enabled, canEnable) {
+ setEnabled(enabled) {
this.eventListeners.removeAllEventListeners();
- this.enabled = enabled && canEnable;
+ this.enabled = enabled;
if (this.enabled) {
this.hookEvents();
} else {
@@ -233,9 +243,9 @@ class TextScanner extends EventDispatcher {
}
hookEvents() {
- let eventListenerInfos = this.getMouseEventListeners();
+ const eventListenerInfos = this.getMouseEventListeners();
if (this.options.scanning.touchInputEnabled) {
- eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners());
+ eventListenerInfos.push(...this.getTouchEventListeners());
}
for (const [node, type, listener, options] of eventListenerInfos) {
@@ -264,9 +274,8 @@ class TextScanner extends EventDispatcher {
];
}
- setOptions(options, canEnable=true) {
+ setOptions(options) {
this.options = options;
- this.setEnabled(this.options.general.enable, canEnable);
}
async searchAt(x, y, cause) {
@@ -324,6 +333,7 @@ class TextScanner extends EventDispatcher {
}
clearSelection(passive) {
+ if (!this._canClearSelection) { return; }
if (this.textSourceCurrent !== null) {
if (this.textSourceCurrentSelected) {
this.textSourceCurrent.deselect();
--
cgit v1.2.3
From cae6b657ab418a1cafedcb1cf72d0e793fa5178b Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 12:50:16 -0400
Subject: Anki audio download (#477)
* Update how audio is added to Anki cards
* Upgrade Anki templates
* Update comments
---
.../data/default-anki-field-templates.handlebars | 4 +-
ext/bg/js/anki-note-builder.js | 47 +++++++------
ext/bg/js/backend.js | 2 +-
ext/bg/js/options.js | 19 ++++++
ext/mixed/js/audio-system.js | 78 +++++++++++++++++++---
5 files changed, 113 insertions(+), 37 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/data/default-anki-field-templates.handlebars b/ext/bg/data/default-anki-field-templates.handlebars
index 6061851f..77818a43 100644
--- a/ext/bg/data/default-anki-field-templates.handlebars
+++ b/ext/bg/data/default-anki-field-templates.handlebars
@@ -14,7 +14,9 @@
{{~/if~}}
{{/inline}}
-{{#*inline "audio"}}{{/inline}}
+{{#*inline "audio"~}}
+ [sound:{{definition.audioFileName}}]
+{{~/inline}}
{{#*inline "character"}}
{{~definition.character~}}
diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js
index dc1e9427..1f9c6ed2 100644
--- a/ext/bg/js/anki-note-builder.js
+++ b/ext/bg/js/anki-note-builder.js
@@ -42,25 +42,6 @@ class AnkiNoteBuilder {
note.fields[fieldName] = await this.formatField(fieldValue, definition, mode, context, options, templates, null);
}
- if (!isKanji && definition.audio) {
- const audioFields = [];
-
- for (const [fieldName, fieldValue] of modeOptionsFieldEntries) {
- if (fieldValue.includes('{audio}')) {
- audioFields.push(fieldName);
- }
- }
-
- if (audioFields.length > 0) {
- note.audio = {
- url: definition.audio.url,
- filename: definition.audio.filename,
- skipHash: '7e2c2f954ef6051373ba916f000168dc', // hash of audio data that should be skipped
- fields: audioFields
- };
- }
- }
-
return note;
}
@@ -88,18 +69,31 @@ class AnkiNoteBuilder {
});
}
- async injectAudio(definition, fields, sources, details) {
+ async injectAudio(definition, fields, sources, customSourceUrl) {
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, details);
const filename = this._createInjectedAudioFileName(audioSourceDefinition);
- if (filename !== null) {
- definition.audio = {url: uri, filename};
- }
+ if (filename === null) { return; }
+
+ const {audio} = await this._audioSystem.getDefinitionAudio(
+ audioSourceDefinition,
+ sources,
+ {
+ textToSpeechVoice: null,
+ customSourceUrl,
+ binary: true,
+ disableCache: true
+ }
+ );
+
+ const data = AnkiNoteBuilder.arrayBufferToBase64(audio);
+ await this._anki.storeMediaFile(filename, data);
+
+ definition.audioFileName = filename;
} catch (e) {
// NOP
}
@@ -129,6 +123,7 @@ class AnkiNoteBuilder {
if (reading) { filename += `_${reading}`; }
if (expression) { filename += `_${expression}`; }
filename += '.mp3';
+ filename = filename.replace(/\]/g, '');
return filename;
}
@@ -152,6 +147,10 @@ class AnkiNoteBuilder {
return false;
}
+ static arrayBufferToBase64(arrayBuffer) {
+ return window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
+ }
+
static stringReplaceAsync(str, regex, replacer) {
let match;
let index = 0;
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index dd1fd8e9..8a8f00eb 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -507,7 +507,7 @@ class Backend {
definition,
options.anki.terms.fields,
options.audio.sources,
- {textToSpeechVoice: null, customSourceUrl}
+ customSourceUrl
);
}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index 8e1814ed..47101b49 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -108,6 +108,25 @@ const profileOptionsVersionUpdates = [
fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}';
options.anki.fieldTemplates = fieldTemplates;
}
+ },
+ (options) => {
+ // Version 14 changes:
+ // Changed template for Anki audio.
+ let fieldTemplates = options.anki.fieldTemplates;
+ if (typeof fieldTemplates !== 'string') { return; }
+
+ const replacement = '{{#*inline "audio"~}}\n [sound:{{definition.audioFileName}}]\n{{~/inline}}';
+ let replaced = false;
+ fieldTemplates = fieldTemplates.replace(/\{\{#\*inline "audio"\}\}\{\{\/inline\}\}/g, () => {
+ replaced = true;
+ return replacement;
+ });
+
+ if (!replaced) {
+ fieldTemplates += '\n\n' + replacement;
+ }
+
+ options.anki.fieldTemplates = fieldTemplates;
}
];
diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js
index 3273f982..108cfc72 100644
--- a/ext/mixed/js/audio-system.js
+++ b/ext/mixed/js/audio-system.js
@@ -79,7 +79,7 @@ class AudioSystem {
async getDefinitionAudio(definition, sources, details) {
const key = `${definition.expression}:${definition.reading}`;
- const hasCache = (this._cache !== null);
+ const hasCache = (this._cache !== null && !details.disableCache);
if (hasCache) {
const cacheValue = this._cache.get(key);
@@ -98,7 +98,11 @@ class AudioSystem {
if (uri === null) { continue; }
try {
- const audio = await this._createAudio(uri);
+ const audio = (
+ details.binary ?
+ await this._createAudioBinary(uri) :
+ await this._createAudio(uri)
+ );
if (hasCache) {
this._cacheCheck();
this._cache.set(key, {audio, uri, source});
@@ -124,6 +128,14 @@ class AudioSystem {
// NOP
}
+ _getAudioUri(definition, source, details) {
+ return (
+ this._audioUriBuilder !== null ?
+ this._audioUriBuilder.getUri(definition, source, details) :
+ null
+ );
+ }
+
async _createAudio(uri) {
const ttsParameters = this._getTextToSpeechParameters(uri);
if (ttsParameters !== null) {
@@ -134,21 +146,20 @@ class AudioSystem {
return await this._createAudioFromUrl(uri);
}
- _getAudioUri(definition, source, details) {
- return (
- this._audioUriBuilder !== null ?
- this._audioUriBuilder.getUri(definition, source, details) :
- null
- );
+ async _createAudioBinary(uri) {
+ const ttsParameters = this._getTextToSpeechParameters(uri);
+ if (ttsParameters !== null) {
+ throw new Error('Cannot create audio from text-to-speech');
+ }
+
+ return await this._createAudioBinaryFromUrl(uri);
}
_createAudioFromUrl(url) {
return new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.addEventListener('loadeddata', () => {
- const duration = audio.duration;
- if (duration === 5.694694 || duration === 5.720718) {
- // Hardcoded values for invalid audio
+ if (!this._isAudioValid(audio)) {
reject(new Error('Could not retrieve audio'));
} else {
resolve(audio);
@@ -158,6 +169,42 @@ class AudioSystem {
});
}
+ _createAudioBinaryFromUrl(url) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.responseType = 'arraybuffer';
+ xhr.addEventListener('load', () => {
+ const arrayBuffer = xhr.response;
+ if (!this._isAudioBinaryValid(arrayBuffer)) {
+ reject(new Error('Could not retrieve audio'));
+ } else {
+ resolve(arrayBuffer);
+ }
+ });
+ xhr.addEventListener('error', () => reject(new Error('Failed to connect')));
+ xhr.open('GET', url);
+ xhr.send();
+ });
+ }
+
+ _isAudioValid(audio) {
+ const duration = audio.duration;
+ return (
+ duration !== 5.694694 && // jpod101 invalid audio (Chrome)
+ duration !== 5.720718 // jpod101 invalid audio (Firefox)
+ );
+ }
+
+ _isAudioBinaryValid(arrayBuffer) {
+ const digest = TextToSpeechAudio.arrayBufferDigest(arrayBuffer);
+ switch (digest) {
+ case 'ae6398b5a27bc8c0a771df6c907ade794be15518174773c58c7c7ddd17098906': // jpod101 invalid audio
+ return false;
+ default:
+ return true;
+ }
+ }
+
_getTextToSpeechVoiceFromVoiceUri(voiceUri) {
try {
for (const voice of speechSynthesis.getVoices()) {
@@ -195,4 +242,13 @@ class AudioSystem {
this._cache.delete(key);
}
}
+
+ static async arrayBufferDigest(arrayBuffer) {
+ const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer)));
+ let digest = '';
+ for (const byte of hash) {
+ digest += byte.toString(16).padStart(2, '0');
+ }
+ return digest;
+ }
}
--
cgit v1.2.3
From 5a61c311adec47d45fd51b48d340a70127d70f8a Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 12:57:13 -0400
Subject: Api invoke with progress (#483)
* Create an internal API function to open a port
* Create system for running actions over a special port
* Don't assign in expression
---
ext/bg/js/backend.js | 75 +++++++++++++++++++++++++++++++-
ext/mixed/js/api.js | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 192 insertions(+), 1 deletion(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 2fce4be9..ed01c8df 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -116,8 +116,10 @@ class Backend {
['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}],
['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}],
['log', {handler: this._onApiLog.bind(this), async: false}],
- ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}]
+ ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}],
+ ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}]
]);
+ this._messageHandlersWithProgress = new Map();
this._commandHandlers = new Map([
['search', this._onCommandSearch.bind(this)],
@@ -787,8 +789,79 @@ class Backend {
this._updateBadge();
}
+ _onApiCreateActionPort(params, sender) {
+ if (!sender || !sender.tab) { throw new Error('Invalid sender'); }
+ const tabId = sender.tab.id;
+ if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); }
+
+ const frameId = sender.frameId;
+ const id = yomichan.generateId(16);
+ const portName = `action-port-${id}`;
+
+ const port = chrome.tabs.connect(tabId, {name: portName, frameId});
+ try {
+ this._createActionListenerPort(port, sender, this._messageHandlersWithProgress);
+ } catch (e) {
+ port.disconnect();
+ throw e;
+ }
+
+ return portName;
+ }
+
// Command handlers
+ _createActionListenerPort(port, sender, handlers) {
+ let hasStarted = false;
+
+ const onProgress = (data) => {
+ try {
+ if (port === null) { return; }
+ port.postMessage({type: 'progress', data});
+ } catch (e) {
+ // NOP
+ }
+ };
+
+ const onMessage = async ({action, params}) => {
+ if (hasStarted) { return; }
+ hasStarted = true;
+ port.onMessage.removeListener(onMessage);
+
+ try {
+ port.postMessage({type: 'ack'});
+
+ const messageHandler = handlers.get(action);
+ if (typeof messageHandler === 'undefined') {
+ throw new Error('Invalid action');
+ }
+ const {handler, async} = messageHandler;
+
+ const promiseOrResult = handler(params, sender, onProgress);
+ const result = async ? await promiseOrResult : promiseOrResult;
+ port.postMessage({type: 'complete', data: result});
+ } catch (e) {
+ if (port !== null) {
+ port.postMessage({type: 'error', data: e});
+ }
+ cleanup();
+ }
+ };
+
+ const cleanup = () => {
+ if (port === null) { return; }
+ if (!hasStarted) {
+ port.onMessage.removeListener(onMessage);
+ }
+ port.onDisconnect.removeListener(cleanup);
+ port = null;
+ handlers = null;
+ };
+
+ port.onMessage.addListener(onMessage);
+ port.onDisconnect.addListener(cleanup);
+ }
+
_getErrorLevelValue(errorLevel) {
switch (errorLevel) {
case 'info': return 0;
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index afd68aa2..bf85338e 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -152,6 +152,124 @@ function apiLogIndicatorClear() {
return _apiInvoke('logIndicatorClear');
}
+function _apiCreateActionPort(timeout=5000) {
+ return new Promise((resolve, reject) => {
+ let timer = null;
+ let portNameResolve;
+ let portNameReject;
+ const portNamePromise = new Promise((resolve2, reject2) => {
+ portNameResolve = resolve2;
+ portNameReject = reject2;
+ });
+
+ const onConnect = async (port) => {
+ try {
+ const portName = await portNamePromise;
+ if (port.name !== portName || timer === null) { return; }
+ } catch (e) {
+ return;
+ }
+
+ clearTimeout(timer);
+ timer = null;
+
+ chrome.runtime.onConnect.removeListener(onConnect);
+ resolve(port);
+ };
+
+ const onError = (e) => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ chrome.runtime.onConnect.removeListener(onConnect);
+ portNameReject(e);
+ reject(e);
+ };
+
+ timer = setTimeout(() => onError(new Error('Timeout')), timeout);
+
+ chrome.runtime.onConnect.addListener(onConnect);
+ _apiInvoke('createActionPort').then(portNameResolve, onError);
+ });
+}
+
+function _apiInvokeWithProgress(action, params, onProgress, timeout=5000) {
+ return new Promise((resolve, reject) => {
+ let timer = null;
+ let port = null;
+
+ if (typeof onProgress !== 'function') {
+ onProgress = () => {};
+ }
+
+ const onMessage = (message) => {
+ switch (message.type) {
+ case 'ack':
+ if (timer !== null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ break;
+ case 'progress':
+ try {
+ onProgress(message.data);
+ } catch (e) {
+ // NOP
+ }
+ break;
+ case 'complete':
+ cleanup();
+ resolve(message.data);
+ break;
+ case 'error':
+ cleanup();
+ reject(jsonToError(message.data));
+ break;
+ }
+ };
+
+ const onDisconnect = () => {
+ cleanup();
+ reject(new Error('Disconnected'));
+ };
+
+ const cleanup = () => {
+ if (timer !== null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ if (port !== null) {
+ port.onMessage.removeListener(onMessage);
+ port.onDisconnect.removeListener(onDisconnect);
+ port.disconnect();
+ port = null;
+ }
+ onProgress = null;
+ };
+
+ timer = setTimeout(() => {
+ cleanup();
+ reject(new Error('Timeout'));
+ }, timeout);
+
+ (async () => {
+ try {
+ port = await _apiCreateActionPort(timeout);
+ port.onMessage.addListener(onMessage);
+ port.onDisconnect.addListener(onDisconnect);
+ port.postMessage({action, params});
+ } catch (e) {
+ cleanup();
+ reject(e);
+ } finally {
+ action = null;
+ params = null;
+ }
+ })();
+ });
+}
+
function _apiInvoke(action, params={}) {
const data = {action, params};
return new Promise((resolve, reject) => {
--
cgit v1.2.3
From 401fe9f8d027fe71f25d2f591f316781e1e0a436 Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 12:57:42 -0400
Subject: Object property accessor API update (#485)
* Simplify function names
* Add delete and swap functions
* Remove custom setter
Not currently part of the expected use cases.
* Add documentation
* Update tests
* Add delete test functions
* Update tests to use fresh objects
* Add swap test functions
* Add empty tests
* Disable delete on arrays
---
ext/mixed/js/object-property-accessor.js | 125 ++++++++++++++++++---
test/test-object-property-accessor.js | 186 ++++++++++++++++++++++++++-----
2 files changed, 268 insertions(+), 43 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/mixed/js/object-property-accessor.js b/ext/mixed/js/object-property-accessor.js
index 349037b3..07b8df61 100644
--- a/ext/mixed/js/object-property-accessor.js
+++ b/ext/mixed/js/object-property-accessor.js
@@ -16,15 +16,27 @@
*/
/**
- * Class used to get and set generic properties of an object by using path strings.
+ * Class used to get and mutate generic properties of an object by using path strings.
*/
class ObjectPropertyAccessor {
- constructor(target, setter=null) {
+ /**
+ * Create a new accessor for a specific object.
+ * @param target The object which the getter and mutation methods are applied to.
+ * @returns A new ObjectPropertyAccessor instance.
+ */
+ constructor(target) {
this._target = target;
- this._setter = (typeof setter === 'function' ? setter : null);
}
- getProperty(pathArray, pathLength) {
+ /**
+ * Gets the value at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @param pathLength How many parts of the pathArray to use.
+ * This parameter is optional and defaults to the length of pathArray.
+ * @returns The value found at the path.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ get(pathArray, pathLength) {
let target = this._target;
const ii = typeof pathLength === 'number' ? Math.min(pathArray.length, pathLength) : pathArray.length;
for (let i = 0; i < ii; ++i) {
@@ -37,24 +49,89 @@ class ObjectPropertyAccessor {
return target;
}
- setProperty(pathArray, value) {
- if (pathArray.length === 0) {
- throw new Error('Invalid path');
+ /**
+ * Sets the value at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @param value The value to assign to the property.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ set(pathArray, value) {
+ const ii = pathArray.length - 1;
+ if (ii < 0) { throw new Error('Invalid path'); }
+
+ const target = this.get(pathArray, ii);
+ const key = pathArray[ii];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
+ throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
}
- const target = this.getProperty(pathArray, pathArray.length - 1);
- const key = pathArray[pathArray.length - 1];
+ target[key] = value;
+ }
+
+ /**
+ * Deletes the property of the target object at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ delete(pathArray) {
+ const ii = pathArray.length - 1;
+ if (ii < 0) { throw new Error('Invalid path'); }
+
+ const target = this.get(pathArray, ii);
+ const key = pathArray[ii];
if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
}
- if (this._setter !== null) {
- this._setter(target, key, value, pathArray);
- } else {
- target[key] = value;
+ if (Array.isArray(target)) {
+ throw new Error('Invalid type');
+ }
+
+ delete target[key];
+ }
+
+ /**
+ * Swaps two properties of an object or array.
+ * @param pathArray1 The path to the first property on the target object.
+ * @param pathArray2 The path to the second property on the target object.
+ * @throws An error is thrown if pathArray1 or pathArray2 is not valid for the target object,
+ * or if the swap cannot be performed.
+ */
+ swap(pathArray1, pathArray2) {
+ const ii1 = pathArray1.length - 1;
+ if (ii1 < 0) { throw new Error('Invalid path 1'); }
+ const target1 = this.get(pathArray1, ii1);
+ const key1 = pathArray1[ii1];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target1, key1)) { throw new Error(`Invalid path 1: ${ObjectPropertyAccessor.getPathString(pathArray1)}`); }
+
+ const ii2 = pathArray2.length - 1;
+ if (ii2 < 0) { throw new Error('Invalid path 2'); }
+ const target2 = this.get(pathArray2, ii2);
+ const key2 = pathArray2[ii2];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target2, key2)) { throw new Error(`Invalid path 2: ${ObjectPropertyAccessor.getPathString(pathArray2)}`); }
+
+ const value1 = target1[key1];
+ const value2 = target2[key2];
+
+ target1[key1] = value2;
+ try {
+ target2[key2] = value1;
+ } catch (e) {
+ // Revert
+ try {
+ target1[key1] = value1;
+ } catch (e2) {
+ // NOP
+ }
+ throw e;
}
}
+ /**
+ * Converts a path string to a path array.
+ * @param pathArray The path array to convert.
+ * @returns A string representation of pathArray.
+ */
static getPathString(pathArray) {
const regexShort = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
let pathString = '';
@@ -86,6 +163,12 @@ class ObjectPropertyAccessor {
return pathString;
}
+ /**
+ * Converts a path array to a path string. For the most part, the format of this string
+ * matches Javascript's notation for property access.
+ * @param pathString The path string to convert.
+ * @returns An array representation of pathString.
+ */
static getPathArray(pathString) {
const pathArray = [];
let state = 'empty';
@@ -201,6 +284,14 @@ class ObjectPropertyAccessor {
return pathArray;
}
+ /**
+ * Checks whether an object or array has the specified property.
+ * @param object The object to test.
+ * @param property The property to check for existence.
+ * This value should be a string if the object is a non-array object.
+ * For arrays, it should be an integer.
+ * @returns true if the property exists, otherwise false.
+ */
static hasProperty(object, property) {
switch (typeof property) {
case 'string':
@@ -222,6 +313,14 @@ class ObjectPropertyAccessor {
}
}
+ /**
+ * Checks whether a property is valid for the given object
+ * @param object The object to test.
+ * @param property The property to check for existence.
+ * @returns true if the property is correct for the given object type, otherwise false.
+ * For arrays, this means that the property should be a positive integer.
+ * For non-array objects, the property should be a string.
+ */
static isValidPropertyType(object, property) {
switch (typeof property) {
case 'string':
diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js
index 0773ba6e..1e694946 100644
--- a/test/test-object-property-accessor.js
+++ b/test/test-object-property-accessor.js
@@ -40,29 +40,30 @@ function createTestObject() {
}
-function testGetProperty1() {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
+function testGet1() {
const data = [
- [[], object],
- [['0'], object['0']],
- [['value1'], object.value1],
- [['value1', 'value2'], object.value1.value2],
- [['value1', 'value3'], object.value1.value3],
- [['value1', 'value4'], object.value1.value4],
- [['value5'], object.value5],
- [['value5', 0], object.value5[0]],
- [['value5', 1], object.value5[1]],
- [['value5', 2], object.value5[2]]
+ [[], (object) => object],
+ [['0'], (object) => object['0']],
+ [['value1'], (object) => object.value1],
+ [['value1', 'value2'], (object) => object.value1.value2],
+ [['value1', 'value3'], (object) => object.value1.value3],
+ [['value1', 'value4'], (object) => object.value1.value4],
+ [['value5'], (object) => object.value5],
+ [['value5', 0], (object) => object.value5[0]],
+ [['value5', 1], (object) => object.value5[1]],
+ [['value5', 2], (object) => object.value5[2]]
];
- for (const [pathArray, expected] of data) {
- assert.strictEqual(accessor.getProperty(pathArray), expected);
+ for (const [pathArray, getExpected] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+ const expected = getExpected(object);
+
+ assert.strictEqual(accessor.get(pathArray), expected);
}
}
-function testGetProperty2() {
+function testGet2() {
const object = createTestObject();
const accessor = new ObjectPropertyAccessor(object);
@@ -89,15 +90,12 @@ function testGetProperty2() {
];
for (const [pathArray, message] of data) {
- assert.throws(() => accessor.getProperty(pathArray), {message});
+ assert.throws(() => accessor.get(pathArray), {message});
}
}
-function testSetProperty1() {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
+function testSet1() {
const testValue = {};
const data = [
['0'],
@@ -112,17 +110,21 @@ function testSetProperty1() {
];
for (const pathArray of data) {
- accessor.setProperty(pathArray, testValue);
- assert.strictEqual(accessor.getProperty(pathArray), testValue);
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ accessor.set(pathArray, testValue);
+ assert.strictEqual(accessor.get(pathArray), testValue);
}
}
-function testSetProperty2() {
+function testSet2() {
const object = createTestObject();
const accessor = new ObjectPropertyAccessor(object);
const testValue = {};
const data = [
+ [[], 'Invalid path'],
[[0], 'Invalid path: [0]'],
[['0', 'invalid'], 'Invalid path: ["0"].invalid'],
[['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
@@ -137,7 +139,127 @@ function testSetProperty2() {
];
for (const [pathArray, message] of data) {
- assert.throws(() => accessor.setProperty(pathArray, testValue), {message});
+ assert.throws(() => accessor.set(pathArray, testValue), {message});
+ }
+}
+
+
+function testDelete1() {
+ const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property);
+
+ const data = [
+ [['0'], (object) => !hasOwn(object, '0')],
+ [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')],
+ [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')],
+ [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')],
+ [['value1'], (object) => !hasOwn(object, 'value1')],
+ [['value5'], (object) => !hasOwn(object, 'value5')]
+ ];
+
+ for (const [pathArray, validate] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ accessor.delete(pathArray);
+ assert.ok(validate(object));
+ }
+}
+
+function testDelete2() {
+ const data = [
+ [[], 'Invalid path'],
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index'],
+ [['value5', 0], 'Invalid type'],
+ [['value5', 1], 'Invalid type'],
+ [['value5', 2], 'Invalid type']
+ ];
+
+ for (const [pathArray, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ assert.throws(() => accessor.delete(pathArray), {message});
+ }
+}
+
+
+function testSwap1() {
+ const data = [
+ [['0'], true],
+ [['value1', 'value2'], true],
+ [['value1', 'value3'], true],
+ [['value1', 'value4'], true],
+ [['value1'], false],
+ [['value5', 0], true],
+ [['value5', 1], true],
+ [['value5', 2], true],
+ [['value5'], false]
+ ];
+
+ for (const [pathArray1, compareValues1] of data) {
+ for (const [pathArray2, compareValues2] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ const value1a = accessor.get(pathArray1);
+ const value2a = accessor.get(pathArray2);
+
+ accessor.swap(pathArray1, pathArray2);
+
+ if (!compareValues1 || !compareValues2) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ assert.deepStrictEqual(value1a, value2b);
+ assert.deepStrictEqual(value2a, value1b);
+ }
+ }
+}
+
+function testSwap2() {
+ const data = [
+ [[], [], false, 'Invalid path 1'],
+ [['0'], [], false, 'Invalid path 2'],
+ [[], ['0'], false, 'Invalid path 1'],
+ [[0], ['0'], false, 'Invalid path 1: [0]'],
+ [['0'], [0], false, 'Invalid path 2: [0]']
+ ];
+
+ for (const [pathArray1, pathArray2, checkRevert, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ let value1a;
+ let value2a;
+ if (checkRevert) {
+ try {
+ value1a = accessor.get(pathArray1);
+ value2a = accessor.get(pathArray2);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ assert.throws(() => accessor.swap(pathArray1, pathArray2), {message});
+
+ if (!checkRevert) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ assert.deepStrictEqual(value1a, value1b);
+ assert.deepStrictEqual(value2a, value2b);
}
}
@@ -272,10 +394,14 @@ function testIsValidPropertyType() {
function main() {
- testGetProperty1();
- testGetProperty2();
- testSetProperty1();
- testSetProperty2();
+ testGet1();
+ testGet2();
+ testSet1();
+ testSet2();
+ testDelete1();
+ testDelete2();
+ testSwap1();
+ testSwap2();
testGetPathString1();
testGetPathString2();
testGetPathArray1();
--
cgit v1.2.3
From c4ea9321dcffbda9004461a7b0027cf5c893f3c0 Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 13:00:46 -0400
Subject: Validate document nodes before use (#493)
* Validate document.body before use in loadScripts
This also fixes an issue where reject wasn't being passed to loadScriptSentinel.
* Validate document nodes before use in _getSiteColor
* Validate document.body before use in _getViewport
* Validate document.body before use in setContentScale
* Validate document.body before use in docImposterCreate
---
ext/fg/js/document.js | 5 ++++-
ext/fg/js/float.js | 4 +++-
ext/fg/js/popup.js | 12 +++++++++---
ext/mixed/js/dynamic-loader.js | 12 ++++++++----
4 files changed, 24 insertions(+), 9 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 3b4cc28f..6103c7c5 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -28,6 +28,9 @@ function docSetImposterStyle(style, propertyName, value) {
}
function docImposterCreate(element, isTextarea) {
+ const body = document.body;
+ if (body === null) { return [null, null]; }
+
const elementStyle = window.getComputedStyle(element);
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
@@ -78,7 +81,7 @@ function docImposterCreate(element, isTextarea) {
}
container.appendChild(imposter);
- document.body.appendChild(container);
+ body.appendChild(container);
// Adjust size
const imposterRect = imposter.getBoundingClientRect();
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 294093cd..77e8edd8 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -162,7 +162,9 @@ class DisplayFloat extends Display {
}
setContentScale(scale) {
- document.body.style.fontSize = `${scale}em`;
+ const body = document.body;
+ if (body === null) { return; }
+ body.style.fontSize = `${scale}em`;
}
async getDocumentTitle() {
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 2b33b714..f5cb6f77 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -389,8 +389,13 @@ class Popup {
_getSiteColor() {
const color = [255, 255, 255];
- Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor));
- Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.body).backgroundColor));
+ const {documentElement, body} = document;
+ if (documentElement !== null) {
+ Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(documentElement).backgroundColor));
+ }
+ if (body !== null) {
+ Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(body).backgroundColor));
+ }
const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128);
return dark ? 'dark' : 'light';
}
@@ -575,10 +580,11 @@ class Popup {
}
}
+ const body = document.body;
return {
left: 0,
top: 0,
- right: document.body.clientWidth,
+ right: (body !== null ? body.clientWidth : 0),
bottom: window.innerHeight
};
}
diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js
index 29672d36..51b6821b 100644
--- a/ext/mixed/js/dynamic-loader.js
+++ b/ext/mixed/js/dynamic-loader.js
@@ -31,8 +31,13 @@ const dynamicLoader = (() => {
}
function loadScripts(urls) {
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
const parent = document.body;
+ if (parent === null) {
+ reject(new Error('Missing body'));
+ return;
+ }
+
for (const url of urls) {
const node = parent.querySelector(`script[src='${escapeCSSAttribute(url)}']`);
if (node !== null) { continue; }
@@ -43,12 +48,11 @@ const dynamicLoader = (() => {
parent.appendChild(script);
}
- loadScriptSentinel(resolve);
+ loadScriptSentinel(parent, resolve, reject);
});
}
- function loadScriptSentinel(resolve, reject) {
- const parent = document.body;
+ function loadScriptSentinel(parent, resolve, reject) {
const script = document.createElement('script');
const sentinelEventName = 'dynamicLoaderSentinel';
--
cgit v1.2.3
From acfdaa4f483790cf3d70a2c1a59d82a422ebed1f Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Sat, 2 May 2020 17:24:05 -0400
Subject: Fix incorrect static call (#498)
---
ext/mixed/js/audio-system.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'ext/mixed')
diff --git a/ext/mixed/js/audio-system.js b/ext/mixed/js/audio-system.js
index 108cfc72..4b4d9765 100644
--- a/ext/mixed/js/audio-system.js
+++ b/ext/mixed/js/audio-system.js
@@ -196,7 +196,7 @@ class AudioSystem {
}
_isAudioBinaryValid(arrayBuffer) {
- const digest = TextToSpeechAudio.arrayBufferDigest(arrayBuffer);
+ const digest = AudioSystem.arrayBufferDigest(arrayBuffer);
switch (digest) {
case 'ae6398b5a27bc8c0a771df6c907ade794be15518174773c58c7c7ddd17098906': // jpod101 invalid audio
return false;
--
cgit v1.2.3
From 77b744e675f8abf17ff5e8433f4f1717e0c9ffb5 Mon Sep 17 00:00:00 2001
From: siikamiika
Date: Sun, 3 May 2020 04:39:24 +0300
Subject: Modifier key profile condition (#487)
* update Frontend options on modifier change
* add modifier key profile condition
* use select element for modifier condition value
* support "is" and "is not" modifier key conditions
* use plural
* remove dead null check
it's never null in that function
* pass element on rather than assigning to this
* rename event
* remove Firefox OS key to Meta detection
* hide Meta from dropdown on Firefox
* move input type
---
.eslintrc.json | 1 +
ext/bg/js/profile-conditions.js | 66 +++++++++++++++++
ext/bg/js/search.js | 3 +-
ext/bg/js/settings/conditions-ui.js | 139 ++++++++++++++++++++++++++++++++----
ext/bg/settings.html | 5 +-
ext/fg/js/frontend.js | 27 ++++++-
ext/mixed/js/core.js | 6 ++
ext/mixed/js/display.js | 7 +-
ext/mixed/js/dom.js | 14 ++++
ext/mixed/js/text-scanner.js | 3 +
10 files changed, 248 insertions(+), 23 deletions(-)
(limited to 'ext/mixed')
diff --git a/.eslintrc.json b/.eslintrc.json
index a2de6671..3186a491 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -97,6 +97,7 @@
"parseUrl": "readonly",
"areSetsEqual": "readonly",
"getSetIntersection": "readonly",
+ "getSetDifference": "readonly",
"EventDispatcher": "readonly",
"EventListenerCollection": "readonly",
"EXTENSION_IS_BROWSER_EDGE": "readonly"
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index a0710bd1..c0f5d3f5 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -36,6 +36,24 @@ function _profileConditionTestDomainList(url, domainList) {
return false;
}
+const _profileModifierKeys = [
+ {optionValue: 'alt', name: 'Alt'},
+ {optionValue: 'ctrl', name: 'Ctrl'},
+ {optionValue: 'shift', name: 'Shift'}
+];
+
+if (!hasOwn(window, 'netscape')) {
+ _profileModifierKeys.push({optionValue: 'meta', name: 'Meta'});
+}
+
+const _profileModifierValueToName = new Map(
+ _profileModifierKeys.map(({optionValue, name}) => [optionValue, name])
+);
+
+const _profileModifierNameToValue = new Map(
+ _profileModifierKeys.map(({optionValue, name}) => [name, optionValue])
+);
+
const profileConditionsDescriptor = {
popupLevel: {
name: 'Popup Level',
@@ -100,5 +118,53 @@ const profileConditionsDescriptor = {
test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
}
}
+ },
+ modifierKeys: {
+ name: 'Modifier Keys',
+ description: 'Use profile depending on the active modifier keys.',
+ values: _profileModifierKeys,
+ defaultOperator: 'are',
+ operators: {
+ are: {
+ name: 'are',
+ placeholder: 'Press one or more modifier keys here',
+ defaultValue: '',
+ type: 'keyMulti',
+ transform: (optionValue) => optionValue
+ .split(' + ')
+ .filter((v) => v.length > 0)
+ .map((v) => _profileModifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => _profileModifierValueToName.get(v))
+ .join(' + '),
+ test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
+ },
+ areNot: {
+ name: 'are not',
+ placeholder: 'Press one or more modifier keys here',
+ defaultValue: '',
+ type: 'keyMulti',
+ transform: (optionValue) => optionValue
+ .split(' + ')
+ .filter((v) => v.length > 0)
+ .map((v) => _profileModifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => _profileModifierValueToName.get(v))
+ .join(' + '),
+ test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
+ },
+ include: {
+ name: 'include',
+ type: 'select',
+ defaultValue: 'alt',
+ test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
+ },
+ notInclude: {
+ name: 'don\'t include',
+ type: 'select',
+ defaultValue: 'alt',
+ test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
+ }
+ }
}
};
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index b7d2eed8..47d495e6 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -17,6 +17,7 @@
/* global
* ClipboardMonitor
+ * DOM
* Display
* QueryParser
* apiClipboardGet
@@ -178,7 +179,7 @@ class DisplaySearch extends Display {
}
onKeyDown(e) {
- const key = Display.getKeyFromEvent(e);
+ const key = DOM.getKeyFromEvent(e);
const ignoreKeys = this._onKeyDownIgnoreKeys;
const activeModifierMap = new Map([
diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index 84498b42..5b356101 100644
--- a/ext/bg/js/settings/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -16,6 +16,7 @@
*/
/* global
+ * DOM
* conditionsNormalizeOptionValue
*/
@@ -177,7 +178,8 @@ ConditionsUI.Condition = class Condition {
this.parent = parent;
this.condition = condition;
this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container);
- this.input = this.container.find('input');
+ this.input = this.container.find('.condition-input');
+ this.inputInner = null;
this.typeSelect = this.container.find('.condition-type');
this.operatorSelect = this.container.find('.condition-operator');
this.removeButton = this.container.find('.condition-remove');
@@ -186,14 +188,13 @@ ConditionsUI.Condition = class Condition {
this.updateOperators();
this.updateInput();
- this.input.on('change', this.onInputChanged.bind(this));
this.typeSelect.on('change', this.onConditionTypeChanged.bind(this));
this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this));
this.removeButton.on('click', this.onRemoveClicked.bind(this));
}
cleanup() {
- this.input.off('change');
+ this.inputInner.off('change');
this.typeSelect.off('change');
this.operatorSelect.off('change');
this.removeButton.off('click');
@@ -236,21 +237,48 @@ ConditionsUI.Condition = class Condition {
updateInput() {
const conditionDescriptors = this.parent.parent.conditionDescriptors;
const {type, operator} = this.condition;
- const props = new Map([
- ['placeholder', ''],
- ['type', 'text']
- ]);
const objects = [];
+ let inputType = null;
if (hasOwn(conditionDescriptors, type)) {
const conditionDescriptor = conditionDescriptors[type];
objects.push(conditionDescriptor);
+ if (hasOwn(conditionDescriptor, 'type')) {
+ inputType = conditionDescriptor.type;
+ }
if (hasOwn(conditionDescriptor.operators, operator)) {
const operatorDescriptor = conditionDescriptor.operators[operator];
objects.push(operatorDescriptor);
+ if (hasOwn(operatorDescriptor, 'type')) {
+ inputType = operatorDescriptor.type;
+ }
}
}
+ this.input.empty();
+ if (inputType === 'select') {
+ this.inputInner = this.createSelectElement(objects);
+ } else if (inputType === 'keyMulti') {
+ this.inputInner = this.createInputKeyMultiElement(objects);
+ } else {
+ this.inputInner = this.createInputElement(objects);
+ }
+ this.inputInner.appendTo(this.input);
+ this.inputInner.on('change', this.onInputChanged.bind(this));
+
+ const {valid} = this.validateValue(this.condition.value);
+ this.inputInner.toggleClass('is-invalid', !valid);
+ this.inputInner.val(this.condition.value);
+ }
+
+ createInputElement(objects) {
+ const inputInner = ConditionsUI.instantiateTemplate('#condition-input-text-template');
+
+ const props = new Map([
+ ['placeholder', ''],
+ ['type', 'text']
+ ]);
+
for (const object of objects) {
if (hasOwn(object, 'placeholder')) {
props.set('placeholder', object.placeholder);
@@ -266,12 +294,95 @@ ConditionsUI.Condition = class Condition {
}
for (const [prop, value] of props.entries()) {
- this.input.prop(prop, value);
+ inputInner.prop(prop, value);
}
- const {valid} = this.validateValue(this.condition.value);
- this.input.toggleClass('is-invalid', !valid);
- this.input.val(this.condition.value);
+ return inputInner;
+ }
+
+ createInputKeyMultiElement(objects) {
+ const inputInner = this.createInputElement(objects);
+
+ inputInner.prop('readonly', true);
+
+ let values = [];
+ for (const object of objects) {
+ if (hasOwn(object, 'values')) {
+ values = object.values;
+ }
+ }
+
+ const pressedKeyIndices = new Set();
+
+ const onKeyDown = ({originalEvent}) => {
+ const pressedKeyEventName = DOM.getKeyFromEvent(originalEvent);
+ if (pressedKeyEventName === 'Escape' || pressedKeyEventName === 'Backspace') {
+ pressedKeyIndices.clear();
+ inputInner.val('');
+ inputInner.change();
+ return;
+ }
+
+ const pressedModifiers = DOM.getActiveModifiers(originalEvent);
+ // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey
+ // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta
+ // It works with mouse events on some platforms, so try to determine if metaKey is pressed
+ // hack; only works when Shift and Alt are not pressed
+ const isMetaKeyChrome = (
+ pressedKeyEventName === 'Meta' &&
+ getSetDifference(new Set(['shift', 'alt']), pressedModifiers).size !== 0
+ );
+ if (isMetaKeyChrome) {
+ pressedModifiers.add('meta');
+ }
+
+ for (const modifier of pressedModifiers) {
+ const foundIndex = values.findIndex(({optionValue}) => optionValue === modifier);
+ if (foundIndex !== -1) {
+ pressedKeyIndices.add(foundIndex);
+ }
+ }
+
+ const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + ');
+ inputInner.val(inputValue);
+ inputInner.change();
+ };
+
+ inputInner.on('keydown', onKeyDown);
+
+ return inputInner;
+ }
+
+ createSelectElement(objects) {
+ const inputInner = ConditionsUI.instantiateTemplate('#condition-input-select-template');
+
+ const data = new Map([
+ ['values', []],
+ ['defaultValue', null]
+ ]);
+
+ for (const object of objects) {
+ if (hasOwn(object, 'values')) {
+ data.set('values', object.values);
+ }
+ if (hasOwn(object, 'defaultValue')) {
+ data.set('defaultValue', object.defaultValue);
+ }
+ }
+
+ for (const {optionValue, name} of data.get('values')) {
+ const option = ConditionsUI.instantiateTemplate('#condition-input-option-template');
+ option.attr('value', optionValue);
+ option.text(name);
+ option.appendTo(inputInner);
+ }
+
+ const defaultValue = data.get('defaultValue');
+ if (defaultValue !== null) {
+ inputInner.val(defaultValue);
+ }
+
+ return inputInner;
}
validateValue(value) {
@@ -291,9 +402,9 @@ ConditionsUI.Condition = class Condition {
}
onInputChanged() {
- const {valid, value} = this.validateValue(this.input.val());
- this.input.toggleClass('is-invalid', !valid);
- this.input.val(value);
+ const {valid, value} = this.validateValue(this.inputInner.val());
+ this.inputInner.toggleClass('is-invalid', !valid);
+ this.inputInner.val(value);
this.condition.value = value;
this.save();
}
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index a0220e96..fc9221f8 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -117,7 +117,7 @@
-
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 76ad27e0..d979246d 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -50,6 +50,9 @@ class Frontend {
);
this._textScanner.onSearchSource = this.onSearchSource.bind(this);
+ this._activeModifiers = new Set();
+ this._optionsUpdatePending = false;
+
this._windowMessageHandlers = new Map([
['popupClose', () => this._textScanner.clearSelection(false)],
['selectionCopy', () => document.execCommand('copy')]
@@ -90,6 +93,7 @@ class Frontend {
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
this._textScanner.on('clearSelection', this.onClearSelection.bind(this));
+ this._textScanner.on('activeModifiersChanged', this.onActiveModifiersChanged.bind(this));
this._updateContentScale();
this._broadcastRootPopupInformation();
@@ -173,12 +177,21 @@ class Frontend {
}
}
+ async updatePendingOptions() {
+ if (this._optionsUpdatePending) {
+ this._optionsUpdatePending = false;
+ await this.updateOptions();
+ }
+ }
+
async setTextSource(textSource) {
await this.onSearchSource(textSource, 'script');
this._textScanner.setCurrentTextSource(textSource);
}
async onSearchSource(textSource, cause) {
+ await this.updatePendingOptions();
+
let results = null;
try {
@@ -254,12 +267,24 @@ class Frontend {
onClearSelection({passive}) {
this.popup.hide(!passive);
this.popup.clearAutoPlayTimer();
+ this.updatePendingOptions();
+ }
+
+ async onActiveModifiersChanged({modifiers}) {
+ if (areSetsEqual(modifiers, this._activeModifiers)) { return; }
+ this._activeModifiers = modifiers;
+ if (await this.popup.isVisible()) {
+ this._optionsUpdatePending = true;
+ return;
+ }
+ await this.updateOptions();
}
async getOptionsContext() {
const url = this._getUrl !== null ? await this._getUrl() : window.location.href;
const depth = this.popup.depth;
- return {depth, url};
+ const modifierKeys = [...this._activeModifiers];
+ return {depth, url, modifierKeys};
}
_showPopupContent(textSource, optionsContext, type=null, details=null) {
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index fbe9943a..835d9cea 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -146,6 +146,12 @@ function getSetIntersection(set1, set2) {
return result;
}
+function getSetDifference(set1, set2) {
+ return new Set(
+ [...set1].filter((value) => !set2.has(value))
+ );
+}
+
/*
* Async utilities
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 32081c70..783af7d8 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -338,7 +338,7 @@ class Display {
}
onKeyDown(e) {
- const key = Display.getKeyFromEvent(e);
+ const key = DOM.getKeyFromEvent(e);
const handler = this._onKeyDownHandlers.get(key);
if (typeof handler === 'function') {
if (handler(e)) {
@@ -964,11 +964,6 @@ class Display {
return elementRect.top - documentRect.top;
}
- static getKeyFromEvent(event) {
- const key = event.key;
- return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
- }
-
async _getNoteContext() {
const documentTitle = await this.getDocumentTitle();
return {
diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js
index 31ba33d6..0e8f4462 100644
--- a/ext/mixed/js/dom.js
+++ b/ext/mixed/js/dom.js
@@ -63,6 +63,20 @@ class DOM {
}
}
+ static getActiveModifiers(event) {
+ const modifiers = new Set();
+ if (event.altKey) { modifiers.add('alt'); }
+ if (event.ctrlKey) { modifiers.add('ctrl'); }
+ if (event.metaKey) { modifiers.add('meta'); }
+ if (event.shiftKey) { modifiers.add('shift'); }
+ return modifiers;
+ }
+
+ static getKeyFromEvent(event) {
+ const key = event.key;
+ return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
+ }
+
static getFullscreenElement() {
return (
document.fullscreenElement ||
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 774eef44..d74a04f8 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -70,6 +70,9 @@ class TextScanner extends EventDispatcher {
return;
}
+ const modifiers = DOM.getActiveModifiers(e);
+ this.trigger('activeModifiersChanged', {modifiers});
+
const scanningOptions = this.options.scanning;
const scanningModifier = scanningOptions.modifier;
if (!(
--
cgit v1.2.3
From 7d7ff165ced305bc269b24aa0ce8664272d86fa6 Mon Sep 17 00:00:00 2001
From: siikamiika
Date: Tue, 5 May 2020 00:13:43 +0300
Subject: Extract textSource text inside TextScanner (#500)
* extract text inside TextScanner
* clone textSource before text extraction
---
ext/bg/js/search-query-parser.js | 3 +--
ext/fg/js/frontend.js | 8 ++------
ext/mixed/js/text-scanner.js | 27 +++++++++++++++------------
3 files changed, 18 insertions(+), 20 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 137234e8..935f01f2 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -51,8 +51,7 @@ class QueryParser extends TextScanner {
async onSearchSource(textSource, cause) {
if (textSource === null) { return null; }
- this.setTextSourceScanLength(textSource, this.options.scanning.length);
- const searchText = textSource.text();
+ const searchText = this.getTextSourceContent(textSource, this.options.scanning.length);
if (searchText.length === 0) { return; }
const {definitions, length} = await apiTermsFind(searchText, {}, this.getOptionsContext());
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index d979246d..b2cb0cd8 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -239,9 +239,7 @@ class Frontend {
}
async findTerms(textSource, optionsContext) {
- this._textScanner.setTextSourceScanLength(textSource, this._options.scanning.length);
-
- const searchText = textSource.text();
+ const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length);
if (searchText.length === 0) { return null; }
const {definitions, length} = await apiTermsFind(searchText, {}, optionsContext);
@@ -253,9 +251,7 @@ class Frontend {
}
async findKanji(textSource, optionsContext) {
- this._textScanner.setTextSourceScanLength(textSource, 1);
-
- const searchText = textSource.text();
+ const searchText = this._textScanner.getTextSourceContent(textSource, 1);
if (searchText.length === 0) { return null; }
const definitions = await apiKanjiFind(searchText, optionsContext);
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index d74a04f8..9dcf6009 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -318,21 +318,24 @@ class TextScanner extends EventDispatcher {
}
}
- setTextSourceScanLength(textSource, length) {
- textSource.setEndOffset(length);
- if (this.ignoreNodes === null || !textSource.range) {
- return;
- }
+ getTextSourceContent(textSource, length) {
+ const clonedTextSource = textSource.clone();
+
+ clonedTextSource.setEndOffset(length);
- length = textSource.text().length;
- while (textSource.range && length > 0) {
- const nodes = TextSourceRange.getNodesInRange(textSource.range);
- if (!TextSourceRange.anyNodeMatchesSelector(nodes, this.ignoreNodes)) {
- break;
+ if (this.ignoreNodes !== null && clonedTextSource.range) {
+ length = clonedTextSource.text().length;
+ while (clonedTextSource.range && length > 0) {
+ const nodes = TextSourceRange.getNodesInRange(clonedTextSource.range);
+ if (!TextSourceRange.anyNodeMatchesSelector(nodes, this.ignoreNodes)) {
+ break;
+ }
+ --length;
+ clonedTextSource.setEndOffset(length);
}
- --length;
- textSource.setEndOffset(length);
}
+
+ return clonedTextSource.text();
}
clearSelection(passive) {
--
cgit v1.2.3
From a1f8f0d1de7286a55c97a783504b23bd76740e50 Mon Sep 17 00:00:00 2001
From: siikamiika
Date: Tue, 5 May 2020 00:14:21 +0300
Subject: support meta as scanning modifier (#502)
---
ext/bg/data/options-schema.json | 2 +-
ext/bg/settings.html | 1 +
ext/mixed/js/text-scanner.js | 1 +
3 files changed, 3 insertions(+), 1 deletion(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json
index 8622f16b..656da989 100644
--- a/ext/bg/data/options-schema.json
+++ b/ext/bg/data/options-schema.json
@@ -351,7 +351,7 @@
},
"modifier": {
"type": "string",
- "enum": ["none", "alt", "ctrl", "shift"],
+ "enum": ["none", "alt", "ctrl", "shift", "meta"],
"default": "shift"
},
"deepDomScan": {
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index fc9221f8..b8477e46 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -417,6 +417,7 @@
+
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 9dcf6009..cc81f758 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -369,6 +369,7 @@ class TextScanner extends EventDispatcher {
case 'alt': return mouseEvent.altKey;
case 'ctrl': return mouseEvent.ctrlKey;
case 'shift': return mouseEvent.shiftKey;
+ case 'meta': return mouseEvent.metaKey;
case 'none': return true;
default: return false;
}
--
cgit v1.2.3
From 501281e887fb66b490f90e7593639112b058ab97 Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Wed, 6 May 2020 19:27:21 -0400
Subject: Popup init update (#497)
* Add API function to send a message to a specific frameId in a tab
* Update _windowMessageHandlers to support additional info per handler
* Remove message token
* Add new authorization check
* Set up new initialization handler
* Update initialization
* Remove message token
* Replace 'prepare' with 'configure'
* Create new prepare function
* Change configure guard
* Log errors in onMessage
* Improve popup initialize function
* Clear secret/token in _resetFrame
* Remove backend message token
* Clear src and srcdoc attributes before loading
* Don't treat about:blank unloads as load events
---
ext/bg/js/backend.js | 19 +++--
ext/fg/js/float-main.js | 3 +-
ext/fg/js/float.js | 135 ++++++++++++++++++-----------------
ext/fg/js/popup.js | 184 ++++++++++++++++++++++++++++++++++++++++--------
ext/mixed/js/api.js | 8 +--
5 files changed, 246 insertions(+), 103 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 43fa8190..c5173a2e 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -73,8 +73,6 @@ class Backend {
const apiForwarder = new BackendApiForwarder();
apiForwarder.prepare();
- this.messageToken = yomichan.generateId(16);
-
this._defaultBrowserActionTitle = null;
this._isPrepared = false;
this._prepareError = false;
@@ -98,6 +96,7 @@ class Backend {
['commandExec', {handler: this._onApiCommandExec.bind(this), async: false}],
['audioGetUri', {handler: this._onApiAudioGetUri.bind(this), async: true}],
['screenshotGet', {handler: this._onApiScreenshotGet.bind(this), async: true}],
+ ['sendMessageToFrame', {handler: this._onApiSendMessageToFrame.bind(this), async: false}],
['broadcastTab', {handler: this._onApiBroadcastTab.bind(this), async: false}],
['frameInformationGet', {handler: this._onApiFrameInformationGet.bind(this), async: true}],
['injectStylesheet', {handler: this._onApiInjectStylesheet.bind(this), async: true}],
@@ -106,7 +105,6 @@ class Backend {
['getDisplayTemplatesHtml', {handler: this._onApiGetDisplayTemplatesHtml.bind(this), async: true}],
['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}],
['getAnkiDeckNames', {handler: this._onApiGetAnkiDeckNames.bind(this), async: true}],
['getAnkiModelNames', {handler: this._onApiGetAnkiModelNames.bind(this), async: true}],
@@ -600,6 +598,17 @@ class Backend {
});
}
+ _onApiSendMessageToFrame({frameId, action, params}, sender) {
+ if (!(sender && sender.tab)) {
+ return false;
+ }
+
+ const tabId = sender.tab.id;
+ const callback = () => this.checkLastError(chrome.runtime.lastError);
+ chrome.tabs.sendMessage(tabId, {action, params}, {frameId}, callback);
+ return true;
+ }
+
_onApiBroadcastTab({action, params}, sender) {
if (!(sender && sender.tab)) {
return false;
@@ -731,10 +740,6 @@ class Backend {
});
}
- _onApiGetMessageToken() {
- return this.messageToken;
- }
-
_onApiGetDefaultAnkiFieldTemplates() {
return this.defaultAnkiFieldTemplates;
}
diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js
index e7e50a54..20771910 100644
--- a/ext/fg/js/float-main.js
+++ b/ext/fg/js/float-main.js
@@ -56,5 +56,6 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
(async () => {
apiForwardLogsToBackend();
- new DisplayFloat();
+ const display = new DisplayFloat();
+ await display.prepare();
})();
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 77e8edd8..845bf7f6 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -18,7 +18,7 @@
/* global
* Display
* apiBroadcastTab
- * apiGetMessageToken
+ * apiSendMessageToFrame
* popupNestedInitialize
*/
@@ -27,12 +27,11 @@ class DisplayFloat extends Display {
super(document.querySelector('#spinner'), document.querySelector('#definitions'));
this.autoPlayAudioTimer = null;
- this._popupId = null;
+ this._secret = yomichan.generateId(16);
+ this._token = null;
this._orphaned = false;
- this._prepareInvoked = false;
- this._messageToken = null;
- this._messageTokenPromise = null;
+ this._initializedNestedPopups = false;
this._onKeyDownHandlers = new Map([
['C', (e) => {
@@ -46,38 +45,23 @@ class DisplayFloat extends Display {
]);
this._windowMessageHandlers = new Map([
- ['setOptionsContext', ({optionsContext}) => this.setOptionsContext(optionsContext)],
- ['setContent', ({type, details}) => this.setContent(type, details)],
- ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()],
- ['setCustomCss', ({css}) => this.setCustomCss(css)],
- ['prepare', ({popupInfo, optionsContext, childrenSupported, scale}) => this.prepare(popupInfo, optionsContext, childrenSupported, scale)],
- ['setContentScale', ({scale}) => this.setContentScale(scale)]
+ ['initialize', {handler: this._initialize.bind(this), authenticate: false}],
+ ['configure', {handler: this._configure.bind(this)}],
+ ['setOptionsContext', {handler: ({optionsContext}) => this.setOptionsContext(optionsContext)}],
+ ['setContent', {handler: ({type, details}) => this.setContent(type, details)}],
+ ['clearAutoPlayTimer', {handler: () => this.clearAutoPlayTimer()}],
+ ['setCustomCss', {handler: ({css}) => this.setCustomCss(css)}],
+ ['setContentScale', {handler: ({scale}) => this.setContentScale(scale)}]
]);
-
- yomichan.on('orphaned', this.onOrphaned.bind(this));
- window.addEventListener('message', this.onMessage.bind(this), false);
}
- async prepare(popupInfo, optionsContext, childrenSupported, scale) {
- if (this._prepareInvoked) { return; }
- this._prepareInvoked = true;
-
- const {id, parentFrameId} = popupInfo;
- this._popupId = id;
-
- this.optionsContext = optionsContext;
-
+ async prepare() {
await super.prepare();
- await this.updateOptions();
-
- if (childrenSupported) {
- const {depth, url} = optionsContext;
- popupNestedInitialize(id, depth, parentFrameId, url);
- }
- this.setContentScale(scale);
+ yomichan.on('orphaned', this.onOrphaned.bind(this));
+ window.addEventListener('message', this.onMessage.bind(this), false);
- apiBroadcastTab('popupPrepareCompleted', {targetPopupId: this._popupId});
+ apiBroadcastTab('popupPrepared', {secret: this._secret});
}
onError(error) {
@@ -102,46 +86,30 @@ class DisplayFloat extends Display {
onMessage(e) {
const data = e.data;
- if (typeof data !== 'object' || data === null) { return; } // Invalid data
-
- const token = data.token;
- if (typeof token !== 'string') { return; } // Invalid data
-
- if (this._messageToken === null) {
- // Async
- this.getMessageToken()
- .then(
- () => { this.handleAction(token, data); },
- () => {}
- );
- } else {
- // Sync
- this.handleAction(token, data);
+ if (typeof data !== 'object' || data === null) {
+ this._logMessageError(e, 'Invalid data');
+ return;
}
- }
- async getMessageToken() {
- // this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made.
- if (this._messageTokenPromise === null) {
- this._messageTokenPromise = apiGetMessageToken();
- }
- const messageToken = await this._messageTokenPromise;
- if (this._messageToken === null) {
- this._messageToken = messageToken;
+ const action = data.action;
+ if (typeof action !== 'string') {
+ this._logMessageError(e, 'Invalid data');
+ return;
}
- this._messageTokenPromise = null;
- }
- handleAction(token, {action, params}) {
- if (token !== this._messageToken) {
- // Invalid token
+ const handlerInfo = this._windowMessageHandlers.get(action);
+ if (typeof handlerInfo === 'undefined') {
+ this._logMessageError(e, `Invalid action: ${JSON.stringify(action)}`);
return;
}
- const handler = this._windowMessageHandlers.get(action);
- if (typeof handler !== 'function') { return; }
+ if (handlerInfo.authenticate !== false && !this._isMessageAuthenticated(data)) {
+ this._logMessageError(e, 'Invalid authentication');
+ return;
+ }
- handler(params);
+ const handler = handlerInfo.handler;
+ handler(data.params);
}
autoPlayAudio() {
@@ -193,4 +161,45 @@ class DisplayFloat extends Display {
return '';
}
}
+
+ _logMessageError(event, type) {
+ yomichan.logWarning(new Error(`Popup received invalid message from origin ${JSON.stringify(event.origin)}: ${type}`));
+ }
+
+ _initialize(params) {
+ if (this._token !== null) { return; } // Already initialized
+ if (!isObject(params)) { return; } // Invalid data
+
+ const secret = params.secret;
+ if (secret !== this._secret) { return; } // Invalid authentication
+
+ const {token, frameId} = params;
+ this._token = token;
+
+ apiSendMessageToFrame(frameId, 'popupInitialized', {secret, token});
+ }
+
+ async _configure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) {
+ this.optionsContext = optionsContext;
+
+ await this.updateOptions();
+
+ if (childrenSupported && !this._initializedNestedPopups) {
+ const {depth, url} = optionsContext;
+ popupNestedInitialize(popupId, depth, frameId, url);
+ this._initializedNestedPopups = true;
+ }
+
+ this.setContentScale(scale);
+
+ apiSendMessageToFrame(frameId, 'popupConfigured', {messageId});
+ }
+
+ _isMessageAuthenticated(message) {
+ return (
+ this._token !== null &&
+ this._token === message.token &&
+ this._secret === message.secret
+ );
+ }
}
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index f5cb6f77..7db53f0d 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -17,7 +17,6 @@
/* global
* DOM
- * apiGetMessageToken
* apiInjectStylesheet
* apiOptionsGet
*/
@@ -39,8 +38,9 @@ class Popup {
this._contentScale = 1.0;
this._containerSizeContentScale = null;
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
- this._messageToken = null;
this._previousOptionsContextSource = null;
+ this._containerSecret = null;
+ this._containerToken = null;
this._container = document.createElement('iframe');
this._container.className = 'yomichan-float';
@@ -216,40 +216,154 @@ class Popup {
return injectPromise;
}
+ _initializeFrame(frame, targetOrigin, frameId, setupFrame, timeout=10000) {
+ return new Promise((resolve, reject) => {
+ const tokenMap = new Map();
+ let timer = null;
+ let containerLoadedResolve = null;
+ let containerLoadedReject = null;
+ const containerLoaded = new Promise((resolve2, reject2) => {
+ containerLoadedResolve = resolve2;
+ containerLoadedReject = reject2;
+ });
+
+ const postMessage = (action, params) => {
+ const contentWindow = frame.contentWindow;
+ if (contentWindow === null) { throw new Error('Frame missing content window'); }
+
+ let validOrigin = true;
+ try {
+ validOrigin = (contentWindow.location.origin === targetOrigin);
+ } catch (e) {
+ // NOP
+ }
+ if (!validOrigin) { throw new Error('Unexpected frame origin'); }
+
+ contentWindow.postMessage({action, params}, targetOrigin);
+ };
+
+ const onMessage = (message) => {
+ onMessageInner(message);
+ return false;
+ };
+
+ const onMessageInner = async (message) => {
+ try {
+ if (!isObject(message)) { return; }
+ const {action, params} = message;
+ if (!isObject(params)) { return; }
+ await containerLoaded;
+ if (timer === null) { return; } // Done
+
+ switch (action) {
+ case 'popupPrepared':
+ {
+ const {secret} = params;
+ const token = yomichan.generateId(16);
+ tokenMap.set(secret, token);
+ postMessage('initialize', {secret, token, frameId});
+ }
+ break;
+ case 'popupInitialized':
+ {
+ const {secret, token} = params;
+ const token2 = tokenMap.get(secret);
+ if (typeof token2 !== 'undefined' && token === token2) {
+ cleanup();
+ resolve({secret, token});
+ }
+ }
+ break;
+ }
+ } catch (e) {
+ cleanup();
+ reject(e);
+ }
+ };
+
+ const onLoad = () => {
+ if (containerLoadedResolve === null) {
+ cleanup();
+ reject(new Error('Unexpected load event'));
+ return;
+ }
+
+ if (Popup.isFrameAboutBlank(frame)) {
+ return;
+ }
+
+ containerLoadedResolve();
+ containerLoadedResolve = null;
+ containerLoadedReject = null;
+ };
+
+ const cleanup = () => {
+ if (timer === null) { return; } // Done
+ clearTimeout(timer);
+ timer = null;
+
+ containerLoadedResolve = null;
+ if (containerLoadedReject !== null) {
+ containerLoadedReject(new Error('Terminated'));
+ containerLoadedReject = null;
+ }
+
+ chrome.runtime.onMessage.removeListener(onMessage);
+ frame.removeEventListener('load', onLoad);
+ };
+
+ // Start
+ timer = setTimeout(() => {
+ cleanup();
+ reject(new Error('Timeout'));
+ }, timeout);
+
+ chrome.runtime.onMessage.addListener(onMessage);
+ frame.addEventListener('load', onLoad);
+
+ // Prevent unhandled rejections
+ containerLoaded.catch(() => {}); // NOP
+
+ setupFrame(frame);
+ });
+ }
+
async _createInjectPromise() {
- if (this._messageToken === null) {
- this._messageToken = await apiGetMessageToken();
- }
+ this._injectStyles();
+
+ const {secret, token} = await this._initializeFrame(this._container, this._targetOrigin, this._frameId, (frame) => {
+ frame.removeAttribute('src');
+ frame.removeAttribute('srcdoc');
+ frame.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
+ this._observeFullscreen(true);
+ this._onFullscreenChanged();
+ });
+ this._containerSecret = secret;
+ this._containerToken = token;
+ // Configure
+ const messageId = yomichan.generateId(16);
const popupPreparedPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage,
- ({action, params}, {resolve}) => {
+ (message, {resolve}) => {
if (
- action === 'popupPrepareCompleted' &&
- isObject(params) &&
- params.targetPopupId === this._id
+ isObject(message) &&
+ message.action === 'popupConfigured' &&
+ isObject(message.params) &&
+ message.params.messageId === messageId
) {
resolve();
}
}
);
-
- const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);
- this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
- this._container.addEventListener('load', () => {
- this._invokeApi('prepare', {
- popupInfo: {
- id: this._id,
- parentFrameId
- },
- optionsContext: this._optionsContext,
- childrenSupported: this._childrenSupported,
- scale: this._contentScale
- });
+ this._invokeApi('configure', {
+ messageId,
+ frameId: this._frameId,
+ popupId: this._id,
+ optionsContext: this._optionsContext,
+ childrenSupported: this._childrenSupported,
+ scale: this._contentScale
});
- this._observeFullscreen(true);
- this._onFullscreenChanged();
- this._injectStyles();
return popupPreparedPromise;
}
@@ -267,6 +381,8 @@ class Popup {
this._container.removeAttribute('src');
this._container.removeAttribute('srcdoc');
+ this._containerSecret = null;
+ this._containerToken = null;
this._injectPromise = null;
this._injectPromiseComplete = false;
}
@@ -401,11 +517,12 @@ class Popup {
}
_invokeApi(action, params={}) {
- const token = this._messageToken;
+ const secret = this._containerSecret;
+ const token = this._containerToken;
const contentWindow = this._container.contentWindow;
- if (token === null || contentWindow === null) { return; }
+ if (secret === null || token === null || contentWindow === null) { return; }
- contentWindow.postMessage({action, params, token}, this._targetOrigin);
+ contentWindow.postMessage({action, params, secret, token}, this._targetOrigin);
}
_getFrameParentElement() {
@@ -653,6 +770,17 @@ class Popup {
injectedStylesheets.set(id, styleNode);
return styleNode;
}
+
+ static isFrameAboutBlank(frame) {
+ try {
+ const contentDocument = frame.contentDocument;
+ if (contentDocument === null) { return false; }
+ const url = contentDocument.location.href;
+ return /^about:blank(?:[#?]|$)/.test(url);
+ } catch (e) {
+ return false;
+ }
+ }
}
Popup._injectedStylesheets = new Map();
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index bf85338e..ca4bdd6c 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -76,6 +76,10 @@ function apiScreenshotGet(options) {
return _apiInvoke('screenshotGet', {options});
}
+function apiSendMessageToFrame(frameId, action, params) {
+ return _apiInvoke('sendMessageToFrame', {frameId, action, params});
+}
+
function apiBroadcastTab(action, params) {
return _apiInvoke('broadcastTab', {action, params});
}
@@ -108,10 +112,6 @@ function apiGetZoom() {
return _apiInvoke('getZoom');
}
-function apiGetMessageToken() {
- return _apiInvoke('getMessageToken');
-}
-
function apiGetDefaultAnkiFieldTemplates() {
return _apiInvoke('getDefaultAnkiFieldTemplates');
}
--
cgit v1.2.3
From 021ccb5ac3038f63d07ccc9575ee56480031a251 Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Wed, 6 May 2020 19:28:26 -0400
Subject: Move util database modification functions (#499)
* Update onProgress callback to handle multiple arguments
* Add apiImportDictionaryArchive
* Add apiDeleteDictionary
* Make onProgress the last argument for consistency
* Remove deprecated util functions
* Fix issue with missing progress args
* Remove function calls which modify the database from Translator
* Update tests
* Fix errors not being serialized correctly in _createActionListenerPort
---
ext/bg/js/backend.js | 23 +++++++++++++++++++----
ext/bg/js/database.js | 2 +-
ext/bg/js/dictionary-importer.js | 2 +-
ext/bg/js/settings/dictionaries.js | 18 ++++++++++++++----
ext/bg/js/translator.js | 8 +-------
ext/bg/js/util.js | 25 -------------------------
ext/mixed/js/api.js | 10 +++++++++-
test/test-database.js | 18 +++++++++---------
8 files changed, 54 insertions(+), 52 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index c5173a2e..d454aa22 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -117,7 +117,10 @@ class Backend {
['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}],
['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}]
]);
- this._messageHandlersWithProgress = new Map();
+ this._messageHandlersWithProgress = new Map([
+ ['importDictionaryArchive', {handler: this._onApiImportDictionaryArchive.bind(this), async: true}],
+ ['deleteDictionary', {handler: this._onApiDeleteDictionary.bind(this), async: true}]
+ ]);
this._commandHandlers = new Map([
['search', this._onCommandSearch.bind(this)],
@@ -771,7 +774,8 @@ class Backend {
async _onApiPurgeDatabase(params, sender) {
this._validatePrivilegedMessageSender(sender);
- return await this.translator.purgeDatabase();
+ this.translator.clearDatabaseCaches();
+ await this.database.purge();
}
async _onApiGetMedia({targets}) {
@@ -814,12 +818,23 @@ class Backend {
return portName;
}
+ async _onApiImportDictionaryArchive({archiveContent, details}, sender, onProgress) {
+ this._validatePrivilegedMessageSender(sender);
+ return await this.dictionaryImporter.import(this.database, archiveContent, details, onProgress);
+ }
+
+ async _onApiDeleteDictionary({dictionaryName}, sender, onProgress) {
+ this._validatePrivilegedMessageSender(sender);
+ this.translator.clearDatabaseCaches();
+ await this.database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress);
+ }
+
// Command handlers
_createActionListenerPort(port, sender, handlers) {
let hasStarted = false;
- const onProgress = (data) => {
+ const onProgress = (...data) => {
try {
if (port === null) { return; }
port.postMessage({type: 'progress', data});
@@ -847,7 +862,7 @@ class Backend {
port.postMessage({type: 'complete', data: result});
} catch (e) {
if (port !== null) {
- port.postMessage({type: 'error', data: e});
+ port.postMessage({type: 'error', data: errorToJson(e)});
}
cleanup();
}
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index a94aa720..930cd0d0 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -129,7 +129,7 @@ class Database {
await this.prepare();
}
- async deleteDictionary(dictionaryName, onProgress, progressSettings) {
+ async deleteDictionary(dictionaryName, progressSettings, onProgress) {
this._validate();
const targets = [
diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js
index 3727f7ee..10e30cec 100644
--- a/ext/bg/js/dictionary-importer.js
+++ b/ext/bg/js/dictionary-importer.js
@@ -27,7 +27,7 @@ class DictionaryImporter {
this._schemas = new Map();
}
- async import(database, archiveSource, onProgress, details) {
+ async import(database, archiveSource, details, onProgress) {
if (!database) {
throw new Error('Invalid database');
}
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index 50add4c7..632c01ea 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -17,8 +17,10 @@
/* global
* PageExitPrevention
+ * apiDeleteDictionary
* apiGetDictionaryCounts
* apiGetDictionaryInfo
+ * apiImportDictionaryArchive
* apiOptionsGet
* apiOptionsGetFull
* apiPurgeDatabase
@@ -29,8 +31,6 @@
* storageEstimate
* storageUpdateStats
* utilBackgroundIsolate
- * utilDatabaseDeleteDictionary
- * utilDatabaseImport
*/
let dictionaryUI = null;
@@ -312,7 +312,7 @@ class SettingsDictionaryEntryUI {
progressBar.style.width = `${percent}%`;
};
- await utilDatabaseDeleteDictionary(this.dictionaryInfo.title, onProgress, {rate: 1000});
+ await apiDeleteDictionary(this.dictionaryInfo.title, onProgress);
} catch (e) {
dictionaryErrorsShow([e]);
} finally {
@@ -679,7 +679,8 @@ async function onDictionaryImport(e) {
dictImportInfo.textContent = `(${i + 1} of ${ii})`;
}
- const {result, errors} = await utilDatabaseImport(files[i], updateProgress, importDetails);
+ const archiveContent = await dictReadFile(files[i]);
+ const {result, errors} = await apiImportDictionaryArchive(archiveContent, importDetails, updateProgress);
for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
dictionaryOptions.enabled = true;
@@ -713,6 +714,15 @@ async function onDictionaryImport(e) {
}
}
+function dictReadFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ reader.readAsBinaryString(file);
+ });
+}
+
async function onDatabaseEnablePrefixWildcardSearchesChanged(e) {
const optionsFull = await getOptionsFullMutable();
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 8708e4d8..3fd329d1 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -45,14 +45,8 @@ class Translator {
this.deinflector = new Deinflector(reasons);
}
- async purgeDatabase() {
+ clearDatabaseCaches() {
this.tagCache.clear();
- await this.database.purge();
- }
-
- async deleteDictionary(dictionaryName) {
- this.tagCache.clear();
- await this.database.deleteDictionary(dictionaryName);
}
async getSequencedDefinitions(definitions, mainDictionary) {
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index d2fb0e49..8f86e47a 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -66,31 +66,6 @@ function utilBackend() {
return backend;
}
-async function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
- return utilIsolate(await utilBackend().translator.database.deleteDictionary(
- utilBackgroundIsolate(dictionaryName),
- utilBackgroundFunctionIsolate(onProgress)
- ));
-}
-
-async function utilDatabaseImport(data, onProgress, details) {
- data = await utilReadFile(data);
- return utilIsolate(await utilBackend().importDictionary(
- utilBackgroundIsolate(data),
- utilBackgroundFunctionIsolate(onProgress),
- utilBackgroundIsolate(details)
- ));
-}
-
-function utilReadFile(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.onerror = () => reject(reader.error);
- reader.readAsBinaryString(file);
- });
-}
-
function utilReadFileArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index ca4bdd6c..af97ac3d 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -152,6 +152,14 @@ function apiLogIndicatorClear() {
return _apiInvoke('logIndicatorClear');
}
+function apiImportDictionaryArchive(archiveContent, details, onProgress) {
+ return _apiInvokeWithProgress('importDictionaryArchive', {archiveContent, details}, onProgress);
+}
+
+function apiDeleteDictionary(dictionaryName, onProgress) {
+ return _apiInvokeWithProgress('deleteDictionary', {dictionaryName}, onProgress);
+}
+
function _apiCreateActionPort(timeout=5000) {
return new Promise((resolve, reject) => {
let timer = null;
@@ -213,7 +221,7 @@ function _apiInvokeWithProgress(action, params, onProgress, timeout=5000) {
break;
case 'progress':
try {
- onProgress(message.data);
+ onProgress(...message.data);
} catch (e) {
// NOP
}
diff --git a/test/test-database.js b/test/test-database.js
index 3684051b..e8a4a343 100644
--- a/test/test-database.js
+++ b/test/test-database.js
@@ -233,10 +233,10 @@ async function testDatabase1() {
let progressEvent = false;
await database.deleteDictionary(
title,
+ {rate: 1000},
() => {
progressEvent = true;
- },
- {rate: 1000}
+ }
);
assert.ok(progressEvent);
@@ -267,10 +267,10 @@ async function testDatabase1() {
const {result, errors} = await dictionaryImporter.import(
database,
testDictionarySource,
+ {prefixWildcardsSupported: true},
() => {
progressEvent = true;
- },
- {prefixWildcardsSupported: true}
+ }
);
vm.assert.deepStrictEqual(errors, []);
vm.assert.deepStrictEqual(result, expectedSummary);
@@ -908,7 +908,7 @@ async function testDatabase2() {
// Error: not prepared
await assert.rejects(async () => await database.purge());
- await assert.rejects(async () => await database.deleteDictionary(title, () => {}, {}));
+ await assert.rejects(async () => await database.deleteDictionary(title, {}, () => {}));
await assert.rejects(async () => await database.findTermsBulk(['?'], titles, null));
await assert.rejects(async () => await database.findTermsExactBulk(['?'], ['?'], titles));
await assert.rejects(async () => await database.findTermsBySequenceBulk([1], title));
@@ -919,17 +919,17 @@ async function testDatabase2() {
await assert.rejects(async () => await database.findTagForTitle('tag', title));
await assert.rejects(async () => await database.getDictionaryInfo());
await assert.rejects(async () => await database.getDictionaryCounts(titles, true));
- await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {}));
+ await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, {}, () => {}));
await database.prepare();
// Error: already prepared
await assert.rejects(async () => await database.prepare());
- await dictionaryImporter.import(database, testDictionarySource, () => {}, {});
+ await dictionaryImporter.import(database, testDictionarySource, {}, () => {});
// Error: dictionary already imported
- await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, () => {}, {}));
+ await assert.rejects(async () => await dictionaryImporter.import(database, testDictionarySource, {}, () => {}));
await database.close();
}
@@ -956,7 +956,7 @@ async function testDatabase3() {
let error = null;
try {
- await dictionaryImporter.import(database, testDictionarySource, () => {}, {});
+ await dictionaryImporter.import(database, testDictionarySource, {}, () => {});
} catch (e) {
error = e;
}
--
cgit v1.2.3
From bb2d9501afc0e406b0dacf5675cd90985238be98 Mon Sep 17 00:00:00 2001
From: toasted-nutbread
Date: Wed, 6 May 2020 19:32:28 -0400
Subject: Add apiModifySettings (#501)
* Update getProfile/getProfileFromContext to store this.options in a variable
* Add useSchema parameter to options getter functions
* Add apiModifySettings
* Use apiModifySettings instead of apiOptionsSet
* Remove apiOptionsSet
* Fix incorrect deleteCount check
* Require explicit scope for options
* Throw on invalid scope
---
ext/bg/background.html | 1 +
ext/bg/js/backend.js | 138 ++++++++++++++++++++++++---------------
ext/bg/js/search-query-parser.js | 22 +++++--
ext/bg/js/search.js | 30 +++++++--
ext/mixed/js/api.js | 8 +--
5 files changed, 132 insertions(+), 67 deletions(-)
(limited to 'ext/mixed')
diff --git a/ext/bg/background.html b/ext/bg/background.html
index ee5a1f32..9c740adf 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -46,6 +46,7 @@
+