diff options
author | Alex Yatskov <alex@foosoft.net> | 2019-10-20 11:23:20 -0700 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2019-10-20 11:23:20 -0700 |
commit | 438498435227cfa59cf9ed3430045b288cd2a7c0 (patch) | |
tree | 6a05520e5d6fa8d26d372673a9ed3e5d2da7e3fd /ext/mixed | |
parent | 06d7713189be9eb51669d3842b78278371e6cfa4 (diff) | |
parent | d32fd1381b6cd5141a21c22f9ef639b2fe9774fb (diff) |
Merge branch 'master' into testing
Diffstat (limited to 'ext/mixed')
-rw-r--r-- | ext/mixed/css/display-dark.css | 50 | ||||
-rw-r--r-- | ext/mixed/css/display-default.css | 50 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 96 | ||||
-rw-r--r-- | ext/mixed/js/audio.js | 128 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 200 |
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) +}; |