From 20d60a2ba79c065586805806ea703a8057839f75 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 10 Apr 2021 23:55:11 -0400 Subject: Initial safari compatibility (#1609) * Update environment info to return the 'safari' browser * Fix popup display on Safari * Update environment assignment * Add data-loading-stalled property when loading takes longer than expected * Add notification when loading has stalled * Allow getDictionaryInfo invocation on non-privileged contexts * Update _validatePrivilegedMessageSender * Don't listen to 'voiceschanged' event unless addEventListener is present Also expose an event --- ext/css/settings.css | 17 ++++++++++++++++- ext/js/background/backend.js | 14 +++++++++----- ext/js/background/environment.js | 14 ++++++++++++++ ext/js/media/audio-system.js | 18 ++++++++++++------ ext/js/pages/action-popup-main.js | 9 +++++++++ ext/js/pages/settings/audio-controller.js | 4 +--- ext/js/pages/settings/settings-main.js | 27 ++++++++++++++++++++++----- ext/settings.html | 19 ++++++++++++++++++- 8 files changed, 101 insertions(+), 21 deletions(-) diff --git a/ext/css/settings.css b/ext/css/settings.css index c0e75fc2..6bc377c2 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -134,7 +134,7 @@ ul+p, ul+ol, ul+ul, li { - margin: 0.425em 0; + margin: 0.425em 0 0; } a { color: var(--link-color); @@ -2152,6 +2152,13 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { margin-left: 0.375em; } +.page-loading-stalled-notification { + border: 1px solid var(--danger-color); +} +:root:not([data-loading-stalled=true]) .page-loading-stalled-notification { + display: none; +} + /* Generic layouts */ .margin-above { @@ -2233,19 +2240,25 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { /* Environment-specific display */ +:root[data-browser=unknown] [data-show-for-browser], +:root[data-browser=unknown] [data-hide-for-browser], :root[data-browser=edge] [data-show-for-browser]:not([data-show-for-browser~=edge]), :root[data-browser=edge-legacy] [data-show-for-browser]:not([data-show-for-browser~=edge-legacy]), :root[data-browser=chrome] [data-show-for-browser]:not([data-show-for-browser~=chrome]), +:root[data-browser=safari] [data-show-for-browser]:not([data-show-for-browser~=safari]), :root[data-browser=firefox] [data-show-for-browser]:not([data-show-for-browser~=firefox]), :root[data-browser=firefox-mobile] [data-show-for-browser]:not([data-show-for-browser~=firefox-mobile]), :root[data-browser=edge] [data-hide-for-browser~=edge], :root[data-browser=edge-legacy] [data-hide-for-browser~=edge-legacy], :root[data-browser=chrome] [data-hide-for-browser~=chrome], +:root[data-browser=safari] [data-hide-for-browser~=safari], :root[data-browser=firefox] [data-hide-for-browser~=firefox], :root[data-browser=firefox-mobile] [data-hide-for-browser~=firefox-mobile] { display: none; } +:root[data-os=unknown] [data-show-for-os], +:root[data-os=unknown] [data-hide-for-os], :root[data-os=mac] [data-show-for-os]:not([data-show-for-os~=mac]), :root[data-os=win] [data-show-for-os]:not([data-show-for-os~=win]), :root[data-os=android] [data-show-for-os]:not([data-show-for-os~=android]), @@ -2261,6 +2274,8 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] { display: none; } +:root[data-manifest-version=unknown] [data-show-for-manifest-version], +:root[data-manifest-version=unknown] [data-hide-for-manifest-version], :root[data-manifest-version='2'] [data-show-for-manifest-version]:not([data-show-for-manifest-version~='2']), :root[data-manifest-version='3'] [data-show-for-manifest-version]:not([data-show-for-manifest-version~='3']), :root[data-manifest-version='2'] [data-hide-for-manifest-version~='2'], diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index a6b9c0dc..21b18e99 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -112,7 +112,7 @@ class Backend { ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], ['getZoom', {async: true, contentScript: true, handler: this._onApiGetZoom.bind(this)}], ['getDefaultAnkiFieldTemplates', {async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this)}], - ['getDictionaryInfo', {async: true, contentScript: false, handler: this._onApiGetDictionaryInfo.bind(this)}], + ['getDictionaryInfo', {async: true, contentScript: true, handler: this._onApiGetDictionaryInfo.bind(this)}], ['getDictionaryCounts', {async: true, contentScript: false, handler: this._onApiGetDictionaryCounts.bind(this)}], ['purgeDatabase', {async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this)}], ['getMedia', {async: true, contentScript: true, handler: this._onApiGetMedia.bind(this)}], @@ -1288,10 +1288,14 @@ class Backend { } _validatePrivilegedMessageSender(sender) { - const url = sender.url; - if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) { - throw new Error('Invalid message sender'); - } + let {url} = sender; + if (typeof url === 'string' && yomichan.isExtensionUrl(url)) { return; } + const {tab} = url; + if (typeof tab === 'object' && tab !== null) { + ({url} = tab); + if (typeof url === 'string' && yomichan.isExtensionUrl(url)) { return; } + } + throw new Error('Invalid message sender'); } _getBrowserIconTitle() { diff --git a/ext/js/background/environment.js b/ext/js/background/environment.js index 04099ca1..8111741f 100644 --- a/ext/js/background/environment.js +++ b/ext/js/background/environment.js @@ -83,9 +83,23 @@ class Environment { } catch (e) { // NOP } + if (this._isSafari()) { + return 'safari'; + } return 'firefox'; } else { return 'chrome'; } } + + _isSafari() { + const {vendor, userAgent} = navigator; + return ( + typeof vendor === 'string' && + typeof userAgent === 'string' && + vendor.includes('Apple') && + !userAgent.includes('CriOS') && + !userAgent.includes('FxiOS') + ); + } } diff --git a/ext/js/media/audio-system.js b/ext/js/media/audio-system.js index cf63511f..cc2bcfc0 100644 --- a/ext/js/media/audio-system.js +++ b/ext/js/media/audio-system.js @@ -19,18 +19,20 @@ * TextToSpeechAudio */ -class AudioSystem { +class AudioSystem extends EventDispatcher { constructor() { + super(); this._fallbackAudio = null; } prepare() { // speechSynthesis.getVoices() will not be populated unless some API call is made. - if (typeof speechSynthesis === 'undefined') { return; } - - const eventListeners = new EventListenerCollection(); - const onVoicesChanged = () => { eventListeners.removeAllEventListeners(); }; - eventListeners.addEventListener(speechSynthesis, 'voiceschanged', onVoicesChanged, false); + if ( + typeof speechSynthesis !== 'undefined' && + typeof speechSynthesis.addEventListener === 'function' + ) { + speechSynthesis.addEventListener('voiceschanged', this._onVoicesChanged.bind(this), false); + } } getFallbackAudio() { @@ -64,6 +66,10 @@ class AudioSystem { // Private + _onVoicesChanged(e) { + this.trigger('voiceschanged', e); + } + _isAudioValid(audio, source) { switch (source) { case 'jpod101': diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js index 2de986da..4934802b 100644 --- a/ext/js/pages/action-popup-main.js +++ b/ext/js/pages/action-popup-main.js @@ -103,6 +103,10 @@ class DisplayController { let tab; try { tab = await this._getCurrentTab(); + // Safari assigns a tab object to the popup, other browsers do not + if (tab && await this._isSafari()) { + tab = void 0; + } } catch (e) { // NOP } @@ -220,6 +224,11 @@ class DisplayController { node.hidden = false; } } + + async _isSafari() { + const {browser} = await yomichan.api.getEnvironmentInfo(); + return browser === 'safari'; + } } (async () => { diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js index e62383a8..6b1ce0b5 100644 --- a/ext/js/pages/settings/audio-controller.js +++ b/ext/js/pages/settings/audio-controller.js @@ -39,9 +39,7 @@ class AudioController { this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); - if (typeof speechSynthesis !== 'undefined') { - speechSynthesis.addEventListener('voiceschanged', this._updateTextToSpeechVoices.bind(this), false); - } + this._audioSystem.on('voiceschanged', this._updateTextToSpeechVoices.bind(this), false); this._updateTextToSpeechVoices(); document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._onTestTextToSpeech.bind(this), false); diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index 2560685c..6f3e2a58 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -24,6 +24,7 @@ * DictionaryController * DictionaryImportController * DocumentFocusController + * Environment * ExtensionKeyboardShortcutController * GenericSettingController * KeyboardShortcutController @@ -47,11 +48,16 @@ */ async function setupEnvironmentInfo() { + const {dataset} = document.documentElement; const {manifest_version: manifestVersion} = chrome.runtime.getManifest(); - const {browser, platform} = await yomichan.api.getEnvironmentInfo(); - document.documentElement.dataset.browser = browser; - document.documentElement.dataset.os = platform.os; - document.documentElement.dataset.manifestVersion = `${manifestVersion}`; + dataset.manifestVersion = `${manifestVersion}`; + + const environment = new Environment(); + await environment.prepare(); + const {browser, platform} = environment.getInfo(); + + dataset.browser = browser; + dataset.os = platform.os; } async function setupGenericSettingsController(genericSettingController) { @@ -67,9 +73,20 @@ async function setupGenericSettingsController(genericSettingController) { const statusFooter = new StatusFooter(document.querySelector('.status-footer-container')); statusFooter.prepare(); + setupEnvironmentInfo(); + + let prepareTimer = setTimeout(() => { + prepareTimer = null; + document.documentElement.dataset.loadingStalled = 'true'; + }, 1000); + await yomichan.prepare(); - setupEnvironmentInfo(); + if (prepareTimer !== null) { + clearTimeout(prepareTimer); + prepareTimer = null; + } + delete document.documentElement.dataset.loadingStalled; const optionsFull = await yomichan.api.optionsGetFull(); diff --git a/ext/settings.html b/ext/settings.html index d629e6a6..732a96e2 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -1,5 +1,5 @@ - + @@ -59,6 +59,23 @@

Yomichan Settings

+ +
+
+
+
+

+ This page is taking longer than expected to load. +

+

+ Due to a bug in Safari, it may be necessary to click the Yomichan + button in the browser bar to fully load the page +

+
+
+
+
+
-- cgit v1.2.3