2016-03-26 21:16:21 +00:00
/ *
2022-02-03 01:43:10 +00:00
* Copyright ( C ) 2016 - 2022 Yomichan Authors
2016-03-26 21:16:21 +00:00
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
2020-01-01 17:00:31 +00:00
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
2016-03-26 21:16:21 +00:00
* /
2020-03-11 02:30:36 +00:00
/ * g l o b a l
2021-11-24 03:23:14 +00:00
* AccessibilityController
2020-03-11 02:30:36 +00:00
* AnkiConnect
2021-02-25 03:23:40 +00:00
* AnkiUtil
2022-08-20 16:53:22 +00:00
* ArrayBufferUtil
2020-09-26 17:41:26 +00:00
* AudioDownloader
2020-03-11 02:30:36 +00:00
* ClipboardMonitor
2020-09-20 19:10:57 +00:00
* ClipboardReader
2020-06-28 21:24:06 +00:00
* DictionaryDatabase
2020-05-09 15:36:00 +00:00
* Environment
2020-11-29 18:09:02 +00:00
* JapaneseUtil
2020-03-11 02:30:36 +00:00
* Mecab
2021-02-14 16:32:30 +00:00
* MediaUtil
2020-05-06 23:32:28 +00:00
* ObjectPropertyAccessor
2020-08-01 15:46:35 +00:00
* OptionsUtil
2021-02-11 23:55:09 +00:00
* PermissionsUtil
2021-02-14 16:32:30 +00:00
* ProfileConditionsUtil
2020-08-02 22:58:19 +00:00
* RequestBuilder
2021-11-23 00:29:20 +00:00
* ScriptManager
2020-03-11 02:30:36 +00:00
* Translator
2020-11-29 18:09:02 +00:00
* wanakana
2020-03-11 02:30:36 +00:00
* /
2016-03-26 21:16:21 +00:00
2022-10-17 02:07:57 +00:00
/ * *
* This class controls the core logic of the extension , including API calls
* and various forms of communication between browser tabs and external applications .
* /
2017-08-14 04:11:10 +00:00
class Backend {
2022-10-17 02:07:57 +00:00
/ * *
* Creates a new instance .
* /
2016-03-26 21:16:21 +00:00
constructor ( ) {
2020-11-29 18:09:02 +00:00
this . _japaneseUtil = new JapaneseUtil ( wanakana ) ;
2020-06-28 18:59:01 +00:00
this . _environment = new Environment ( ) ;
2020-06-28 21:24:06 +00:00
this . _dictionaryDatabase = new DictionaryDatabase ( ) ;
2020-11-29 18:09:02 +00:00
this . _translator = new Translator ( {
japaneseUtil : this . _japaneseUtil ,
database : this . _dictionaryDatabase
} ) ;
2020-06-28 18:59:01 +00:00
this . _anki = new AnkiConnect ( ) ;
this . _mecab = new Mecab ( ) ;
2020-09-20 19:10:57 +00:00
this . _clipboardReader = new ClipboardReader ( {
2020-12-18 20:54:05 +00:00
// eslint-disable-next-line no-undef
2020-09-20 19:10:57 +00:00
document : ( typeof document === 'object' && document !== null ? document : null ) ,
pasteTargetSelector : '#clipboard-paste-target' ,
2022-10-16 02:43:12 +00:00
richContentPasteTargetSelector : '#clipboard-rich-content-paste-target'
2020-09-20 19:10:57 +00:00
} ) ;
this . _clipboardMonitor = new ClipboardMonitor ( {
2020-11-29 18:09:02 +00:00
japaneseUtil : this . _japaneseUtil ,
2020-09-20 19:10:57 +00:00
clipboardReader : this . _clipboardReader
} ) ;
2020-06-28 18:59:01 +00:00
this . _options = null ;
2020-09-04 21:44:00 +00:00
this . _profileConditionsSchemaCache = [ ] ;
2021-02-14 16:32:30 +00:00
this . _profileConditionsUtil = new ProfileConditionsUtil ( ) ;
2020-06-28 18:59:01 +00:00
this . _defaultAnkiFieldTemplates = null ;
2020-08-02 22:58:19 +00:00
this . _requestBuilder = new RequestBuilder ( ) ;
2020-09-26 17:41:26 +00:00
this . _audioDownloader = new AudioDownloader ( {
2020-11-29 18:09:02 +00:00
japaneseUtil : this . _japaneseUtil ,
2020-08-02 22:58:19 +00:00
requestBuilder : this . _requestBuilder
} ) ;
2020-09-11 18:15:08 +00:00
this . _optionsUtil = new OptionsUtil ( ) ;
2021-11-23 00:29:20 +00:00
this . _scriptManager = new ScriptManager ( ) ;
2021-11-24 03:23:14 +00:00
this . _accessibilityController = new AccessibilityController ( this . _scriptManager ) ;
2020-04-05 23:34:31 +00:00
2020-07-19 00:30:10 +00:00
this . _searchPopupTabId = null ;
this . _searchPopupTabCreatePromise = null ;
2019-10-27 13:46:27 +00:00
2020-04-12 00:45:23 +00:00
this . _isPrepared = false ;
2020-04-12 00:58:52 +00:00
this . _prepareError = false ;
2020-06-08 01:50:14 +00:00
this . _preparePromise = null ;
2020-06-28 18:39:43 +00:00
const { promise , resolve , reject } = deferPromise ( ) ;
this . _prepareCompletePromise = promise ;
this . _prepareCompleteResolve = resolve ;
this . _prepareCompleteReject = reject ;
2020-06-08 01:50:14 +00:00
this . _defaultBrowserActionTitle = null ;
2020-04-12 00:53:18 +00:00
this . _badgePrepareDelayTimer = null ;
2020-04-26 20:55:25 +00:00
this . _logErrorLevel = null ;
2021-02-11 23:55:09 +00:00
this . _permissions = null ;
this . _permissionsUtil = new PermissionsUtil ( ) ;
2020-04-12 00:45:23 +00:00
2020-02-27 00:22:32 +00:00
this . _messageHandlers = new Map ( [
2020-07-18 21:11:38 +00:00
[ 'requestBackendReadySignal' , { async : false , contentScript : true , handler : this . _onApiRequestBackendReadySignal . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'optionsGet' , { async : false , contentScript : true , handler : this . _onApiOptionsGet . bind ( this ) } ] ,
[ 'optionsGetFull' , { async : false , contentScript : true , handler : this . _onApiOptionsGetFull . bind ( this ) } ] ,
[ 'kanjiFind' , { async : true , contentScript : true , handler : this . _onApiKanjiFind . bind ( this ) } ] ,
[ 'termsFind' , { async : true , contentScript : true , handler : this . _onApiTermsFind . bind ( this ) } ] ,
2021-07-09 20:05:57 +00:00
[ 'parseText' , { async : true , contentScript : true , handler : this . _onApiParseText . bind ( this ) } ] ,
2020-12-12 19:57:24 +00:00
[ 'getAnkiConnectVersion' , { async : true , contentScript : true , handler : this . _onApGetAnkiConnectVersion . bind ( this ) } ] ,
2020-11-08 20:53:06 +00:00
[ 'isAnkiConnected' , { async : true , contentScript : true , handler : this . _onApiIsAnkiConnected . bind ( this ) } ] ,
2020-09-10 20:05:17 +00:00
[ 'addAnkiNote' , { async : true , contentScript : true , handler : this . _onApiAddAnkiNote . bind ( this ) } ] ,
[ 'getAnkiNoteInfo' , { async : true , contentScript : true , handler : this . _onApiGetAnkiNoteInfo . bind ( this ) } ] ,
2020-09-10 20:18:36 +00:00
[ 'injectAnkiNoteMedia' , { async : true , contentScript : true , handler : this . _onApiInjectAnkiNoteMedia . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'noteView' , { async : true , contentScript : true , handler : this . _onApiNoteView . bind ( this ) } ] ,
2021-01-15 03:42:11 +00:00
[ 'suspendAnkiCardsForNote' , { async : true , contentScript : true , handler : this . _onApiSuspendAnkiCardsForNote . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'commandExec' , { async : false , contentScript : true , handler : this . _onApiCommandExec . bind ( this ) } ] ,
2021-04-04 20:22:35 +00:00
[ 'getTermAudioInfoList' , { async : true , contentScript : true , handler : this . _onApiGetTermAudioInfoList . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'sendMessageToFrame' , { async : false , contentScript : true , handler : this . _onApiSendMessageToFrame . bind ( this ) } ] ,
[ 'broadcastTab' , { async : false , contentScript : true , handler : this . _onApiBroadcastTab . bind ( this ) } ] ,
[ 'frameInformationGet' , { async : true , contentScript : true , handler : this . _onApiFrameInformationGet . bind ( this ) } ] ,
[ 'injectStylesheet' , { async : true , contentScript : true , handler : this . _onApiInjectStylesheet . bind ( this ) } ] ,
2020-06-25 01:46:13 +00:00
[ 'getStylesheetContent' , { async : true , contentScript : true , handler : this . _onApiGetStylesheetContent . bind ( this ) } ] ,
2020-05-09 15:36:00 +00:00
[ 'getEnvironmentInfo' , { async : false , contentScript : true , handler : this . _onApiGetEnvironmentInfo . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'clipboardGet' , { async : true , contentScript : true , handler : this . _onApiClipboardGet . bind ( this ) } ] ,
[ '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 ) } ] ,
2021-04-11 03:55:11 +00:00
[ 'getDictionaryInfo' , { async : true , contentScript : true , handler : this . _onApiGetDictionaryInfo . bind ( this ) } ] ,
2020-05-07 23:37:25 +00:00
[ 'purgeDatabase' , { async : true , contentScript : false , handler : this . _onApiPurgeDatabase . bind ( this ) } ] ,
[ 'getMedia' , { async : true , contentScript : true , handler : this . _onApiGetMedia . bind ( this ) } ] ,
[ 'log' , { async : false , contentScript : true , handler : this . _onApiLog . bind ( this ) } ] ,
[ 'logIndicatorClear' , { async : false , contentScript : true , handler : this . _onApiLogIndicatorClear . bind ( this ) } ] ,
[ 'createActionPort' , { async : false , contentScript : true , handler : this . _onApiCreateActionPort . bind ( this ) } ] ,
2020-05-24 17:50:34 +00:00
[ 'modifySettings' , { async : true , contentScript : true , handler : this . _onApiModifySettings . bind ( this ) } ] ,
2020-05-30 20:23:56 +00:00
[ 'getSettings' , { async : false , contentScript : true , handler : this . _onApiGetSettings . bind ( this ) } ] ,
2020-07-19 00:30:10 +00:00
[ 'setAllSettings' , { async : true , contentScript : false , handler : this . _onApiSetAllSettings . bind ( this ) } ] ,
2020-09-04 21:57:51 +00:00
[ 'getOrCreateSearchPopup' , { async : true , contentScript : true , handler : this . _onApiGetOrCreateSearchPopup . bind ( this ) } ] ,
2020-09-10 01:07:18 +00:00
[ 'isTabSearchPopup' , { async : true , contentScript : true , handler : this . _onApiIsTabSearchPopup . bind ( this ) } ] ,
2021-02-09 00:40:49 +00:00
[ 'triggerDatabaseUpdated' , { async : false , contentScript : true , handler : this . _onApiTriggerDatabaseUpdated . bind ( this ) } ] ,
2021-02-28 19:18:18 +00:00
[ 'testMecab' , { async : true , contentScript : true , handler : this . _onApiTestMecab . bind ( this ) } ] ,
2021-08-07 16:40:51 +00:00
[ 'textHasJapaneseCharacters' , { async : false , contentScript : true , handler : this . _onApiTextHasJapaneseCharacters . bind ( this ) } ] ,
2022-05-21 19:28:44 +00:00
[ 'getTermFrequencies' , { async : true , contentScript : true , handler : this . _onApiGetTermFrequencies . bind ( this ) } ] ,
2022-09-24 20:41:21 +00:00
[ 'findAnkiNotes' , { async : true , contentScript : true , handler : this . _onApiFindAnkiNotes . bind ( this ) } ] ,
[ 'loadExtensionScripts' , { async : true , contentScript : true , handler : this . _onApiLoadExtensionScripts . bind ( this ) } ]
2020-02-27 00:22:32 +00:00
] ) ;
2020-05-06 23:28:26 +00:00
this . _messageHandlersWithProgress = new Map ( [
] ) ;
2020-02-27 00:22:32 +00:00
this . _commandHandlers = new Map ( [
2021-01-16 16:33:34 +00:00
[ 'toggleTextScanning' , this . _onCommandToggleTextScanning . bind ( this ) ] ,
2021-01-18 19:22:48 +00:00
[ 'openInfoPage' , this . _onCommandOpenInfoPage . bind ( this ) ] ,
2021-01-16 16:33:34 +00:00
[ 'openSettingsPage' , this . _onCommandOpenSettingsPage . bind ( this ) ] ,
[ 'openSearchPage' , this . _onCommandOpenSearchPage . bind ( this ) ] ,
[ 'openPopupWindow' , this . _onCommandOpenPopupWindow . bind ( this ) ]
2020-02-27 00:22:32 +00:00
] ) ;
2017-08-06 02:02:03 +00:00
}
2016-03-26 21:16:21 +00:00
2022-10-17 02:07:57 +00:00
/ * *
* Initializes the instance .
* @ returns { Promise < void > } A promise which is resolved when initialization completes .
* /
2020-06-08 01:50:14 +00:00
prepare ( ) {
if ( this . _preparePromise === null ) {
const promise = this . _prepareInternal ( ) ;
promise . then (
( value ) => {
this . _isPrepared = true ;
this . _prepareCompleteResolve ( value ) ;
} ,
( error ) => {
this . _prepareError = true ;
this . _prepareCompleteReject ( error ) ;
}
) ;
promise . finally ( ( ) => this . _updateBadge ( ) ) ;
this . _preparePromise = promise ;
}
return this . _prepareCompletePromise ;
}
2021-01-18 22:25:49 +00:00
// Private
2020-06-28 21:22:44 +00:00
_prepareInternalSync ( ) {
if ( isObject ( chrome . commands ) && isObject ( chrome . commands . onCommand ) ) {
const onCommand = this . _onWebExtensionEventWrapper ( this . _onCommand . bind ( this ) ) ;
chrome . commands . onCommand . addListener ( onCommand ) ;
}
if ( isObject ( chrome . tabs ) && isObject ( chrome . tabs . onZoomChange ) ) {
const onZoomChange = this . _onWebExtensionEventWrapper ( this . _onZoomChange . bind ( this ) ) ;
chrome . tabs . onZoomChange . addListener ( onZoomChange ) ;
}
const onConnect = this . _onWebExtensionEventWrapper ( this . _onConnect . bind ( this ) ) ;
chrome . runtime . onConnect . addListener ( onConnect ) ;
const onMessage = this . _onMessageWrapper . bind ( this ) ;
chrome . runtime . onMessage . addListener ( onMessage ) ;
2021-02-11 23:55:09 +00:00
2021-03-11 01:26:57 +00:00
if ( this . _canObservePermissionsChanges ( ) ) {
const onPermissionsChanged = this . _onWebExtensionEventWrapper ( this . _onPermissionsChanged . bind ( this ) ) ;
chrome . permissions . onAdded . addListener ( onPermissionsChanged ) ;
chrome . permissions . onRemoved . addListener ( onPermissionsChanged ) ;
}
2021-03-03 03:27:53 +00:00
chrome . runtime . onInstalled . addListener ( this . _onInstalled . bind ( this ) ) ;
2020-06-28 21:22:44 +00:00
}
2020-06-08 01:50:14 +00:00
async _prepareInternal ( ) {
2020-04-12 00:58:52 +00:00
try {
2020-06-28 21:22:44 +00:00
this . _prepareInternalSync ( ) ;
2021-02-11 23:55:09 +00:00
this . _permissions = await this . _permissionsUtil . getAllPermissions ( ) ;
2020-04-12 00:58:52 +00:00
this . _defaultBrowserActionTitle = await this . _getBrowserIconTitle ( ) ;
this . _badgePrepareDelayTimer = setTimeout ( ( ) => {
this . _badgePrepareDelayTimer = null ;
this . _updateBadge ( ) ;
} , 1000 ) ;
2020-04-12 00:53:18 +00:00
this . _updateBadge ( ) ;
2019-11-28 20:18:27 +00:00
2020-06-13 14:20:12 +00:00
yomichan . on ( 'log' , this . _onLog . bind ( this ) ) ;
2020-12-18 22:18:00 +00:00
await this . _requestBuilder . prepare ( ) ;
2020-06-28 18:59:01 +00:00
await this . _environment . prepare ( ) ;
2020-09-20 19:10:57 +00:00
this . _clipboardReader . browser = this . _environment . getInfo ( ) . browser ;
2020-06-21 20:12:56 +00:00
try {
2020-06-28 21:24:06 +00:00
await this . _dictionaryDatabase . prepare ( ) ;
2020-06-21 20:12:56 +00:00
} catch ( e ) {
2021-02-14 22:52:01 +00:00
log . error ( e ) ;
2020-06-21 20:12:56 +00:00
}
2020-10-04 17:09:04 +00:00
2021-02-13 01:37:43 +00:00
const deinflectionReasions = await this . _fetchAsset ( '/data/deinflect.json' , true ) ;
2020-10-04 17:09:04 +00:00
this . _translator . prepare ( deinflectionReasions ) ;
2020-04-12 00:58:52 +00:00
2020-09-11 18:15:08 +00:00
await this . _optionsUtil . prepare ( ) ;
2021-02-13 00:56:24 +00:00
this . _defaultAnkiFieldTemplates = ( await this . _fetchAsset ( '/data/templates/default-anki-field-templates.handlebars' ) ) . trim ( ) ;
2020-09-11 18:15:08 +00:00
this . _options = await this . _optionsUtil . load ( ) ;
2019-11-28 20:18:27 +00:00
2020-06-28 18:59:01 +00:00
this . _applyOptions ( 'background' ) ;
2017-08-06 02:02:03 +00:00
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( { current : true } ) ;
2020-04-12 00:58:52 +00:00
if ( options . general . showGuide ) {
2020-12-13 21:09:11 +00:00
this . _openWelcomeGuidePage ( ) ;
2020-04-12 00:58:52 +00:00
}
2019-09-07 19:06:15 +00:00
2020-06-28 18:59:01 +00:00
this . _clipboardMonitor . on ( 'change' , this . _onClipboardTextChange . bind ( this ) ) ;
2017-08-06 02:02:03 +00:00
2021-11-21 20:54:58 +00:00
this . _sendMessageAllTabsIgnoreResponse ( 'Yomichan.backendReady' , { } ) ;
this . _sendMessageIgnoreResponse ( { action : 'Yomichan.backendReady' , params : { } } ) ;
2020-04-12 00:58:52 +00:00
} catch ( e ) {
2021-02-14 22:52:01 +00:00
log . error ( e ) ;
2020-04-12 00:58:52 +00:00
throw e ;
} finally {
if ( this . _badgePrepareDelayTimer !== null ) {
clearTimeout ( this . _badgePrepareDelayTimer ) ;
this . _badgePrepareDelayTimer = null ;
}
}
2020-04-12 00:45:23 +00:00
}
2020-06-28 18:59:01 +00:00
// Event handlers
2020-07-19 00:30:10 +00:00
async _onClipboardTextChange ( { text } ) {
2021-01-26 23:30:01 +00:00
const { clipboard : { maximumSearchLength } } = this . _getProfileOptions ( { current : true } ) ;
if ( text . length > maximumSearchLength ) {
text = text . substring ( 0 , maximumSearchLength ) ;
2020-12-18 16:24:43 +00:00
}
2020-07-19 00:30:10 +00:00
try {
const { tab , created } = await this . _getOrCreateSearchPopup ( ) ;
await this . _focusTab ( tab ) ;
await this . _updateSearchQuery ( tab . id , text , ! created ) ;
} catch ( e ) {
// NOP
}
2020-06-28 18:59:01 +00:00
}
_onLog ( { level } ) {
const levelValue = this . _getErrorLevelValue ( level ) ;
if ( levelValue <= this . _getErrorLevelValue ( this . _logErrorLevel ) ) { return ; }
this . _logErrorLevel = level ;
this . _updateBadge ( ) ;
2020-03-02 02:51:45 +00:00
}
2020-06-28 21:22:44 +00:00
// WebExtension event handlers (with prepared checks)
_onWebExtensionEventWrapper ( handler ) {
return ( ... args ) => {
if ( this . _isPrepared ) {
handler ( ... args ) ;
return ;
}
this . _prepareCompletePromise . then (
( ) => { handler ( ... args ) ; } ,
( ) => { } // NOP
) ;
} ;
}
_onMessageWrapper ( message , sender , sendResponse ) {
if ( this . _isPrepared ) {
return this . _onMessage ( message , sender , sendResponse ) ;
}
this . _prepareCompletePromise . then (
( ) => { this . _onMessage ( message , sender , sendResponse ) ; } ,
( ) => { sendResponse ( ) ; }
) ;
return true ;
}
2020-06-28 18:59:01 +00:00
// WebExtension event handlers
_onCommand ( command ) {
this . _runCommand ( command ) ;
}
_onMessage ( { action , params } , sender , callback ) {
2020-04-07 23:41:02 +00:00
const messageHandler = this . _messageHandlers . get ( action ) ;
if ( typeof messageHandler === 'undefined' ) { return false ; }
2020-07-11 19:20:00 +00:00
if ( ! messageHandler . contentScript ) {
try {
2020-05-07 23:37:25 +00:00
this . _validatePrivilegedMessageSender ( sender ) ;
2020-07-11 19:20:00 +00:00
} catch ( error ) {
2021-01-08 02:36:20 +00:00
callback ( { error : serializeError ( error ) } ) ;
2020-04-07 23:41:02 +00:00
return false ;
}
2017-08-06 02:02:03 +00:00
}
2020-07-11 19:20:00 +00:00
2021-02-14 23:18:02 +00:00
return invokeMessageHandler ( messageHandler , params , callback , sender ) ;
2017-08-06 02:02:03 +00:00
}
2019-02-20 03:49:25 +00:00
2020-05-23 17:34:55 +00:00
_onConnect ( port ) {
try {
2020-07-18 18:16:35 +00:00
let details ;
try {
details = JSON . parse ( port . name ) ;
} catch ( e ) {
return ;
}
if ( details . name !== 'background-cross-frame-communication-port' ) { return ; }
2020-05-23 17:34:55 +00:00
2021-02-13 17:13:05 +00:00
const senderTabId = ( port . sender && port . sender . tab ? port . sender . tab . id : null ) ;
if ( typeof senderTabId !== 'number' ) {
2020-05-23 17:34:55 +00:00
throw new Error ( 'Port does not have an associated tab ID' ) ;
}
const senderFrameId = port . sender . frameId ;
2020-07-18 18:16:35 +00:00
if ( typeof senderFrameId !== 'number' ) {
2020-05-23 17:34:55 +00:00
throw new Error ( 'Port does not have an associated frame ID' ) ;
}
2020-07-18 18:16:35 +00:00
let { targetTabId , targetFrameId } = details ;
if ( typeof targetTabId !== 'number' ) {
2021-02-13 17:13:05 +00:00
targetTabId = senderTabId ;
2020-07-18 18:16:35 +00:00
}
2020-05-23 17:34:55 +00:00
2020-07-18 18:16:35 +00:00
const details2 = {
name : 'cross-frame-communication-port' ,
2021-02-13 17:13:05 +00:00
sourceTabId : senderTabId ,
2020-07-18 18:16:35 +00:00
sourceFrameId : senderFrameId
} ;
let forwardPort = chrome . tabs . connect ( targetTabId , { frameId : targetFrameId , name : JSON . stringify ( details2 ) } ) ;
2020-05-23 17:34:55 +00:00
const cleanup = ( ) => {
2020-06-28 18:59:01 +00:00
this . _checkLastError ( chrome . runtime . lastError ) ;
2020-05-23 17:34:55 +00:00
if ( forwardPort !== null ) {
forwardPort . disconnect ( ) ;
forwardPort = null ;
}
if ( port !== null ) {
port . disconnect ( ) ;
port = null ;
}
} ;
port . onMessage . addListener ( ( message ) => { forwardPort . postMessage ( message ) ; } ) ;
forwardPort . onMessage . addListener ( ( message ) => { port . postMessage ( message ) ; } ) ;
port . onDisconnect . addListener ( cleanup ) ;
forwardPort . onDisconnect . addListener ( cleanup ) ;
} catch ( e ) {
port . disconnect ( ) ;
2021-02-14 22:52:01 +00:00
log . error ( e ) ;
2020-05-23 17:34:55 +00:00
}
}
2019-12-23 16:59:47 +00:00
_onZoomChange ( { tabId , oldZoomFactor , newZoomFactor } ) {
2021-11-21 20:54:58 +00:00
this . _sendMessageTabIgnoreResponse ( tabId , { action : 'Yomichan.zoomChanged' , params : { oldZoomFactor , newZoomFactor } } ) ;
2019-12-23 16:59:47 +00:00
}
2021-02-11 23:55:09 +00:00
_onPermissionsChanged ( ) {
this . _checkPermissions ( ) ;
}
2021-03-03 03:27:53 +00:00
_onInstalled ( { reason } ) {
if ( reason !== 'install' ) { return ; }
this . _requestPersistentStorage ( ) ;
}
2019-12-09 01:53:42 +00:00
// Message handlers
2020-07-18 21:11:38 +00:00
_onApiRequestBackendReadySignal ( _params , sender ) {
2020-03-02 21:26:55 +00:00
// tab ID isn't set in background (e.g. browser_action)
2021-11-21 20:54:58 +00:00
const data = { action : 'Yomichan.backendReady' , params : { } } ;
2020-03-02 21:26:55 +00:00
if ( typeof sender . tab === 'undefined' ) {
2020-09-26 17:47:09 +00:00
this . _sendMessageIgnoreResponse ( data ) ;
2020-04-07 23:59:10 +00:00
return false ;
} else {
2020-09-26 17:47:09 +00:00
this . _sendMessageTabIgnoreResponse ( sender . tab . id , data ) ;
2020-04-07 23:59:10 +00:00
return true ;
2020-03-02 21:26:55 +00:00
}
2020-03-01 22:39:15 +00:00
}
2020-04-07 23:47:46 +00:00
_onApiOptionsGet ( { optionsContext } ) {
2021-01-18 22:25:49 +00:00
return this . _getProfileOptions ( optionsContext ) ;
2019-12-09 01:53:42 +00:00
}
2020-04-07 23:47:46 +00:00
_onApiOptionsGetFull ( ) {
2021-01-18 22:25:49 +00:00
return this . _getOptionsFull ( ) ;
2019-12-10 02:00:49 +00:00
}
2019-12-10 02:13:03 +00:00
async _onApiKanjiFind ( { text , optionsContext } ) {
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( optionsContext ) ;
2020-10-04 16:54:55 +00:00
const { general : { maxResults } } = options ;
const findKanjiOptions = this . _getTranslatorFindKanjiOptions ( options ) ;
2021-04-04 20:22:35 +00:00
const dictionaryEntries = await this . _translator . findKanji ( text , findKanjiOptions ) ;
dictionaryEntries . splice ( maxResults ) ;
return dictionaryEntries ;
2019-12-09 01:53:42 +00:00
}
2019-12-10 02:15:37 +00:00
async _onApiTermsFind ( { text , details , optionsContext } ) {
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( optionsContext ) ;
2020-10-04 16:54:55 +00:00
const { general : { resultOutputMode : mode , maxResults } } = options ;
2021-06-05 17:35:23 +00:00
const findTermsOptions = this . _getTranslatorFindTermsOptions ( mode , details , options ) ;
2021-03-25 23:55:31 +00:00
const { dictionaryEntries , originalTextLength } = await this . _translator . findTerms ( mode , text , findTermsOptions ) ;
dictionaryEntries . splice ( maxResults ) ;
2021-04-04 20:22:35 +00:00
return { dictionaryEntries , originalTextLength } ;
2019-12-09 01:53:42 +00:00
}
2021-07-09 20:05:57 +00:00
async _onApiParseText ( { text , optionsContext , scanLength , useInternalParser , useMecabParser } ) {
const [ internalResults , mecabResults ] = await Promise . all ( [
( useInternalParser ? this . _textParseScanning ( text , scanLength , optionsContext ) : null ) ,
( useMecabParser ? this . _textParseMecab ( text ) : null )
] ) ;
2019-12-10 02:18:23 +00:00
const results = [ ] ;
2020-04-12 00:53:24 +00:00
2021-07-09 20:05:57 +00:00
if ( internalResults !== null ) {
2020-04-12 00:53:24 +00:00
results . push ( {
id : 'scan' ,
2021-07-09 20:05:57 +00:00
source : 'scanning-parser' ,
dictionary : null ,
content : internalResults
2020-04-12 00:53:24 +00:00
} ) ;
2019-12-10 02:18:23 +00:00
}
2019-12-09 01:53:42 +00:00
2021-07-09 20:05:57 +00:00
if ( mecabResults !== null ) {
for ( const [ dictionary , content ] of mecabResults ) {
2020-04-12 00:53:24 +00:00
results . push ( {
2021-07-09 20:05:57 +00:00
id : ` mecab- ${ dictionary } ` ,
2020-04-12 00:53:24 +00:00
source : 'mecab' ,
2021-07-09 20:05:57 +00:00
dictionary ,
content
2020-04-12 00:53:24 +00:00
} ) ;
2019-12-10 02:21:17 +00:00
}
}
2020-04-12 00:53:24 +00:00
2019-12-10 02:21:17 +00:00
return results ;
2019-12-09 01:53:42 +00:00
}
2020-12-12 19:57:24 +00:00
async _onApGetAnkiConnectVersion ( ) {
return await this . _anki . getVersion ( ) ;
}
2020-11-08 20:53:06 +00:00
async _onApiIsAnkiConnected ( ) {
return await this . _anki . isConnected ( ) ;
}
2020-09-10 20:05:17 +00:00
async _onApiAddAnkiNote ( { note } ) {
return await this . _anki . addNote ( note ) ;
}
2021-04-30 21:57:53 +00:00
async _onApiGetAnkiNoteInfo ( { notes , fetchAdditionalInfo } ) {
2020-09-10 20:05:17 +00:00
const results = [ ] ;
const cannotAdd = [ ] ;
const canAddArray = await this . _anki . canAddNotes ( notes ) ;
for ( let i = 0 ; i < notes . length ; ++ i ) {
const note = notes [ i ] ;
2021-02-25 03:23:40 +00:00
let canAdd = canAddArray [ i ] ;
const valid = AnkiUtil . isNoteDataValid ( note ) ;
if ( ! valid ) { canAdd = false ; }
const info = { canAdd , valid , noteIds : null } ;
2020-09-10 20:05:17 +00:00
results . push ( info ) ;
2021-02-25 03:23:40 +00:00
if ( ! canAdd && valid ) {
2020-09-10 20:05:17 +00:00
cannotAdd . push ( { note , info } ) ;
}
}
if ( cannotAdd . length > 0 ) {
const cannotAddNotes = cannotAdd . map ( ( { note } ) => note ) ;
2021-01-11 23:37:07 +00:00
const noteIdsArray = await this . _anki . findNoteIds ( cannotAddNotes ) ;
2020-09-10 20:05:17 +00:00
for ( let i = 0 , ii = Math . min ( cannotAdd . length , noteIdsArray . length ) ; i < ii ; ++ i ) {
const noteIds = noteIdsArray [ i ] ;
if ( noteIds . length > 0 ) {
cannotAdd [ i ] . info . noteIds = noteIds ;
2021-04-30 21:57:53 +00:00
if ( fetchAdditionalInfo ) {
cannotAdd [ i ] . info . noteInfos = await this . _anki . notesInfo ( noteIds ) ;
}
2020-09-10 20:05:17 +00:00
}
}
}
return results ;
}
2021-07-07 02:00:18 +00:00
async _onApiInjectAnkiNoteMedia ( { timestamp , definitionDetails , audioDetails , screenshotDetails , clipboardDetails , dictionaryMediaDetails } ) {
2020-09-10 20:18:36 +00:00
return await this . _injectAnkNoteMedia (
this . _anki ,
timestamp ,
2020-11-27 03:53:58 +00:00
definitionDetails ,
2020-09-10 20:18:36 +00:00
audioDetails ,
screenshotDetails ,
2021-07-07 02:00:18 +00:00
clipboardDetails ,
dictionaryMediaDetails
2020-09-10 20:18:36 +00:00
) ;
}
2022-05-30 01:24:41 +00:00
async _onApiNoteView ( { noteId , mode , allowFallback } ) {
if ( mode === 'edit' ) {
try {
await this . _anki . guiEditNote ( noteId ) ;
return 'edit' ;
} catch ( e ) {
if ( ! this . _anki . isErrorUnsupportedAction ( e ) ) {
throw e ;
} else if ( ! allowFallback ) {
throw new Error ( 'Mode not supported' ) ;
}
}
}
// Fallback
await this . _anki . guiBrowseNote ( noteId ) ;
return 'browse' ;
2019-12-09 01:53:42 +00:00
}
2021-01-15 03:42:11 +00:00
async _onApiSuspendAnkiCardsForNote ( { noteId } ) {
const cardIds = await this . _anki . findCardsForNote ( noteId ) ;
const count = cardIds . length ;
if ( count > 0 ) {
const okay = await this . _anki . suspendCards ( cardIds ) ;
if ( ! okay ) { return 0 ; }
}
return count ;
}
2020-04-07 23:47:46 +00:00
_onApiCommandExec ( { command , params } ) {
2019-12-13 00:59:43 +00:00
return this . _runCommand ( command , params ) ;
2019-12-09 01:53:42 +00:00
}
2021-05-30 16:15:07 +00:00
async _onApiGetTermAudioInfoList ( { source , term , reading } ) {
return await this . _audioDownloader . getTermAudioInfoList ( source , term , reading ) ;
2020-09-26 17:41:26 +00:00
}
2020-07-08 23:58:06 +00:00
_onApiSendMessageToFrame ( { frameId : targetFrameId , action , params } , sender ) {
2020-05-06 23:27:21 +00:00
if ( ! ( sender && sender . tab ) ) {
return false ;
}
const tabId = sender . tab . id ;
2020-07-08 23:58:06 +00:00
const frameId = sender . frameId ;
2020-09-26 17:47:09 +00:00
this . _sendMessageTabIgnoreResponse ( tabId , { action , params , frameId } , { frameId : targetFrameId } ) ;
2020-05-06 23:27:21 +00:00
return true ;
}
2020-04-11 00:00:18 +00:00
_onApiBroadcastTab ( { action , params } , sender ) {
2019-12-10 02:54:45 +00:00
if ( ! ( sender && sender . tab ) ) {
2020-04-07 23:49:54 +00:00
return false ;
2019-12-10 02:54:45 +00:00
}
const tabId = sender . tab . id ;
2020-07-08 23:58:06 +00:00
const frameId = sender . frameId ;
2020-09-26 17:47:09 +00:00
this . _sendMessageTabIgnoreResponse ( tabId , { action , params , frameId } ) ;
2020-04-07 23:49:54 +00:00
return true ;
2019-12-09 01:53:42 +00:00
}
_onApiFrameInformationGet ( params , sender ) {
2021-02-10 03:56:04 +00:00
const tab = sender . tab ;
const tabId = tab ? tab . id : void 0 ;
2019-12-10 02:55:45 +00:00
const frameId = sender . frameId ;
2021-02-10 03:56:04 +00:00
return Promise . resolve ( { tabId , frameId } ) ;
2019-12-09 01:53:42 +00:00
}
2021-11-23 00:29:20 +00:00
async _onApiInjectStylesheet ( { type , value } , sender ) {
const { frameId , tab } = sender ;
if ( ! isObject ( tab ) ) { throw new Error ( 'Invalid tab' ) ; }
2021-11-23 21:16:13 +00:00
return await this . _scriptManager . injectStylesheet ( type , value , tab . id , frameId , false , true , 'document_start' ) ;
2019-12-09 01:53:42 +00:00
}
2020-06-25 01:46:13 +00:00
async _onApiGetStylesheetContent ( { url } ) {
if ( ! url . startsWith ( '/' ) || url . startsWith ( '//' ) || ! url . endsWith ( '.css' ) ) {
throw new Error ( 'Invalid URL' ) ;
}
2020-08-02 17:30:55 +00:00
return await this . _fetchAsset ( url ) ;
2020-06-25 01:46:13 +00:00
}
2020-05-09 15:36:00 +00:00
_onApiGetEnvironmentInfo ( ) {
2020-06-28 18:59:01 +00:00
return this . _environment . getInfo ( ) ;
2019-12-09 01:53:42 +00:00
}
2019-12-10 02:59:18 +00:00
async _onApiClipboardGet ( ) {
2022-10-16 02:43:12 +00:00
return this . _clipboardReader . getText ( false ) ;
2020-09-06 18:38:03 +00:00
}
2019-12-27 23:58:11 +00:00
async _onApiGetDisplayTemplatesHtml ( ) {
2021-02-13 04:03:15 +00:00
return await this . _fetchAsset ( '/display-templates.html' ) ;
2019-12-27 23:58:11 +00:00
}
2019-12-23 16:59:47 +00:00
_onApiGetZoom ( params , sender ) {
if ( ! sender || ! sender . tab ) {
return Promise . reject ( new Error ( 'Invalid tab' ) ) ;
}
return new Promise ( ( resolve , reject ) => {
const tabId = sender . tab . id ;
2020-01-11 20:34:12 +00:00
if ( ! (
chrome . tabs !== null &&
typeof chrome . tabs === 'object' &&
typeof chrome . tabs . getZoom === 'function'
) ) {
// Not supported
resolve ( { zoomFactor : 1.0 } ) ;
return ;
}
2019-12-23 16:59:47 +00:00
chrome . tabs . getZoom ( tabId , ( zoomFactor ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( { zoomFactor } ) ;
}
} ) ;
} ) ;
}
2020-04-07 23:47:46 +00:00
_onApiGetDefaultAnkiFieldTemplates ( ) {
2020-06-28 18:59:01 +00:00
return this . _defaultAnkiFieldTemplates ;
2020-02-28 01:33:13 +00:00
}
2020-05-07 23:37:25 +00:00
async _onApiGetDictionaryInfo ( ) {
2020-08-09 17:21:14 +00:00
return await this . _dictionaryDatabase . getDictionaryInfo ( ) ;
2020-04-11 19:21:43 +00:00
}
2020-05-07 23:37:25 +00:00
async _onApiPurgeDatabase ( ) {
2020-06-28 21:24:06 +00:00
await this . _dictionaryDatabase . purge ( ) ;
2020-09-13 22:43:44 +00:00
this . _triggerDatabaseUpdated ( 'dictionary' , 'purge' ) ;
2020-04-11 19:21:43 +00:00
}
2020-04-11 18:23:02 +00:00
async _onApiGetMedia ( { targets } ) {
2021-09-04 02:33:58 +00:00
return await this . _getNormalizedDictionaryDatabaseMedia ( targets ) ;
2020-04-11 18:23:02 +00:00
}
2020-04-26 20:55:25 +00:00
_onApiLog ( { error , level , context } ) {
2021-02-14 22:52:01 +00:00
log . log ( deserializeError ( error ) , level , context ) ;
2020-04-26 20:55:25 +00:00
}
_onApiLogIndicatorClear ( ) {
if ( this . _logErrorLevel === null ) { return ; }
this . _logErrorLevel = null ;
this . _updateBadge ( ) ;
}
2020-05-02 16:57:13 +00:00
_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 ;
2020-08-22 19:49:24 +00:00
const id = generateId ( 16 ) ;
2020-07-18 18:16:35 +00:00
const details = {
name : 'action-port' ,
id
} ;
2020-05-02 16:57:13 +00:00
2020-07-18 18:16:35 +00:00
const port = chrome . tabs . connect ( tabId , { name : JSON . stringify ( details ) , frameId } ) ;
2020-05-02 16:57:13 +00:00
try {
this . _createActionListenerPort ( port , sender , this . _messageHandlersWithProgress ) ;
} catch ( e ) {
port . disconnect ( ) ;
throw e ;
}
2020-07-18 18:16:35 +00:00
return details ;
2020-05-02 16:57:13 +00:00
}
2021-01-18 22:25:49 +00:00
_onApiModifySettings ( { targets , source } ) {
return this . _modifySettings ( targets , source ) ;
2020-05-06 23:32:28 +00:00
}
2020-05-24 17:50:34 +00:00
_onApiGetSettings ( { targets } ) {
const results = [ ] ;
for ( const target of targets ) {
try {
const result = this . _getSetting ( target ) ;
2020-06-28 16:38:34 +00:00
results . push ( { result : clone ( result ) } ) ;
2020-05-24 17:50:34 +00:00
} catch ( e ) {
2021-01-08 02:36:20 +00:00
results . push ( { error : serializeError ( e ) } ) ;
2020-05-24 17:50:34 +00:00
}
}
return results ;
}
2020-05-30 20:23:56 +00:00
async _onApiSetAllSettings ( { value , source } ) {
2020-09-11 18:15:08 +00:00
this . _optionsUtil . validate ( value ) ;
this . _options = clone ( value ) ;
2020-09-15 23:48:58 +00:00
await this . _saveOptions ( source ) ;
2020-05-30 20:23:56 +00:00
}
2020-07-19 00:30:10 +00:00
async _onApiGetOrCreateSearchPopup ( { focus = false , text = null } ) {
const { tab , created } = await this . _getOrCreateSearchPopup ( ) ;
if ( focus === true || ( focus === 'ifCreated' && created ) ) {
await this . _focusTab ( tab ) ;
}
if ( typeof text === 'string' ) {
await this . _updateSearchQuery ( tab . id , text , ! created ) ;
}
return { tabId : tab . id , windowId : tab . windowId } ;
}
2020-09-04 21:57:51 +00:00
async _onApiIsTabSearchPopup ( { tabId } ) {
2021-02-13 04:03:15 +00:00
const baseUrl = chrome . runtime . getURL ( '/search.html' ) ;
2020-09-04 21:57:51 +00:00
const tab = typeof tabId === 'number' ? await this . _checkTabUrl ( tabId , ( url ) => url . startsWith ( baseUrl ) ) : null ;
return ( tab !== null ) ;
}
2020-09-13 22:43:44 +00:00
_onApiTriggerDatabaseUpdated ( { type , cause } ) {
this . _triggerDatabaseUpdated ( type , cause ) ;
}
2021-02-09 00:40:49 +00:00
async _onApiTestMecab ( ) {
if ( ! this . _mecab . isEnabled ( ) ) {
throw new Error ( 'MeCab not enabled' ) ;
}
let permissionsOkay = false ;
try {
2021-02-11 23:55:09 +00:00
permissionsOkay = await this . _permissionsUtil . hasPermissions ( { permissions : [ 'nativeMessaging' ] } ) ;
2021-02-09 00:40:49 +00:00
} catch ( e ) {
// NOP
}
if ( ! permissionsOkay ) {
throw new Error ( 'Insufficient permissions' ) ;
}
const disconnect = ! this . _mecab . isConnected ( ) ;
try {
const version = await this . _mecab . getVersion ( ) ;
if ( version === null ) {
throw new Error ( 'Could not connect to native MeCab component' ) ;
}
const localVersion = this . _mecab . getLocalVersion ( ) ;
if ( version !== localVersion ) {
throw new Error ( ` MeCab component version not supported: ${ version } ` ) ;
}
} finally {
// Disconnect if the connection was previously disconnected
if ( disconnect && this . _mecab . isEnabled ( ) && this . _mecab . isActive ( ) ) {
this . _mecab . disconnect ( ) ;
}
}
return true ;
}
2021-02-28 19:18:18 +00:00
_onApiTextHasJapaneseCharacters ( { text } ) {
return this . _japaneseUtil . isStringPartiallyJapanese ( text ) ;
}
2021-09-26 15:08:16 +00:00
async _onApiGetTermFrequencies ( { termReadingList , dictionaries } ) {
return await this . _translator . getTermFrequencies ( termReadingList , dictionaries ) ;
}
2022-05-21 19:28:44 +00:00
async _onApiFindAnkiNotes ( { query } ) {
return await this . _anki . findNotes ( query ) ;
}
2022-09-24 20:41:21 +00:00
async _onApiLoadExtensionScripts ( { files } , 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 ;
for ( const file of files ) {
await this . _scriptManager . injectScript ( file , tabId , frameId , false , true , 'document_start' ) ;
}
}
2019-12-10 02:41:24 +00:00
// Command handlers
2021-01-16 16:33:34 +00:00
async _onCommandOpenSearchPage ( params ) {
2020-06-28 18:59:01 +00:00
const { mode = 'existingOrNewTab' , query } = params || { } ;
2021-02-13 04:03:15 +00:00
const baseUrl = chrome . runtime . getURL ( '/search.html' ) ;
2020-07-26 20:52:45 +00:00
const queryParams = { } ;
2020-06-28 18:59:01 +00:00
if ( query && query . length > 0 ) { queryParams . query = query ; }
const queryString = new URLSearchParams ( queryParams ) . toString ( ) ;
2020-07-26 20:52:45 +00:00
let url = baseUrl ;
if ( queryString . length > 0 ) {
url += ` ? ${ queryString } ` ;
}
2020-06-28 18:59:01 +00:00
2021-02-13 17:13:01 +00:00
const predicate = ( { url : url2 } ) => {
2020-06-28 18:59:01 +00:00
if ( url2 === null || ! url2 . startsWith ( baseUrl ) ) { return false ; }
2021-01-08 02:36:20 +00:00
const parsedUrl = new URL ( url2 ) ;
const baseUrl2 = ` ${ parsedUrl . origin } ${ parsedUrl . pathname } ` ;
const mode2 = parsedUrl . searchParams . get ( 'mode' ) ;
return baseUrl2 === baseUrl && ( mode2 === mode || ( ! mode2 && mode === 'existingOrNewTab' ) ) ;
2020-06-28 18:59:01 +00:00
} ;
const openInTab = async ( ) => {
2021-03-15 22:53:03 +00:00
const tabInfo = await this . _findTabs ( 1000 , false , predicate , false ) ;
if ( tabInfo !== null ) {
const { tab } = tabInfo ;
2020-06-28 18:59:01 +00:00
await this . _focusTab ( tab ) ;
if ( queryParams . query ) {
2020-07-19 00:30:10 +00:00
await this . _updateSearchQuery ( tab . id , queryParams . query , true ) ;
2020-06-28 18:59:01 +00:00
}
return true ;
}
} ;
switch ( mode ) {
case 'existingOrNewTab' :
try {
if ( await openInTab ( ) ) { return ; }
} catch ( e ) {
// NOP
}
2020-12-13 21:09:11 +00:00
await this . _createTab ( url ) ;
2020-06-28 18:59:01 +00:00
return ;
case 'newTab' :
2020-12-13 21:09:11 +00:00
await this . _createTab ( url ) ;
2020-06-28 18:59:01 +00:00
return ;
}
}
2021-01-18 19:22:48 +00:00
async _onCommandOpenInfoPage ( ) {
2020-12-13 21:09:11 +00:00
await this . _openInfoPage ( ) ;
2020-06-28 18:59:01 +00:00
}
2021-01-16 16:33:34 +00:00
async _onCommandOpenSettingsPage ( params ) {
2020-06-28 18:59:01 +00:00
const { mode = 'existingOrNewTab' } = params || { } ;
2020-12-13 21:09:11 +00:00
await this . _openSettingsPage ( mode ) ;
2020-06-28 18:59:01 +00:00
}
2021-01-16 16:33:34 +00:00
async _onCommandToggleTextScanning ( ) {
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( { current : true } ) ;
await this . _modifySettings ( [ {
action : 'set' ,
path : 'general.enable' ,
value : ! options . general . enable ,
scope : 'profile' ,
optionsContext : { current : true }
} ] , 'backend' ) ;
2020-06-28 18:59:01 +00:00
}
2021-01-16 16:33:34 +00:00
async _onCommandOpenPopupWindow ( ) {
await this . _onApiGetOrCreateSearchPopup ( { focus : true } ) ;
}
2020-06-28 18:59:01 +00:00
// Utilities
2021-01-18 22:25:49 +00:00
async _modifySettings ( targets , source ) {
const results = [ ] ;
for ( const target of targets ) {
try {
const result = this . _modifySetting ( target ) ;
results . push ( { result : clone ( result ) } ) ;
} catch ( e ) {
results . push ( { error : serializeError ( e ) } ) ;
}
}
await this . _saveOptions ( source ) ;
return results ;
}
2020-07-19 00:30:10 +00:00
_getOrCreateSearchPopup ( ) {
if ( this . _searchPopupTabCreatePromise === null ) {
const promise = this . _getOrCreateSearchPopup2 ( ) ;
this . _searchPopupTabCreatePromise = promise ;
promise . then ( ( ) => { this . _searchPopupTabCreatePromise = null ; } ) ;
}
return this . _searchPopupTabCreatePromise ;
}
async _getOrCreateSearchPopup2 ( ) {
2021-02-13 17:13:01 +00:00
// Use existing tab
2021-02-13 04:03:15 +00:00
const baseUrl = chrome . runtime . getURL ( '/search.html' ) ;
2021-02-13 17:13:01 +00:00
const urlPredicate = ( url ) => url !== null && url . startsWith ( baseUrl ) ;
2020-07-19 00:30:10 +00:00
if ( this . _searchPopupTabId !== null ) {
2021-02-13 17:13:01 +00:00
const tab = await this . _checkTabUrl ( this . _searchPopupTabId , urlPredicate ) ;
2020-07-19 00:30:10 +00:00
if ( tab !== null ) {
2020-09-04 21:57:51 +00:00
return { tab , created : false } ;
2020-07-19 00:30:10 +00:00
}
this . _searchPopupTabId = null ;
}
2021-02-13 17:13:01 +00:00
// Find existing tab
const existingTabInfo = await this . _findSearchPopupTab ( urlPredicate ) ;
if ( existingTabInfo !== null ) {
const existingTab = existingTabInfo . tab ;
this . _searchPopupTabId = existingTab . id ;
return { tab : existingTab , created : false } ;
}
2020-07-19 00:30:10 +00:00
// chrome.windows not supported (e.g. on Firefox mobile)
if ( ! isObject ( chrome . windows ) ) {
throw new Error ( 'Window creation not supported' ) ;
}
// Create a new window
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( { current : true } ) ;
2021-01-16 15:22:24 +00:00
const createData = this . _getSearchPopupWindowCreateData ( baseUrl , options ) ;
const { popupWindow : { windowState } } = options ;
const popupWindow = await this . _createWindow ( createData ) ;
if ( windowState !== 'normal' ) {
await this . _updateWindow ( popupWindow . id , { state : windowState } ) ;
}
2020-07-19 00:30:10 +00:00
const { tabs } = popupWindow ;
if ( tabs . length === 0 ) {
throw new Error ( 'Created window did not contain a tab' ) ;
}
const tab = tabs [ 0 ] ;
await this . _waitUntilTabFrameIsReady ( tab . id , 0 , 2000 ) ;
2020-09-26 17:47:09 +00:00
await this . _sendMessageTabPromise (
2020-08-09 17:11:41 +00:00
tab . id ,
2021-11-21 20:54:58 +00:00
{ action : 'SearchDisplayController.setMode' , params : { mode : 'popup' } } ,
2020-08-09 17:11:41 +00:00
{ frameId : 0 }
) ;
2020-07-19 00:30:10 +00:00
this . _searchPopupTabId = tab . id ;
return { tab , created : true } ;
}
2021-02-13 17:13:01 +00:00
async _findSearchPopupTab ( urlPredicate ) {
const predicate = async ( { url , tab } ) => {
if ( ! urlPredicate ( url ) ) { return false ; }
try {
const mode = await this . _sendMessageTabPromise (
tab . id ,
2021-11-21 20:54:58 +00:00
{ action : 'SearchDisplayController.getMode' , params : { } } ,
2021-02-13 17:13:01 +00:00
{ frameId : 0 }
) ;
return mode === 'popup' ;
} catch ( e ) {
return false ;
}
} ;
return await this . _findTabs ( 1000 , false , predicate , true ) ;
}
2021-01-16 15:22:24 +00:00
_getSearchPopupWindowCreateData ( url , options ) {
const { popupWindow : { width , height , left , top , useLeft , useTop , windowType } } = options ;
return {
url ,
width ,
height ,
left : useLeft ? left : void 0 ,
top : useTop ? top : void 0 ,
type : windowType ,
state : 'normal'
} ;
}
_createWindow ( createData ) {
return new Promise ( ( resolve , reject ) => {
chrome . windows . create (
createData ,
( result ) => {
const error = chrome . runtime . lastError ;
if ( error ) {
reject ( new Error ( error . message ) ) ;
} else {
resolve ( result ) ;
}
}
) ;
} ) ;
}
_updateWindow ( windowId , updateInfo ) {
return new Promise ( ( resolve , reject ) => {
chrome . windows . update (
windowId ,
updateInfo ,
( result ) => {
const error = chrome . runtime . lastError ;
if ( error ) {
reject ( new Error ( error . message ) ) ;
} else {
resolve ( result ) ;
}
}
) ;
} ) ;
}
2020-07-19 00:30:10 +00:00
_updateSearchQuery ( tabId , text , animate ) {
2020-09-26 17:47:09 +00:00
return this . _sendMessageTabPromise (
2020-08-09 17:11:41 +00:00
tabId ,
2021-11-21 20:54:58 +00:00
{ action : 'SearchDisplayController.updateSearchQuery' , params : { text , animate } } ,
2020-08-09 17:11:41 +00:00
{ frameId : 0 }
) ;
2020-07-11 19:20:00 +00:00
}
2020-06-28 18:59:01 +00:00
_applyOptions ( source ) {
2021-01-18 22:25:49 +00:00
const options = this . _getProfileOptions ( { current : true } ) ;
2020-06-28 18:59:01 +00:00
this . _updateBadge ( ) ;
2021-10-03 20:46:22 +00:00
const enabled = options . general . enable ;
2022-05-30 16:03:24 +00:00
let { apiKey } = options . anki ;
if ( apiKey === '' ) { apiKey = null ; }
2020-09-09 15:54:40 +00:00
this . _anki . server = options . anki . server ;
2021-10-03 20:46:22 +00:00
this . _anki . enabled = options . anki . enable && enabled ;
2022-05-30 16:03:24 +00:00
this . _anki . apiKey = apiKey ;
2020-06-28 18:59:01 +00:00
2021-10-03 20:46:22 +00:00
this . _mecab . setEnabled ( options . parsing . enableMecabParser && enabled ) ;
2020-06-28 18:59:01 +00:00
2021-10-03 20:46:22 +00:00
if ( options . clipboard . enableBackgroundMonitor && enabled ) {
2020-06-28 18:59:01 +00:00
this . _clipboardMonitor . start ( ) ;
} else {
this . _clipboardMonitor . stop ( ) ;
}
2021-11-24 03:23:14 +00:00
this . _accessibilityController . update ( this . _getOptionsFull ( false ) ) ;
2021-11-21 20:54:58 +00:00
this . _sendMessageAllTabsIgnoreResponse ( 'Yomichan.optionsUpdated' , { source } ) ;
2020-06-28 18:59:01 +00:00
}
2021-01-18 22:25:49 +00:00
_getOptionsFull ( useSchema = false ) {
const options = this . _options ;
return useSchema ? this . _optionsUtil . createValidatingProxy ( options ) : options ;
}
_getProfileOptions ( optionsContext , useSchema = false ) {
return this . _getProfile ( optionsContext , useSchema ) . options ;
}
2020-06-28 18:59:01 +00:00
_getProfile ( optionsContext , useSchema = false ) {
2021-01-18 22:25:49 +00:00
const options = this . _getOptionsFull ( useSchema ) ;
2020-06-28 18:59:01 +00:00
const profiles = options . profiles ;
2021-07-14 00:19:48 +00:00
if ( ! optionsContext . current ) {
// Specific index
const { index } = optionsContext ;
if ( typeof index === 'number' ) {
if ( index < 0 || index >= profiles . length ) {
throw this . _createDataError ( ` Invalid profile index: ${ index } ` , optionsContext ) ;
}
return profiles [ index ] ;
}
// From context
const profile = this . _getProfileFromContext ( options , optionsContext ) ;
if ( profile !== null ) {
return profile ;
}
2020-07-11 19:20:51 +00:00
}
2021-07-14 00:19:48 +00:00
// Default
const { profileCurrent } = options ;
if ( profileCurrent < 0 || profileCurrent >= profiles . length ) {
throw this . _createDataError ( ` Invalid current profile index: ${ profileCurrent } ` , optionsContext ) ;
2020-06-28 18:59:01 +00:00
}
2021-07-14 00:19:48 +00:00
return profiles [ profileCurrent ] ;
2020-06-28 18:59:01 +00:00
}
_getProfileFromContext ( options , optionsContext ) {
2020-09-04 21:44:00 +00:00
optionsContext = this . _profileConditionsUtil . normalizeContext ( optionsContext ) ;
let index = 0 ;
2020-06-28 18:59:01 +00:00
for ( const profile of options . profiles ) {
const conditionGroups = profile . conditionGroups ;
2020-09-04 21:44:00 +00:00
let schema ;
if ( index < this . _profileConditionsSchemaCache . length ) {
schema = this . _profileConditionsSchemaCache [ index ] ;
} else {
schema = this . _profileConditionsUtil . createSchema ( conditionGroups ) ;
this . _profileConditionsSchemaCache . push ( schema ) ;
}
2020-06-28 18:59:01 +00:00
2021-05-22 19:45:20 +00:00
if ( conditionGroups . length > 0 && schema . isValid ( optionsContext ) ) {
2020-09-04 21:44:00 +00:00
return profile ;
2020-06-28 18:59:01 +00:00
}
2020-09-04 21:44:00 +00:00
++ index ;
2020-06-28 18:59:01 +00:00
}
2020-09-04 21:44:00 +00:00
return null ;
2020-06-28 18:59:01 +00:00
}
2021-07-14 00:19:48 +00:00
_createDataError ( message , data ) {
const error = new Error ( message ) ;
error . data = data ;
return error ;
}
2020-09-04 21:44:00 +00:00
_clearProfileConditionsSchemaCache ( ) {
this . _profileConditionsSchemaCache = [ ] ;
2020-06-28 18:59:01 +00:00
}
_checkLastError ( ) {
// NOP
}
_runCommand ( command , params ) {
const handler = this . _commandHandlers . get ( command ) ;
if ( typeof handler !== 'function' ) { return false ; }
handler ( params ) ;
return true ;
}
2021-07-09 20:05:57 +00:00
async _textParseScanning ( text , scanLength , optionsContext ) {
2020-11-29 18:09:02 +00:00
const jp = this . _japaneseUtil ;
2021-06-05 17:35:23 +00:00
const mode = 'simple' ;
2021-07-09 20:05:57 +00:00
const options = this . _getProfileOptions ( optionsContext ) ;
2021-12-17 22:02:13 +00:00
const details = { matchType : 'exact' , deinflect : true } ;
2021-12-17 21:11:19 +00:00
const findTermsOptions = this . _getTranslatorFindTermsOptions ( mode , details , options ) ;
2020-06-28 18:59:01 +00:00
const results = [ ] ;
2021-02-28 21:38:01 +00:00
let previousUngroupedSegment = null ;
let i = 0 ;
const ii = text . length ;
while ( i < ii ) {
2021-03-25 23:55:31 +00:00
const { dictionaryEntries , originalTextLength } = await this . _translator . findTerms (
2021-06-05 17:35:23 +00:00
mode ,
2021-07-09 20:05:57 +00:00
text . substring ( i , i + scanLength ) ,
2020-10-04 16:54:55 +00:00
findTermsOptions
2020-06-28 18:59:01 +00:00
) ;
2021-02-28 21:38:01 +00:00
const codePoint = text . codePointAt ( i ) ;
const character = String . fromCodePoint ( codePoint ) ;
2021-02-28 20:44:57 +00:00
if (
2021-03-25 23:55:31 +00:00
dictionaryEntries . length > 0 &&
originalTextLength > 0 &&
2021-07-09 21:31:16 +00:00
( originalTextLength !== character . length || jp . isCodePointJapanese ( codePoint ) )
2021-02-28 20:44:57 +00:00
) {
2021-02-28 21:38:01 +00:00
previousUngroupedSegment = null ;
2021-03-25 23:55:31 +00:00
const { headwords : [ { term , reading } ] } = dictionaryEntries [ 0 ] ;
const source = text . substring ( i , i + originalTextLength ) ;
const textSegments = [ ] ;
2021-04-14 00:32:24 +00:00
for ( const { text : text2 , reading : reading2 } of jp . distributeFuriganaInflected ( term , reading , source ) ) {
2021-07-09 20:05:57 +00:00
textSegments . push ( { text : text2 , reading : reading2 } ) ;
2020-06-28 18:59:01 +00:00
}
2021-03-25 23:55:31 +00:00
results . push ( textSegments ) ;
i += originalTextLength ;
2020-06-28 18:59:01 +00:00
} else {
2021-02-28 21:38:01 +00:00
if ( previousUngroupedSegment === null ) {
previousUngroupedSegment = { text : character , reading : '' } ;
results . push ( [ previousUngroupedSegment ] ) ;
} else {
previousUngroupedSegment . text += character ;
}
i += character . length ;
2020-06-28 18:59:01 +00:00
}
}
return results ;
}
2021-07-09 20:05:57 +00:00
async _textParseMecab ( text ) {
2020-11-29 18:09:02 +00:00
const jp = this . _japaneseUtil ;
2021-02-08 22:53:12 +00:00
let parseTextResults ;
try {
parseTextResults = await this . _mecab . parseText ( text ) ;
} catch ( e ) {
return [ ] ;
}
2020-06-28 18:59:01 +00:00
const results = [ ] ;
2021-02-08 22:53:12 +00:00
for ( const { name , lines } of parseTextResults ) {
2020-06-28 18:59:01 +00:00
const result = [ ] ;
2021-02-08 22:53:12 +00:00
for ( const line of lines ) {
2021-04-04 20:22:35 +00:00
for ( const { term , reading , source } of line ) {
const termParts = [ ] ;
2021-04-14 00:32:24 +00:00
for ( const { text : text2 , reading : reading2 } of jp . distributeFuriganaInflected (
2021-04-04 20:22:35 +00:00
term . length > 0 ? term : source ,
2020-06-28 18:59:01 +00:00
jp . convertKatakanaToHiragana ( reading ) ,
source
) ) {
2021-07-09 20:05:57 +00:00
termParts . push ( { text : text2 , reading : reading2 } ) ;
2020-06-28 18:59:01 +00:00
}
2021-04-04 20:22:35 +00:00
result . push ( termParts ) ;
2020-06-28 18:59:01 +00:00
}
result . push ( [ { text : '\n' , reading : '' } ] ) ;
}
2021-02-08 22:53:12 +00:00
results . push ( [ name , result ] ) ;
2020-06-28 18:59:01 +00:00
}
return results ;
}
2020-05-02 16:57:13 +00:00
_createActionListenerPort ( port , sender , handlers ) {
let hasStarted = false ;
2020-05-31 22:17:12 +00:00
let messageString = '' ;
2020-05-02 16:57:13 +00:00
2020-05-06 23:28:26 +00:00
const onProgress = ( ... data ) => {
2020-05-02 16:57:13 +00:00
try {
if ( port === null ) { return ; }
port . postMessage ( { type : 'progress' , data } ) ;
} catch ( e ) {
// NOP
}
} ;
2020-05-31 22:17:12 +00:00
const onMessage = ( message ) => {
2020-05-02 16:57:13 +00:00
if ( hasStarted ) { return ; }
try {
2020-05-31 22:17:12 +00:00
const { action , data } = message ;
switch ( action ) {
case 'fragment' :
messageString += data ;
break ;
case 'invoke' :
{
hasStarted = true ;
port . onMessage . removeListener ( onMessage ) ;
const messageData = JSON . parse ( messageString ) ;
messageString = null ;
onMessageComplete ( messageData ) ;
}
break ;
}
} catch ( e ) {
cleanup ( e ) ;
}
} ;
const onMessageComplete = async ( message ) => {
try {
const { action , params } = message ;
2020-05-02 16:57:13 +00:00
port . postMessage ( { type : 'ack' } ) ;
const messageHandler = handlers . get ( action ) ;
if ( typeof messageHandler === 'undefined' ) {
throw new Error ( 'Invalid action' ) ;
}
2020-05-07 23:37:25 +00:00
const { handler , async , contentScript } = messageHandler ;
if ( ! contentScript ) {
this . _validatePrivilegedMessageSender ( sender ) ;
}
2020-05-02 16:57:13 +00:00
const promiseOrResult = handler ( params , sender , onProgress ) ;
const result = async ? await promiseOrResult : promiseOrResult ;
port . postMessage ( { type : 'complete' , data : result } ) ;
} catch ( e ) {
2020-05-31 22:17:12 +00:00
cleanup ( e ) ;
2020-05-02 16:57:13 +00:00
}
} ;
2020-05-31 22:17:12 +00:00
const onDisconnect = ( ) => {
cleanup ( null ) ;
} ;
const cleanup = ( error ) => {
2020-05-02 16:57:13 +00:00
if ( port === null ) { return ; }
2020-05-31 22:17:12 +00:00
if ( error !== null ) {
2021-01-08 02:36:20 +00:00
port . postMessage ( { type : 'error' , data : serializeError ( error ) } ) ;
2020-05-31 22:17:12 +00:00
}
2020-05-02 16:57:13 +00:00
if ( ! hasStarted ) {
port . onMessage . removeListener ( onMessage ) ;
}
2020-05-31 22:17:12 +00:00
port . onDisconnect . removeListener ( onDisconnect ) ;
2020-05-02 16:57:13 +00:00
port = null ;
handlers = null ;
} ;
port . onMessage . addListener ( onMessage ) ;
2020-05-31 22:17:12 +00:00
port . onDisconnect . addListener ( onDisconnect ) ;
2020-05-02 16:57:13 +00:00
}
2020-04-26 20:55:25 +00:00
_getErrorLevelValue ( errorLevel ) {
switch ( errorLevel ) {
case 'info' : return 0 ;
case 'debug' : return 0 ;
case 'warn' : return 1 ;
case 'error' : return 2 ;
default : return 0 ;
}
}
2020-05-06 23:32:28 +00:00
_getModifySettingObject ( target ) {
const scope = target . scope ;
switch ( scope ) {
case 'profile' :
if ( ! isObject ( target . optionsContext ) ) { throw new Error ( 'Invalid optionsContext' ) ; }
2021-01-18 22:25:49 +00:00
return this . _getProfileOptions ( target . optionsContext , true ) ;
2020-05-06 23:32:28 +00:00
case 'global' :
2021-01-18 22:25:49 +00:00
return this . _getOptionsFull ( true ) ;
2020-05-06 23:32:28 +00:00
default :
throw new Error ( ` Invalid scope: ${ scope } ` ) ;
}
}
2020-05-24 17:50:34 +00:00
_getSetting ( target ) {
const options = this . _getModifySettingObject ( target ) ;
const accessor = new ObjectPropertyAccessor ( options ) ;
const { path } = target ;
if ( typeof path !== 'string' ) { throw new Error ( 'Invalid path' ) ; }
return accessor . get ( ObjectPropertyAccessor . getPathArray ( path ) ) ;
}
_modifySetting ( target ) {
2020-05-06 23:32:28 +00:00
const options = this . _getModifySettingObject ( target ) ;
const accessor = new ObjectPropertyAccessor ( options ) ;
const action = target . action ;
switch ( action ) {
case 'set' :
2020-05-24 17:50:34 +00:00
{
const { path , value } = target ;
if ( typeof path !== 'string' ) { throw new Error ( 'Invalid path' ) ; }
const pathArray = ObjectPropertyAccessor . getPathArray ( path ) ;
accessor . set ( pathArray , value ) ;
return accessor . get ( pathArray ) ;
}
2020-05-06 23:32:28 +00:00
case 'delete' :
2020-05-24 17:50:34 +00:00
{
const { path } = target ;
if ( typeof path !== 'string' ) { throw new Error ( 'Invalid path' ) ; }
accessor . delete ( ObjectPropertyAccessor . getPathArray ( path ) ) ;
return true ;
}
2020-05-06 23:32:28 +00:00
case 'swap' :
2020-05-24 17:50:34 +00:00
{
const { path1 , path2 } = target ;
if ( typeof path1 !== 'string' ) { throw new Error ( 'Invalid path1' ) ; }
if ( typeof path2 !== 'string' ) { throw new Error ( 'Invalid path2' ) ; }
accessor . swap ( ObjectPropertyAccessor . getPathArray ( path1 ) , ObjectPropertyAccessor . getPathArray ( path2 ) ) ;
return true ;
}
2020-05-06 23:32:28 +00:00
case 'splice' :
2020-05-24 17:50:34 +00:00
{
const { path , start , deleteCount , items } = target ;
if ( typeof path !== 'string' ) { throw new Error ( 'Invalid path' ) ; }
if ( typeof start !== 'number' || Math . floor ( start ) !== start ) { throw new Error ( 'Invalid start' ) ; }
if ( typeof deleteCount !== 'number' || Math . floor ( deleteCount ) !== deleteCount ) { throw new Error ( 'Invalid deleteCount' ) ; }
if ( ! Array . isArray ( items ) ) { throw new Error ( 'Invalid items' ) ; }
const array = accessor . get ( ObjectPropertyAccessor . getPathArray ( path ) ) ;
if ( ! Array . isArray ( array ) ) { throw new Error ( 'Invalid target type' ) ; }
return array . splice ( start , deleteCount , ... items ) ;
}
2021-04-03 17:02:49 +00:00
case 'push' :
{
const { path , items } = target ;
if ( typeof path !== 'string' ) { throw new Error ( 'Invalid path' ) ; }
if ( ! Array . isArray ( items ) ) { throw new Error ( 'Invalid items' ) ; }
const array = accessor . get ( ObjectPropertyAccessor . getPathArray ( path ) ) ;
if ( ! Array . isArray ( array ) ) { throw new Error ( 'Invalid target type' ) ; }
const start = array . length ;
array . push ( ... items ) ;
return start ;
}
2020-05-06 23:32:28 +00:00
default :
throw new Error ( ` Unknown action: ${ action } ` ) ;
}
}
2020-04-11 19:21:43 +00:00
_validatePrivilegedMessageSender ( sender ) {
2021-04-11 03:55:11 +00:00
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 ; }
2020-04-11 19:21:43 +00:00
}
2021-04-11 03:55:11 +00:00
throw new Error ( 'Invalid message sender' ) ;
2020-04-11 19:21:43 +00:00
}
2020-04-12 00:46:11 +00:00
_getBrowserIconTitle ( ) {
return (
2020-04-17 23:19:38 +00:00
isObject ( chrome . browserAction ) &&
2020-04-12 00:46:11 +00:00
typeof chrome . browserAction . getTitle === 'function' ?
2020-04-17 23:19:38 +00:00
new Promise ( ( resolve ) => chrome . browserAction . getTitle ( { } , resolve ) ) :
Promise . resolve ( '' )
2020-04-12 00:46:11 +00:00
) ;
}
_updateBadge ( ) {
let title = this . _defaultBrowserActionTitle ;
2020-04-17 23:19:38 +00:00
if ( title === null || ! isObject ( chrome . browserAction ) ) {
2020-04-12 00:46:11 +00:00
// Not ready or invalid
return ;
}
let text = '' ;
let color = null ;
let status = null ;
2020-04-26 20:55:25 +00:00
if ( this . _logErrorLevel !== null ) {
switch ( this . _logErrorLevel ) {
case 'error' :
text = '!!' ;
color = '#f04e4e' ;
status = 'Error' ;
break ;
default : // 'warn'
text = '!' ;
color = '#f0ad4e' ;
status = 'Warning' ;
break ;
}
} else if ( ! this . _isPrepared ) {
2020-04-19 01:15:15 +00:00
if ( this . _prepareError ) {
2020-04-12 00:58:52 +00:00
text = '!!' ;
color = '#f04e4e' ;
status = 'Error' ;
} else if ( this . _badgePrepareDelayTimer === null ) {
2020-04-12 00:53:18 +00:00
text = '...' ;
color = '#f0ad4e' ;
status = 'Loading' ;
}
2021-01-25 03:32:29 +00:00
} else {
const options = this . _getProfileOptions ( { current : true } ) ;
if ( ! options . general . enable ) {
text = 'off' ;
color = '#555555' ;
status = 'Disabled' ;
2021-02-11 23:55:09 +00:00
} else if ( ! this . _hasRequiredPermissionsForSettings ( options ) ) {
text = '!' ;
color = '#f0ad4e' ;
status = 'Some settings require additional permissions' ;
2021-01-25 03:32:29 +00:00
} else if ( ! this . _isAnyDictionaryEnabled ( options ) ) {
text = '!' ;
color = '#f0ad4e' ;
status = 'No dictionaries installed' ;
}
2020-04-12 00:46:11 +00:00
}
if ( color !== null && typeof chrome . browserAction . setBadgeBackgroundColor === 'function' ) {
chrome . browserAction . setBadgeBackgroundColor ( { color } ) ;
}
if ( text !== null && typeof chrome . browserAction . setBadgeText === 'function' ) {
chrome . browserAction . setBadgeText ( { text } ) ;
}
if ( typeof chrome . browserAction . setTitle === 'function' ) {
if ( status !== null ) {
title = ` ${ title } - ${ status } ` ;
}
chrome . browserAction . setTitle ( { title } ) ;
}
}
_isAnyDictionaryEnabled ( options ) {
2021-04-03 17:02:49 +00:00
for ( const { enabled } of options . dictionaries ) {
2020-04-12 00:46:11 +00:00
if ( enabled ) {
return true ;
}
}
return false ;
}
_anyOptionsMatches ( predicate ) {
2020-06-28 18:59:01 +00:00
for ( const { options } of this . _options . profiles ) {
2020-04-12 00:46:11 +00:00
const value = predicate ( options ) ;
if ( value ) { return value ; }
}
return false ;
}
2020-08-09 17:11:41 +00:00
async _getTabUrl ( tabId ) {
try {
2020-09-26 17:47:09 +00:00
const { url } = await this . _sendMessageTabPromise (
2020-08-09 17:11:41 +00:00
tabId ,
2021-11-21 20:54:58 +00:00
{ action : 'Yomichan.getUrl' , params : { } } ,
2020-08-09 17:11:41 +00:00
{ frameId : 0 }
) ;
if ( typeof url === 'string' ) {
return url ;
}
} catch ( e ) {
// NOP
}
return null ;
2019-12-10 02:41:24 +00:00
}
2021-02-13 17:13:01 +00:00
_getAllTabs ( ) {
return new Promise ( ( resolve , reject ) => {
chrome . tabs . query ( { } , ( tabs ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( tabs ) ;
}
} ) ;
} ) ;
}
async _findTabs ( timeout , multiple , predicate , predicateIsAsync ) {
2019-12-10 02:41:24 +00:00
// This function works around the need to have the "tabs" permission to access tab.url.
2021-02-13 17:13:01 +00:00
const tabs = await this . _getAllTabs ( ) ;
let done = false ;
const checkTab = async ( tab , add ) => {
const url = await this . _getTabUrl ( tab . id ) ;
if ( done ) { return ; }
2019-12-10 02:41:24 +00:00
2021-02-13 17:13:01 +00:00
let okay = false ;
const item = { tab , url } ;
try {
okay = predicate ( item ) ;
if ( predicateIsAsync ) { okay = await okay ; }
} catch ( e ) {
// NOP
2019-12-10 02:41:24 +00:00
}
2021-02-13 17:13:01 +00:00
if ( okay && ! done ) {
if ( add ( item ) ) {
done = true ;
}
}
} ;
2019-12-10 02:41:24 +00:00
2021-02-13 17:13:01 +00:00
if ( multiple ) {
const results = [ ] ;
const add = ( value ) => {
results . push ( value ) ;
return false ;
} ;
const checkTabPromises = tabs . map ( ( tab ) => checkTab ( tab , add ) ) ;
await Promise . race ( [
Promise . all ( checkTabPromises ) ,
promiseTimeout ( timeout )
] ) ;
return results ;
} else {
const { promise , resolve } = deferPromise ( ) ;
let result = null ;
const add = ( value ) => {
result = value ;
resolve ( ) ;
return true ;
} ;
const checkTabPromises = tabs . map ( ( tab ) => checkTab ( tab , add ) ) ;
await Promise . race ( [
promise ,
Promise . all ( checkTabPromises ) ,
promiseTimeout ( timeout )
] ) ;
resolve ( ) ;
return result ;
2019-12-10 02:41:24 +00:00
}
}
2020-06-28 18:59:01 +00:00
async _focusTab ( tab ) {
2019-12-10 02:41:24 +00:00
await new Promise ( ( resolve , reject ) => {
chrome . tabs . update ( tab . id , { active : true } , ( ) => {
const e = chrome . runtime . lastError ;
2020-02-16 00:48:02 +00:00
if ( e ) {
2020-02-23 16:59:57 +00:00
reject ( new Error ( e . message ) ) ;
2020-02-16 00:48:02 +00:00
} else {
resolve ( ) ;
}
2019-12-10 02:41:24 +00:00
} ) ;
} ) ;
if ( ! ( typeof chrome . windows === 'object' && chrome . windows !== null ) ) {
// Windows not supported (e.g. on Firefox mobile)
return ;
}
try {
2020-02-01 19:39:26 +00:00
const tabWindow = await new Promise ( ( resolve , reject ) => {
2020-02-17 20:21:30 +00:00
chrome . windows . get ( tab . windowId , { } , ( value ) => {
2019-12-10 02:41:24 +00:00
const e = chrome . runtime . lastError ;
2020-02-16 00:48:02 +00:00
if ( e ) {
2020-02-23 16:59:57 +00:00
reject ( new Error ( e . message ) ) ;
2020-02-16 00:48:02 +00:00
} else {
2020-02-17 20:21:30 +00:00
resolve ( value ) ;
2020-02-16 00:48:02 +00:00
}
2019-12-10 02:41:24 +00:00
} ) ;
} ) ;
if ( ! tabWindow . focused ) {
await new Promise ( ( resolve , reject ) => {
chrome . windows . update ( tab . windowId , { focused : true } , ( ) => {
const e = chrome . runtime . lastError ;
2020-02-16 00:48:02 +00:00
if ( e ) {
2020-02-23 16:59:57 +00:00
reject ( new Error ( e . message ) ) ;
2020-02-16 00:48:02 +00:00
} else {
resolve ( ) ;
}
2019-12-10 02:41:24 +00:00
} ) ;
} ) ;
}
} catch ( e ) {
// Edge throws exception for no reason here.
}
}
2020-07-18 18:18:10 +00:00
_waitUntilTabFrameIsReady ( tabId , frameId , timeout = null ) {
return new Promise ( ( resolve , reject ) => {
let timer = null ;
let onMessage = ( message , sender ) => {
if (
! sender . tab ||
sender . tab . id !== tabId ||
sender . frameId !== frameId ||
! isObject ( message ) ||
2020-07-18 21:11:38 +00:00
message . action !== 'yomichanReady'
2020-07-18 18:18:10 +00:00
) {
return ;
}
cleanup ( ) ;
resolve ( ) ;
} ;
const cleanup = ( ) => {
if ( timer !== null ) {
clearTimeout ( timer ) ;
timer = null ;
}
if ( onMessage !== null ) {
chrome . runtime . onMessage . removeListener ( onMessage ) ;
onMessage = null ;
}
} ;
chrome . runtime . onMessage . addListener ( onMessage ) ;
2021-11-21 20:54:58 +00:00
this . _sendMessageTabPromise ( tabId , { action : 'Yomichan.isReady' } , { frameId } )
2020-09-26 17:47:09 +00:00
. then (
( value ) => {
if ( ! value ) { return ; }
cleanup ( ) ;
resolve ( ) ;
} ,
( ) => { } // NOP
) ;
2020-07-18 18:18:10 +00:00
if ( timeout !== null ) {
timer = setTimeout ( ( ) => {
timer = null ;
cleanup ( ) ;
reject ( new Error ( 'Timeout' ) ) ;
} , timeout ) ;
}
} ) ;
}
2020-08-02 17:30:55 +00:00
async _fetchAsset ( url , json = false ) {
const response = await fetch ( chrome . runtime . getURL ( url ) , {
method : 'GET' ,
mode : 'no-cors' ,
cache : 'default' ,
credentials : 'omit' ,
redirect : 'follow' ,
referrerPolicy : 'no-referrer'
} ) ;
if ( ! response . ok ) {
throw new Error ( ` Failed to fetch ${ url } : ${ response . status } ` ) ;
}
return await ( json ? response . json ( ) : response . text ( ) ) ;
}
2020-08-09 17:11:41 +00:00
2020-09-26 17:47:09 +00:00
_sendMessageIgnoreResponse ( ... args ) {
const callback = ( ) => this . _checkLastError ( chrome . runtime . lastError ) ;
chrome . runtime . sendMessage ( ... args , callback ) ;
}
_sendMessageTabIgnoreResponse ( ... args ) {
const callback = ( ) => this . _checkLastError ( chrome . runtime . lastError ) ;
chrome . tabs . sendMessage ( ... args , callback ) ;
}
_sendMessageAllTabsIgnoreResponse ( action , params ) {
const callback = ( ) => this . _checkLastError ( chrome . runtime . lastError ) ;
chrome . tabs . query ( { } , ( tabs ) => {
for ( const tab of tabs ) {
chrome . tabs . sendMessage ( tab . id , { action , params } , callback ) ;
}
} ) ;
}
_sendMessageTabPromise ( ... args ) {
2020-08-09 17:11:41 +00:00
return new Promise ( ( resolve , reject ) => {
const callback = ( response ) => {
try {
2021-02-14 23:18:02 +00:00
resolve ( this . _getMessageResponseResult ( response ) ) ;
2020-08-09 17:11:41 +00:00
} catch ( error ) {
reject ( error ) ;
}
} ;
chrome . tabs . sendMessage ( ... args , callback ) ;
} ) ;
}
2020-09-04 21:57:51 +00:00
2021-02-14 23:18:02 +00:00
_getMessageResponseResult ( response ) {
let error = chrome . runtime . lastError ;
if ( error ) {
throw new Error ( error . message ) ;
}
if ( ! isObject ( response ) ) {
throw new Error ( 'Tab did not respond' ) ;
}
error = response . error ;
if ( error ) {
throw deserializeError ( error ) ;
}
return response . result ;
}
2020-09-04 21:57:51 +00:00
async _checkTabUrl ( tabId , urlPredicate ) {
2021-02-10 04:14:29 +00:00
let tab ;
try {
tab = await this . _getTabById ( tabId ) ;
} catch ( e ) {
return null ;
}
2020-09-04 21:57:51 +00:00
const url = await this . _getTabUrl ( tabId ) ;
const isValidTab = urlPredicate ( url ) ;
return isValidTab ? tab : null ;
}
2020-09-06 18:38:03 +00:00
2021-02-10 04:14:29 +00:00
async _getScreenshot ( tabId , frameId , format , quality ) {
const tab = await this . _getTabById ( tabId ) ;
const { windowId } = tab ;
2020-09-09 16:54:59 +00:00
let token = null ;
try {
2021-02-10 03:56:04 +00:00
if ( typeof tabId === 'number' && typeof frameId === 'number' ) {
2021-11-21 20:54:58 +00:00
const action = 'Frontend.setAllVisibleOverride' ;
2020-09-09 16:54:59 +00:00
const params = { value : false , priority : 0 , awaitFrame : true } ;
2021-02-10 03:56:04 +00:00
token = await this . _sendMessageTabPromise ( tabId , { action , params } , { frameId } ) ;
2020-09-09 16:54:59 +00:00
}
return await new Promise ( ( resolve , reject ) => {
chrome . tabs . captureVisibleTab ( windowId , { format , quality } , ( result ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( result ) ;
}
} ) ;
} ) ;
} finally {
if ( token !== null ) {
2021-11-21 20:54:58 +00:00
const action = 'Frontend.clearAllVisibleOverride' ;
2020-09-09 16:54:59 +00:00
const params = { token } ;
try {
2021-02-10 03:56:04 +00:00
await this . _sendMessageTabPromise ( tabId , { action , params } , { frameId } ) ;
2020-09-09 16:54:59 +00:00
} catch ( e ) {
// NOP
}
}
}
}
2020-09-10 01:07:18 +00:00
2021-07-07 02:00:18 +00:00
async _injectAnkNoteMedia ( ankiConnect , timestamp , definitionDetails , audioDetails , screenshotDetails , clipboardDetails , dictionaryMediaDetails ) {
2020-11-27 03:53:58 +00:00
let screenshotFileName = null ;
let clipboardImageFileName = null ;
2020-09-26 17:45:48 +00:00
let clipboardText = null ;
2020-11-27 03:53:58 +00:00
let audioFileName = null ;
2021-01-30 17:33:29 +00:00
const errors = [ ] ;
2020-11-27 03:53:58 +00:00
2020-09-26 17:45:48 +00:00
try {
2020-11-27 03:53:58 +00:00
if ( screenshotDetails !== null ) {
2021-07-07 00:07:13 +00:00
screenshotFileName = await this . _injectAnkiNoteScreenshot ( ankiConnect , timestamp , definitionDetails , screenshotDetails ) ;
2020-09-26 17:45:48 +00:00
}
} catch ( e ) {
2021-01-30 17:33:29 +00:00
errors . push ( serializeError ( e ) ) ;
2020-09-26 17:45:48 +00:00
}
2020-09-10 19:04:54 +00:00
try {
2020-11-27 03:53:58 +00:00
if ( clipboardDetails !== null && clipboardDetails . image ) {
2021-07-07 00:07:13 +00:00
clipboardImageFileName = await this . _injectAnkiNoteClipboardImage ( ankiConnect , timestamp , definitionDetails ) ;
2020-09-10 19:04:54 +00:00
}
2020-11-27 03:53:58 +00:00
} catch ( e ) {
2021-01-30 17:33:29 +00:00
errors . push ( serializeError ( e ) ) ;
2020-11-27 03:53:58 +00:00
}
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
try {
if ( clipboardDetails !== null && clipboardDetails . text ) {
2022-10-16 02:43:12 +00:00
clipboardText = await this . _clipboardReader . getText ( false ) ;
2020-11-27 03:53:58 +00:00
}
} catch ( e ) {
2021-01-30 17:33:29 +00:00
errors . push ( serializeError ( e ) ) ;
2020-11-27 03:53:58 +00:00
}
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
try {
if ( audioDetails !== null ) {
2021-07-07 00:07:13 +00:00
audioFileName = await this . _injectAnkiNoteAudio ( ankiConnect , timestamp , definitionDetails , audioDetails ) ;
2020-11-27 03:53:58 +00:00
}
2020-09-10 19:04:54 +00:00
} catch ( e ) {
2021-01-30 17:33:29 +00:00
errors . push ( serializeError ( e ) ) ;
2020-09-10 19:04:54 +00:00
}
2020-11-27 03:53:58 +00:00
2021-07-07 02:00:18 +00:00
let dictionaryMedia ;
try {
let errors2 ;
( { results : dictionaryMedia , errors : errors2 } = await this . _injectAnkiNoteDictionaryMedia ( ankiConnect , timestamp , definitionDetails , dictionaryMediaDetails ) ) ;
for ( const error of errors2 ) {
errors . push ( serializeError ( error ) ) ;
}
} catch ( e ) {
dictionaryMedia = [ ] ;
errors . push ( serializeError ( e ) ) ;
}
2021-01-30 17:33:29 +00:00
return {
2021-07-07 02:00:18 +00:00
screenshotFileName ,
clipboardImageFileName ,
clipboardText ,
audioFileName ,
dictionaryMedia ,
errors : errors
2021-01-30 17:33:29 +00:00
} ;
2020-09-10 19:04:54 +00:00
}
2021-07-07 00:07:13 +00:00
async _injectAnkiNoteAudio ( ankiConnect , timestamp , definitionDetails , details ) {
2021-03-26 23:57:57 +00:00
const { type , term , reading } = definitionDetails ;
2021-01-30 17:33:29 +00:00
if (
type === 'kanji' ||
2021-03-26 23:57:57 +00:00
typeof term !== 'string' ||
2021-01-30 17:33:29 +00:00
typeof reading !== 'string' ||
2021-03-26 23:57:57 +00:00
( term . length === 0 && reading . length === 0 )
2021-01-30 17:33:29 +00:00
) {
return null ;
2020-11-27 03:53:58 +00:00
}
2022-08-20 15:17:24 +00:00
const { sources , preferredAudioIndex , idleTimeout } = details ;
2021-01-30 17:33:29 +00:00
let data ;
2021-02-11 00:18:28 +00:00
let contentType ;
2021-01-30 17:33:29 +00:00
try {
2021-04-04 20:22:35 +00:00
( { data , contentType } = await this . _audioDownloader . downloadTermAudio (
2021-01-30 17:33:29 +00:00
sources ,
2021-02-16 01:47:35 +00:00
preferredAudioIndex ,
2021-03-26 23:57:57 +00:00
term ,
2022-08-20 15:17:24 +00:00
reading ,
idleTimeout
2021-02-11 00:18:28 +00:00
) ) ;
2021-01-30 17:33:29 +00:00
} catch ( e ) {
2022-05-29 01:55:37 +00:00
const error = this . _getAudioDownloadError ( e ) ;
if ( error !== null ) { throw error ; }
2021-01-30 17:33:29 +00:00
// No audio
return null ;
}
2020-09-10 19:04:54 +00:00
2021-03-14 22:04:19 +00:00
let extension = MediaUtil . getFileExtensionFromAudioMediaType ( contentType ) ;
2021-02-11 00:18:28 +00:00
if ( extension === null ) { extension = '.mp3' ; }
let fileName = this . _generateAnkiNoteMediaFileName ( 'yomichan_audio' , extension , timestamp , definitionDetails ) ;
2020-11-27 03:53:58 +00:00
fileName = fileName . replace ( /\]/g , '' ) ;
2022-03-14 01:17:41 +00:00
fileName = await ankiConnect . storeMediaFile ( fileName , data ) ;
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
return fileName ;
}
2020-09-10 19:04:54 +00:00
2021-07-07 00:07:13 +00:00
async _injectAnkiNoteScreenshot ( ankiConnect , timestamp , definitionDetails , details ) {
2021-02-10 04:14:29 +00:00
const { tabId , frameId , format , quality } = details ;
const dataUrl = await this . _getScreenshot ( tabId , frameId , format , quality ) ;
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
const { mediaType , data } = this . _getDataUrlInfo ( dataUrl ) ;
2021-03-14 22:04:19 +00:00
const extension = MediaUtil . getFileExtensionFromImageMediaType ( mediaType ) ;
2021-01-30 17:33:29 +00:00
if ( extension === null ) {
throw new Error ( 'Unknown media type for screenshot image' ) ;
}
2020-09-10 19:04:54 +00:00
2022-03-14 01:17:41 +00:00
let fileName = this . _generateAnkiNoteMediaFileName ( 'yomichan_browser_screenshot' , extension , timestamp , definitionDetails ) ;
fileName = await ankiConnect . storeMediaFile ( fileName , data ) ;
2020-11-27 03:53:58 +00:00
return fileName ;
2020-09-10 19:04:54 +00:00
}
2021-07-07 00:07:13 +00:00
async _injectAnkiNoteClipboardImage ( ankiConnect , timestamp , definitionDetails ) {
2020-11-27 03:53:58 +00:00
const dataUrl = await this . _clipboardReader . getImage ( ) ;
if ( dataUrl === null ) {
2021-01-30 17:33:29 +00:00
return null ;
2020-11-27 03:53:58 +00:00
}
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
const { mediaType , data } = this . _getDataUrlInfo ( dataUrl ) ;
2021-03-14 22:04:19 +00:00
const extension = MediaUtil . getFileExtensionFromImageMediaType ( mediaType ) ;
2021-01-30 17:33:29 +00:00
if ( extension === null ) {
throw new Error ( 'Unknown media type for clipboard image' ) ;
}
2020-09-10 19:04:54 +00:00
2022-03-14 01:17:41 +00:00
let fileName = this . _generateAnkiNoteMediaFileName ( 'yomichan_clipboard_image' , extension , timestamp , definitionDetails ) ;
fileName = await ankiConnect . storeMediaFile ( fileName , data ) ;
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
return fileName ;
}
2020-09-10 19:04:54 +00:00
2021-07-07 02:00:18 +00:00
async _injectAnkiNoteDictionaryMedia ( ankiConnect , timestamp , definitionDetails , dictionaryMediaDetails ) {
const targets = [ ] ;
const detailsList = [ ] ;
const detailsMap = new Map ( ) ;
for ( const { dictionary , path } of dictionaryMediaDetails ) {
const target = { dictionary , path } ;
const details = { dictionary , path , media : null } ;
const key = JSON . stringify ( target ) ;
targets . push ( target ) ;
detailsList . push ( details ) ;
detailsMap . set ( key , details ) ;
}
2021-09-04 02:33:58 +00:00
const mediaList = await this . _getNormalizedDictionaryDatabaseMedia ( targets ) ;
2021-07-07 02:00:18 +00:00
for ( const media of mediaList ) {
const { dictionary , path } = media ;
const key = JSON . stringify ( { dictionary , path } ) ;
const details = detailsMap . get ( key ) ;
if ( typeof details === 'undefined' || details . media !== null ) { continue ; }
details . media = media ;
}
const errors = [ ] ;
const results = [ ] ;
for ( let i = 0 , ii = detailsList . length ; i < ii ; ++ i ) {
const { dictionary , path , media } = detailsList [ i ] ;
let fileName = null ;
if ( media !== null ) {
const { content , mediaType } = media ;
const extension = MediaUtil . getFileExtensionFromImageMediaType ( mediaType ) ;
fileName = this . _generateAnkiNoteMediaFileName ( ` yomichan_dictionary_media_ ${ i + 1 } ` , extension , timestamp , definitionDetails ) ;
try {
2022-03-14 01:17:41 +00:00
fileName = await ankiConnect . storeMediaFile ( fileName , content ) ;
2021-07-07 02:00:18 +00:00
} catch ( e ) {
errors . push ( e ) ;
fileName = null ;
}
}
results . push ( { dictionary , path , fileName } ) ;
}
return { results , errors } ;
}
2022-05-29 01:55:37 +00:00
_getAudioDownloadError ( error ) {
if ( isObject ( error . data ) ) {
const { errors } = error . data ;
if ( Array . isArray ( errors ) ) {
for ( const error2 of errors ) {
2022-08-20 15:17:24 +00:00
if ( error2 . name === 'AbortError' ) {
return this . _createAudioDownloadError ( 'Audio download was cancelled due to an idle timeout' , 'audio-download-idle-timeout' , errors ) ;
}
2022-05-29 01:55:37 +00:00
if ( ! isObject ( error2 . data ) ) { continue ; }
const { details } = error2 . data ;
if ( ! isObject ( details ) ) { continue ; }
2022-10-06 02:51:15 +00:00
switch ( details . error ) {
case 'net::ERR_FAILED' :
// This is potentially an error due to the extension not having enough URL privileges.
// The message logged to the console looks like this:
// Access to fetch at '<URL>' from origin 'chrome-extension://<ID>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
return this . _createAudioDownloadError ( 'Audio download failed due to possible extension permissions error' , 'audio-download-failed-permissions-error' , errors ) ;
case 'net::ERR_CERT_DATE_INVALID' : // Chrome
case 'Peer’ s Certificate has expired.' : // Firefox
// This error occurs when a server certificate expires.
return this . _createAudioDownloadError ( 'Audio download failed due to an expired server certificate' , 'audio-download-failed-expired-server-certificate' , errors ) ;
2022-05-29 01:55:37 +00:00
}
}
}
}
return null ;
}
2022-08-20 15:17:24 +00:00
_createAudioDownloadError ( message , issueId , errors ) {
const error = new Error ( message ) ;
const hasErrors = Array . isArray ( errors ) ;
const hasIssueId = ( typeof issueId === 'string' ) ;
if ( hasErrors || hasIssueId ) {
error . data = { } ;
if ( hasErrors ) {
// Errors need to be serialized since they are passed to other frames
error . data . errors = errors . map ( ( e ) => serializeError ( e ) ) ;
}
if ( hasIssueId ) {
error . data . referenceUrl = ` /issues.html# ${ issueId } ` ;
}
}
return error ;
}
2020-11-27 03:53:58 +00:00
_generateAnkiNoteMediaFileName ( prefix , extension , timestamp , definitionDetails ) {
let fileName = prefix ;
2020-09-10 19:04:54 +00:00
2020-11-27 03:53:58 +00:00
switch ( definitionDetails . type ) {
case 'kanji' :
{
const { character } = definitionDetails ;
if ( character ) { fileName += ` _ ${ character } ` ; }
}
break ;
default :
{
2021-03-26 23:57:57 +00:00
const { reading , term } = definitionDetails ;
2020-11-27 03:53:58 +00:00
if ( reading ) { fileName += ` _ ${ reading } ` ; }
2021-03-26 23:57:57 +00:00
if ( term ) { fileName += ` _ ${ term } ` ; }
2020-11-27 03:53:58 +00:00
}
break ;
2020-09-10 19:04:54 +00:00
}
2020-11-27 03:53:58 +00:00
fileName += ` _ ${ this . _ankNoteDateToString ( new Date ( timestamp ) ) } ` ;
fileName += extension ;
fileName = this . _replaceInvalidFileNameCharacters ( fileName ) ;
return fileName ;
2020-09-10 19:04:54 +00:00
}
_replaceInvalidFileNameCharacters ( fileName ) {
// eslint-disable-next-line no-control-regex
return fileName . replace ( /[<>:"/\\|?*\x00-\x1F]/g , '-' ) ;
}
_ankNoteDateToString ( date ) {
const year = date . getUTCFullYear ( ) ;
const month = date . getUTCMonth ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const day = date . getUTCDate ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const hours = date . getUTCHours ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const minutes = date . getUTCMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
const seconds = date . getUTCSeconds ( ) . toString ( ) . padStart ( 2 , '0' ) ;
return ` ${ year } - ${ month } - ${ day } - ${ hours } - ${ minutes } - ${ seconds } ` ;
}
_getDataUrlInfo ( dataUrl ) {
const match = /^data:([^,]*?)(;base64)?,/ . exec ( dataUrl ) ;
if ( match === null ) {
throw new Error ( 'Invalid data URL' ) ;
}
let mediaType = match [ 1 ] ;
if ( mediaType . length === 0 ) { mediaType = 'text/plain' ; }
let data = dataUrl . substring ( match [ 0 ] . length ) ;
if ( typeof match [ 2 ] === 'undefined' ) { data = btoa ( data ) ; }
return { mediaType , data } ;
}
2020-09-13 22:43:44 +00:00
_triggerDatabaseUpdated ( type , cause ) {
2020-09-19 21:17:33 +00:00
this . _translator . clearDatabaseCaches ( ) ;
2021-11-21 20:54:58 +00:00
this . _sendMessageAllTabsIgnoreResponse ( 'Yomichan.databaseUpdated' , { type , cause } ) ;
2020-09-13 22:43:44 +00:00
}
2020-09-15 23:48:58 +00:00
async _saveOptions ( source ) {
this . _clearProfileConditionsSchemaCache ( ) ;
2021-01-18 22:25:49 +00:00
const options = this . _getOptionsFull ( ) ;
2020-09-15 23:48:58 +00:00
await this . _optionsUtil . save ( options ) ;
this . _applyOptions ( source ) ;
}
2020-10-04 16:54:55 +00:00
2022-05-20 00:16:40 +00:00
/ * *
* Creates an options object for use with ` Translator.findTerms ` .
* @ param { string } mode The display mode for the dictionary entries .
* @ param { { matchType : string , deinflect : boolean } } details Custom info for finding terms .
* @ param { object } options The options .
* @ returns { FindTermsOptions } An options object .
* /
2021-06-05 17:35:23 +00:00
_getTranslatorFindTermsOptions ( mode , details , options ) {
2021-12-17 22:02:13 +00:00
let { matchType , deinflect } = details ;
2021-12-17 21:11:19 +00:00
if ( typeof matchType !== 'string' ) { matchType = 'exact' ; }
2021-12-17 22:02:13 +00:00
if ( typeof deinflect !== 'boolean' ) { deinflect = true ; }
2020-10-04 16:54:55 +00:00
const enabledDictionaryMap = this . _getTranslatorEnabledDictionaryMap ( options ) ;
const {
2021-09-26 15:08:16 +00:00
general : { mainDictionary , sortFrequencyDictionary , sortFrequencyDictionaryOrder } ,
2020-10-04 16:54:55 +00:00
scanning : { alphanumeric } ,
translation : {
convertHalfWidthCharacters ,
convertNumericCharacters ,
convertAlphabeticCharacters ,
convertHiraganaToKatakana ,
convertKatakanaToHiragana ,
2021-01-03 17:12:55 +00:00
collapseEmphaticSequences ,
textReplacements : textReplacementsOptions
2020-10-04 16:54:55 +00:00
}
} = options ;
2021-01-03 17:12:55 +00:00
const textReplacements = this . _getTranslatorTextReplacements ( textReplacementsOptions ) ;
2021-06-05 17:35:23 +00:00
let excludeDictionaryDefinitions = null ;
if ( mode === 'merge' && ! enabledDictionaryMap . has ( mainDictionary ) ) {
enabledDictionaryMap . set ( mainDictionary , {
index : enabledDictionaryMap . size ,
priority : 0 ,
allowSecondarySearches : false
} ) ;
excludeDictionaryDefinitions = new Set ( ) ;
excludeDictionaryDefinitions . add ( mainDictionary ) ;
}
2020-10-04 16:54:55 +00:00
return {
2021-12-17 21:11:19 +00:00
matchType ,
2021-12-17 22:02:13 +00:00
deinflect ,
2020-10-04 16:54:55 +00:00
mainDictionary ,
2021-09-26 15:08:16 +00:00
sortFrequencyDictionary ,
sortFrequencyDictionaryOrder ,
2021-04-29 01:17:05 +00:00
removeNonJapaneseCharacters : ! alphanumeric ,
2020-10-04 16:54:55 +00:00
convertHalfWidthCharacters ,
convertNumericCharacters ,
convertAlphabeticCharacters ,
convertHiraganaToKatakana ,
convertKatakanaToHiragana ,
collapseEmphaticSequences ,
2021-01-03 17:12:55 +00:00
textReplacements ,
2021-06-05 17:35:23 +00:00
enabledDictionaryMap ,
excludeDictionaryDefinitions
2020-10-04 16:54:55 +00:00
} ;
}
2022-05-20 00:16:40 +00:00
/ * *
* Creates an options object for use with ` Translator.findKanji ` .
* @ param { object } options The options .
* @ returns { FindKanjiOptions } An options object .
* /
2020-10-04 16:54:55 +00:00
_getTranslatorFindKanjiOptions ( options ) {
const enabledDictionaryMap = this . _getTranslatorEnabledDictionaryMap ( options ) ;
return { enabledDictionaryMap } ;
}
_getTranslatorEnabledDictionaryMap ( options ) {
2021-03-06 18:04:50 +00:00
const enabledDictionaryMap = new Map ( ) ;
2021-04-03 17:02:49 +00:00
for ( const dictionary of options . dictionaries ) {
2021-02-28 04:11:41 +00:00
if ( ! dictionary . enabled ) { continue ; }
2021-04-03 17:02:49 +00:00
enabledDictionaryMap . set ( dictionary . name , {
2021-03-06 18:04:50 +00:00
index : enabledDictionaryMap . size ,
2021-02-28 04:11:41 +00:00
priority : dictionary . priority ,
allowSecondarySearches : dictionary . allowSecondarySearches
} ) ;
2020-10-04 16:54:55 +00:00
}
return enabledDictionaryMap ;
}
2020-12-13 21:09:11 +00:00
2021-01-03 17:12:55 +00:00
_getTranslatorTextReplacements ( textReplacementsOptions ) {
const textReplacements = [ ] ;
for ( const group of textReplacementsOptions . groups ) {
const textReplacementsEntries = [ ] ;
for ( let { pattern , ignoreCase , replacement } of group ) {
try {
pattern = new RegExp ( pattern , ignoreCase ? 'gi' : 'g' ) ;
} catch ( e ) {
// Invalid pattern
continue ;
}
textReplacementsEntries . push ( { pattern , replacement } ) ;
}
if ( textReplacementsEntries . length > 0 ) {
textReplacements . push ( textReplacementsEntries ) ;
}
}
if ( textReplacements . length === 0 || textReplacementsOptions . searchOriginal ) {
textReplacements . unshift ( null ) ;
}
return textReplacements ;
}
2020-12-13 21:09:11 +00:00
async _openWelcomeGuidePage ( ) {
2021-02-13 04:03:15 +00:00
await this . _createTab ( chrome . runtime . getURL ( '/welcome.html' ) ) ;
2020-12-13 21:09:11 +00:00
}
async _openInfoPage ( ) {
2021-02-13 04:03:15 +00:00
await this . _createTab ( chrome . runtime . getURL ( '/info.html' ) ) ;
2020-12-13 21:09:11 +00:00
}
async _openSettingsPage ( mode ) {
const manifest = chrome . runtime . getManifest ( ) ;
2021-03-15 02:51:48 +00:00
const url = chrome . runtime . getURL ( manifest . options _ui . page ) ;
2020-12-13 21:09:11 +00:00
switch ( mode ) {
case 'existingOrNewTab' :
2021-03-15 02:51:48 +00:00
await new Promise ( ( resolve , reject ) => {
chrome . runtime . openOptionsPage ( ( ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( ) ;
}
2020-12-13 21:09:11 +00:00
} ) ;
2021-03-15 02:51:48 +00:00
} ) ;
2020-12-13 21:09:11 +00:00
break ;
case 'newTab' :
await this . _createTab ( url ) ;
break ;
}
}
_createTab ( url ) {
return new Promise ( ( resolve , reject ) => {
chrome . tabs . create ( { url } , ( tab ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( tab ) ;
}
} ) ;
} ) ;
}
2020-12-18 20:54:05 +00:00
2021-02-10 04:14:29 +00:00
_getTabById ( tabId ) {
return new Promise ( ( resolve , reject ) => {
chrome . tabs . get (
tabId ,
( result ) => {
const e = chrome . runtime . lastError ;
if ( e ) {
reject ( new Error ( e . message ) ) ;
} else {
resolve ( result ) ;
}
}
) ;
} ) ;
}
2021-02-11 23:55:09 +00:00
async _checkPermissions ( ) {
this . _permissions = await this . _permissionsUtil . getAllPermissions ( ) ;
this . _updateBadge ( ) ;
}
2021-03-11 01:26:57 +00:00
_canObservePermissionsChanges ( ) {
return isObject ( chrome . permissions ) && isObject ( chrome . permissions . onAdded ) && isObject ( chrome . permissions . onRemoved ) ;
}
2021-02-11 23:55:09 +00:00
_hasRequiredPermissionsForSettings ( options ) {
2021-03-11 01:26:57 +00:00
if ( ! this . _canObservePermissionsChanges ( ) ) { return true ; }
2021-02-11 23:55:09 +00:00
return this . _permissions === null || this . _permissionsUtil . hasRequiredPermissionsForOptions ( this . _permissions , options ) ;
}
2021-03-03 03:27:53 +00:00
async _requestPersistentStorage ( ) {
try {
if ( await navigator . storage . persisted ( ) ) { return ; }
// Only request this permission for Firefox versions >= 77.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1630413
const { vendor , version } = await browser . runtime . getBrowserInfo ( ) ;
if ( vendor !== 'Mozilla' ) { return ; }
const match = /^\d+/ . exec ( version ) ;
if ( match === null ) { return ; }
const versionNumber = Number . parseInt ( match [ 0 ] ) ;
if ( ! ( Number . isFinite ( versionNumber ) && versionNumber >= 77 ) ) { return ; }
await navigator . storage . persist ( ) ;
} catch ( e ) {
// NOP
}
}
2021-08-07 16:40:51 +00:00
2021-09-04 02:33:58 +00:00
async _getNormalizedDictionaryDatabaseMedia ( targets ) {
const results = await this . _dictionaryDatabase . getMedia ( targets ) ;
for ( const item of results ) {
const { content } = item ;
if ( content instanceof ArrayBuffer ) {
2022-08-20 16:53:22 +00:00
item . content = ArrayBufferUtil . arrayBufferToBase64 ( content ) ;
2021-09-04 02:33:58 +00:00
}
}
return results ;
}
2017-08-14 04:11:10 +00:00
}