summaryrefslogtreecommitdiff
path: root/ext/mixed
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2019-10-20 11:23:20 -0700
committerAlex Yatskov <alex@foosoft.net>2019-10-20 11:23:20 -0700
commit438498435227cfa59cf9ed3430045b288cd2a7c0 (patch)
tree6a05520e5d6fa8d26d372673a9ed3e5d2da7e3fd /ext/mixed
parent06d7713189be9eb51669d3842b78278371e6cfa4 (diff)
parentd32fd1381b6cd5141a21c22f9ef639b2fe9774fb (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/mixed')
-rw-r--r--ext/mixed/css/display-dark.css50
-rw-r--r--ext/mixed/css/display-default.css50
-rw-r--r--ext/mixed/css/display.css96
-rw-r--r--ext/mixed/js/audio.js128
-rw-r--r--ext/mixed/js/display.js200
5 files changed, 424 insertions, 100 deletions
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
new file mode 100644
index 00000000..34a0ccd1
--- /dev/null
+++ b/ext/mixed/css/display-dark.css
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the entrys of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+body { background-color: #1e1e1e; color: #d4d4d4; }
+
+hr { border-top-color: #2f2f2f; }
+
+.tag-default { background-color: #69696e; }
+.tag-name { background-color: #489148; }
+.tag-expression { background-color: #b07f39; }
+.tag-popular { background-color: #025caa; }
+.tag-frequent { background-color: #4490a7; }
+.tag-archaism { background-color: #b04340; }
+.tag-dictionary { background-color: #9057ad; }
+.tag-frequency { background-color: #489148; }
+.tag-partOfSpeech { background-color: #565656; }
+
+.reasons { color: #888888; }
+.glossary li { color: #888888; }
+.glossary-item { color: #d4d4d4; }
+.label { color: #e1e1e1; }
+
+.expression .kanji-link {
+ border-bottom-color: #888888;
+ color: #CCCCCC;
+}
+
+.expression-popular, .expression-popular .kanji-link {
+ color: #0275d8;
+}
+
+.expression-rare, .expression-rare .kanji-link {
+ color: #666666;
+}
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
new file mode 100644
index 00000000..176c5387
--- /dev/null
+++ b/ext/mixed/css/display-default.css
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the entrys of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+body { background-color: #ffffff; color: #333333; }
+
+hr { border-top-color: #eeeeee; }
+
+.tag-default { background-color: #8a8a91; }
+.tag-name { background-color: #5cb85c; }
+.tag-expression { background-color: #f0ad4e; }
+.tag-popular { background-color: #0275d8; }
+.tag-frequent { background-color: #5bc0de; }
+.tag-archaism { background-color: #d9534f; }
+.tag-dictionary { background-color: #aa66cc; }
+.tag-frequency { background-color: #5cb85c; }
+.tag-partOfSpeech { background-color: #565656; }
+
+.reasons { color: #777777; }
+.glossary li { color: #777777; }
+.glossary-item { color: #000000; }
+.label { color: #ffffff; }
+
+.expression .kanji-link {
+ border-bottom-color: #777777;
+ color: #333333;
+}
+
+.expression-popular, .expression-popular .kanji-link {
+ color: #0275d8;
+}
+
+.expression-rare, .expression-rare .kanji-link {
+ color: #999999;
+}
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 8a4cf4a7..7793ddeb 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -30,9 +30,31 @@
* General
*/
+html:root[data-yomichan-page=float]:not([data-yomichan-theme]),
+html:root[data-yomichan-page=float]:not([data-yomichan-theme]) body {
+ background-color: transparent;
+}
+
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ margin: 0;
+ border: 0;
+ padding: 0;
+}
+
hr {
padding: 0px;
margin: 0px;
+ border: 0;
+ border-top-width: 1px;
+ border-top-style: solid;
+}
+
+ol, ul {
+ margin-top: 0;
+ margin-bottom: 10px;
}
#spinner {
@@ -60,40 +82,10 @@ hr {
padding-bottom: 10px;
}
-.tag-default {
- background-color: #8a8a91;
-}
-
-.tag-name {
- background-color: #5cb85c;
-}
-
-.tag-expression {
- background-color: #f0ad4e;
-}
-
-.tag-popular {
- background-color: #0275d8;
-}
-
-.tag-frequent {
- background-color: #5bc0de;
-}
-
-.tag-archaism {
- background-color: #d9534f;
-}
-
-.tag-dictionary {
- background-color: #aa66cc;
-}
-
-.tag-frequency {
- background-color: #5cb85c;
-}
-
-.tag-partOfSpeech {
- background-color: #565656;
+html:root[data-yomichan-page=float] .entry,
+html:root[data-yomichan-page=float] .note {
+ padding-left: 10px;
+ padding-right: 10px;
}
.actions .disabled {
@@ -103,6 +95,7 @@ hr {
.actions .disabled img {
-webkit-filter: grayscale(100%);
+ filter: grayscale(100%);
opacity: 0.25;
}
@@ -111,7 +104,7 @@ hr {
}
.actions {
- display: inline-block;
+ display: block;
float: right;
}
@@ -127,19 +120,11 @@ hr {
}
.expression .kanji-link {
- border-bottom: 1px #777 dashed;
- color: #333;
+ border-bottom-width: 1px;
+ border-bottom-style: dashed;
text-decoration: none;
}
-.expression-popular, .expression-popular .kanji-link {
- color: #0275d8;
-}
-
-.expression-rare, .expression-rare .kanji-link {
- color: #999;
-}
-
.expression .peek-wrapper {
font-size: 14px;
white-space: nowrap;
@@ -173,7 +158,6 @@ hr {
}
.reasons {
- color: #777;
display: inline-block;
}
@@ -199,14 +183,6 @@ hr {
content: " | ";
}
-.glossary li {
- color: #777;
-}
-
-.glossary-item {
- color: #000;
-}
-
div.glossary-item.compact-glossary {
display: inline;
}
@@ -234,3 +210,15 @@ div.glossary-item.compact-glossary {
.entry:not(.entry-current) .current {
display: none;
}
+
+.label {
+ display: inline;
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25em;
+}
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
index b905140c..cf8b8d24 100644
--- a/ext/mixed/js/audio.js
+++ b/ext/mixed/js/audio.js
@@ -17,7 +17,90 @@
*/
-function audioGetFromUrl(url) {
+class TextToSpeechAudio {
+ constructor(text, voice) {
+ this.text = text;
+ this.voice = voice;
+ this._utterance = null;
+ this._volume = 1;
+ }
+
+ get currentTime() {
+ return 0;
+ }
+ set currentTime(value) {
+ // NOP
+ }
+
+ get volume() {
+ return this._volume;
+ }
+ set volume(value) {
+ this._volume = value;
+ if (this._utterance !== null) {
+ this._utterance.volume = value;
+ }
+ }
+
+ play() {
+ try {
+ if (this._utterance === null) {
+ this._utterance = new SpeechSynthesisUtterance(this.text || '');
+ this._utterance.lang = 'ja-JP';
+ this._utterance.volume = this._volume;
+ this._utterance.voice = this.voice;
+ }
+
+ speechSynthesis.cancel();
+ speechSynthesis.speak(this._utterance);
+
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ pause() {
+ try {
+ speechSynthesis.cancel();
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static createFromUri(ttsUri) {
+ const m = /^tts:[^#\?]*\?([^#]*)/.exec(ttsUri);
+ if (m === null) { return null; }
+
+ const searchParameters = {};
+ for (const group of m[1].split('&')) {
+ const sep = group.indexOf('=');
+ if (sep < 0) { continue; }
+ searchParameters[decodeURIComponent(group.substr(0, sep))] = decodeURIComponent(group.substr(sep + 1));
+ }
+
+ if (!searchParameters.text) { return null; }
+
+ const voice = audioGetTextToSpeechVoice(searchParameters.voice);
+ if (voice === null) { return null; }
+
+ return new TextToSpeechAudio(searchParameters.text, voice);
+ }
+
+}
+
+function audioGetFromUrl(url, download) {
+ const tts = TextToSpeechAudio.createFromUri(url);
+ if (tts !== null) {
+ if (download) {
+ throw new Error('Download not supported for text-to-speech');
+ }
+ return Promise.resolve(tts);
+ }
+
+ if (download) {
+ return Promise.resolve(null);
+ }
+
return new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.addEventListener('loadeddata', () => {
@@ -32,7 +115,7 @@ function audioGetFromUrl(url) {
});
}
-async function audioGetFromSources(expression, sources, optionsContext, createAudioObject, cache=null) {
+async function audioGetFromSources(expression, sources, optionsContext, download, cache=null) {
const key = `${expression.expression}:${expression.reading}`;
if (cache !== null && cache.hasOwnProperty(expression)) {
return cache[key];
@@ -46,7 +129,7 @@ async function audioGetFromSources(expression, sources, optionsContext, createAu
}
try {
- const audio = createAudioObject ? await audioGetFromUrl(url) : null;
+ const audio = await audioGetFromUrl(url, download);
const result = {audio, url, source};
if (cache !== null) {
cache[key] = result;
@@ -56,5 +139,42 @@ async function audioGetFromSources(expression, sources, optionsContext, createAu
// NOP
}
}
- return {audio: null, source: null};
+ return {audio: null, url: null, source: null};
+}
+
+function audioGetTextToSpeechVoice(voiceURI) {
+ try {
+ for (const voice of speechSynthesis.getVoices()) {
+ if (voice.voiceURI === voiceURI) {
+ return voice;
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+ return null;
+}
+
+function audioPrepareTextToSpeech(options) {
+ if (
+ audioPrepareTextToSpeech.state ||
+ !options.audio.textToSpeechVoice ||
+ !(
+ options.audio.sources.includes('text-to-speech') ||
+ options.audio.sources.includes('text-to-speech-reading')
+ )
+ ) {
+ // Text-to-speech not in use.
+ return;
+ }
+
+ // Chrome needs this value called once before it will become populated.
+ // The first call will return an empty list.
+ audioPrepareTextToSpeech.state = true;
+ try {
+ speechSynthesis.getVoices();
+ } catch (e) {
+ // NOP
+ }
}
+audioPrepareTextToSpeech.state = false;
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 22181301..b40228b0 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -29,15 +29,16 @@ class Display {
this.audioPlaying = null;
this.audioFallback = null;
this.audioCache = {};
- this.optionsContext = {};
- this.eventListeners = [];
+ this.styleNode = null;
- this.dependencies = {};
+ this.eventListeners = [];
+ this.persistentEventListeners = [];
+ this.interactive = false;
+ this.eventListenersActive = false;
this.windowScroll = new WindowScroll();
- document.addEventListener('keydown', this.onKeyDown.bind(this));
- document.addEventListener('wheel', this.onWheel.bind(this), {passive: false});
+ this.setInteractive(true);
}
onError(error) {
@@ -73,8 +74,8 @@ class Display {
context.source.source = this.context.source;
}
- const kanjiDefs = await apiKanjiFind(link.textContent, this.optionsContext);
- this.kanjiShow(kanjiDefs, this.options, context);
+ const definitions = await apiKanjiFind(link.textContent, this.getOptionsContext());
+ this.setContentKanji(definitions, context);
} catch (e) {
this.onError(e);
}
@@ -84,8 +85,6 @@ class Display {
try {
e.preventDefault();
- const {docRangeFromPoint, docSentenceExtract} = this.dependencies;
-
const clickedElement = e.target;
const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options);
if (textSource === null) {
@@ -96,7 +95,7 @@ class Display {
try {
textSource.setEndOffset(this.options.scanning.length);
- ({definitions, length} = await apiTermsFind(textSource.text(), this.optionsContext));
+ ({definitions, length} = await apiTermsFind(textSource.text(), this.getOptionsContext()));
if (definitions.length === 0) {
return false;
}
@@ -123,7 +122,7 @@ class Display {
context.source.source = this.context.source;
}
- this.termsShow(definitions, this.options, context);
+ this.setContentTerms(definitions, context);
} catch (e) {
this.onError(e);
}
@@ -172,16 +171,124 @@ class Display {
}
}
- async termsShow(definitions, options, context) {
+ onRuntimeMessage({action, params}, sender, callback) {
+ const handlers = Display.runtimeMessageHandlers;
+ if (handlers.hasOwnProperty(action)) {
+ const handler = handlers[action];
+ const result = handler(this, params);
+ callback(result);
+ }
+ }
+
+ getOptionsContext() {
+ throw new Error('Override me');
+ }
+
+ isInitialized() {
+ return this.options !== null;
+ }
+
+ async initialize(options=null) {
+ await this.updateOptions(options);
+ chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
+ }
+
+ async updateOptions(options) {
+ this.options = options ? options : await apiOptionsGet(this.getOptionsContext());
+ this.updateTheme(this.options.general.popupTheme);
+ this.setCustomCss(this.options.general.customPopupCss);
+ audioPrepareTextToSpeech(this.options);
+ }
+
+ updateTheme(themeName) {
+ document.documentElement.dataset.yomichanTheme = themeName;
+
+ const stylesheets = document.querySelectorAll('link[data-yomichan-theme-name]');
+ for (const stylesheet of stylesheets) {
+ const match = (stylesheet.dataset.yomichanThemeName === themeName);
+ stylesheet.rel = (match ? 'stylesheet' : 'stylesheet alternate');
+ }
+ }
+
+ setCustomCss(css) {
+ if (this.styleNode === null) {
+ if (css.length === 0) { return; }
+ this.styleNode = document.createElement('style');
+ }
+
+ this.styleNode.textContent = css;
+
+ const parent = document.head;
+ if (this.styleNode.parentNode !== parent) {
+ parent.appendChild(this.styleNode);
+ }
+ }
+
+ setInteractive(interactive) {
+ interactive = !!interactive;
+ if (this.interactive === interactive) { return; }
+ this.interactive = interactive;
+
+ if (interactive) {
+ Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false);
+ Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false});
+ } else {
+ Display.clearEventListeners(this.persistentEventListeners);
+ }
+ this.setEventListenersActive(this.eventListenersActive);
+ }
+
+ setEventListenersActive(active) {
+ active = !!active && this.interactive;
+ if (this.eventListenersActive === active) { return; }
+ this.eventListenersActive = active;
+
+ if (active) {
+ this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
+ this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
+ this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
+ this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
+ this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
+ if (this.options.scanning.enablePopupSearch) {
+ this.addEventListeners('.glossary-item', 'click', this.onTermLookup.bind(this));
+ }
+ } else {
+ Display.clearEventListeners(this.eventListeners);
+ }
+ }
+
+ addEventListeners(selector, type, listener, options) {
+ this.container.querySelectorAll(selector).forEach((node) => {
+ Display.addEventListener(this.eventListeners, node, type, listener, options);
+ });
+ }
+
+ setContent(type, details) {
+ switch (type) {
+ case 'terms':
+ return this.setContentTerms(details.definitions, details.context);
+ case 'kanji':
+ return this.setContentKanji(details.definitions, details.context);
+ case 'orphaned':
+ return this.setContentOrphaned();
+ default:
+ return Promise.resolve();
+ }
+ }
+
+ async setContentTerms(definitions, context) {
+ if (!this.isInitialized()) { return; }
+
try {
- this.clearEventListeners();
+ const options = this.options;
+
+ this.setEventListenersActive(false);
if (!context || context.focus !== false) {
window.focus();
}
this.definitions = definitions;
- this.options = options;
this.context = context;
const sequence = ++this.sequence;
@@ -211,18 +318,11 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- if (this.options.audio.enabled && this.options.audio.autoPlay) {
+ if (options.audio.enabled && options.audio.autoPlay) {
this.autoPlayAudio();
}
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
- this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
- this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
- if (this.options.scanning.enablePopupSearch) {
- this.addEventListeners('.glossary-item', 'click', this.onTermLookup.bind(this));
- }
+ this.setEventListenersActive(true);
await this.adderButtonUpdate(['term-kanji', 'term-kana'], sequence);
} catch (e) {
@@ -230,16 +330,19 @@ class Display {
}
}
- async kanjiShow(definitions, options, context) {
+ async setContentKanji(definitions, context) {
+ if (!this.isInitialized()) { return; }
+
try {
- this.clearEventListeners();
+ const options = this.options;
+
+ this.setEventListenersActive(false);
if (!context || context.focus !== false) {
window.focus();
}
this.definitions = definitions;
- this.options = options;
this.context = context;
const sequence = ++this.sequence;
@@ -265,9 +368,7 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));
+ this.setEventListenersActive(true);
await this.adderButtonUpdate(['kanji'], sequence);
} catch (e) {
@@ -275,13 +376,26 @@ class Display {
}
}
+ async setContentOrphaned() {
+ const definitions = document.querySelector('#definitions');
+ const errorOrphaned = document.querySelector('#error-orphaned');
+
+ if (definitions !== null) {
+ definitions.style.setProperty('display', 'none', 'important');
+ }
+
+ if (errorOrphaned !== null) {
+ errorOrphaned.style.setProperty('display', 'block', 'important');
+ }
+ }
+
autoPlayAudio() {
this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
async adderButtonUpdate(modes, sequence) {
try {
- const states = await apiDefinitionsAddable(this.definitions, modes, this.optionsContext);
+ const states = await apiDefinitionsAddable(this.definitions, modes, this.getOptionsContext());
if (!states || sequence !== this.sequence) {
return;
}
@@ -353,7 +467,7 @@ class Display {
source: this.context.source.source
};
- this.termsShow(this.context.source.definitions, this.options, context);
+ this.setContentTerms(this.context.source.definitions, context);
}
}
@@ -383,7 +497,7 @@ class Display {
}
}
- const noteId = await apiDefinitionAdd(definition, mode, context, this.optionsContext);
+ const noteId = await apiDefinitionAdd(definition, mode, context, this.getOptionsContext());
if (noteId) {
const index = this.definitions.indexOf(definition);
const adderButton = this.adderButtonFind(index, mode);
@@ -413,7 +527,7 @@ class Display {
}
const sources = this.options.audio.sources;
- let {audio, source} = await audioGetFromSources(expression, sources, this.optionsContext, true, this.audioCache);
+ let {audio, source} = await audioGetFromSources(expression, sources, this.getOptionsContext(), false, this.audioCache);
let info;
if (audio === null) {
if (this.audioFallback === null) {
@@ -544,18 +658,16 @@ class Display {
return -1;
}
- addEventListeners(selector, type, listener, options) {
- this.container.querySelectorAll(selector).forEach((node) => {
- node.addEventListener(type, listener, options);
- this.eventListeners.push([node, type, listener, options]);
- });
+ static addEventListener(eventListeners, object, type, listener, options) {
+ object.addEventListener(type, listener, options);
+ eventListeners.push([object, type, listener, options]);
}
- clearEventListeners() {
- for (const [node, type, listener, options] of this.eventListeners) {
- node.removeEventListener(type, listener, options);
+ static clearEventListeners(eventListeners) {
+ for (const [object, type, listener, options] of eventListeners) {
+ object.removeEventListener(type, listener, options);
}
- this.eventListeners = [];
+ eventListeners.length = 0;
}
static getElementTop(element) {
@@ -675,3 +787,7 @@ Display.onKeyDownHandlers = {
return false;
}
};
+
+Display.runtimeMessageHandlers = {
+ optionsUpdate: (self) => self.updateOptions(null)
+};