2020-09-04 21:54:34 +00:00
/ *
2021-01-01 19:50:41 +00:00
* Copyright ( C ) 2020 - 2021 Yomichan Authors
2020-09-04 21:54:34 +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
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
* /
/ * g l o b a l
2021-03-31 22:17:28 +00:00
* DictionaryController
2021-08-28 18:22:16 +00:00
* DictionaryWorker
2020-09-04 21:54:34 +00:00
* /
class DictionaryImportController {
2021-03-03 01:27:51 +00:00
constructor ( settingsController , modalController , statusFooter ) {
2020-09-04 21:54:34 +00:00
this . _settingsController = settingsController ;
2020-10-11 00:58:38 +00:00
this . _modalController = modalController ;
2020-10-18 22:28:14 +00:00
this . _statusFooter = statusFooter ;
2020-09-04 21:54:34 +00:00
this . _modifying = false ;
this . _purgeButton = null ;
this . _purgeConfirmButton = null ;
this . _importFileButton = null ;
this . _importFileInput = null ;
this . _purgeConfirmModal = null ;
this . _errorContainer = null ;
this . _spinner = null ;
this . _purgeNotification = null ;
this . _errorToStringOverrides = [
[
'A mutation operation was attempted on a database that did not allow mutations.' ,
'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
] ,
[
'The operation failed for reasons unrelated to the database itself and not covered by any other error code.' ,
'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
]
] ;
}
async prepare ( ) {
2020-10-11 21:31:58 +00:00
this . _purgeButton = document . querySelector ( '#dictionary-delete-all-button' ) ;
this . _purgeConfirmButton = document . querySelector ( '#dictionary-confirm-delete-all-button' ) ;
this . _importFileButton = document . querySelector ( '#dictionary-import-file-button' ) ;
this . _importFileInput = document . querySelector ( '#dictionary-import-file-input' ) ;
this . _purgeConfirmModal = this . _modalController . getModal ( 'dictionary-confirm-delete-all' ) ;
this . _errorContainer = document . querySelector ( '#dictionary-error' ) ;
this . _spinner = document . querySelector ( '#dictionary-spinner' ) ;
this . _purgeNotification = document . querySelector ( '#dictionary-delete-all-status' ) ;
2020-09-04 21:54:34 +00:00
this . _purgeButton . addEventListener ( 'click' , this . _onPurgeButtonClick . bind ( this ) , false ) ;
this . _purgeConfirmButton . addEventListener ( 'click' , this . _onPurgeConfirmButtonClick . bind ( this ) , false ) ;
this . _importFileButton . addEventListener ( 'click' , this . _onImportButtonClick . bind ( this ) , false ) ;
this . _importFileInput . addEventListener ( 'change' , this . _onImportFileChange . bind ( this ) , false ) ;
}
// Private
_onImportButtonClick ( ) {
this . _importFileInput . click ( ) ;
}
_onPurgeButtonClick ( e ) {
e . preventDefault ( ) ;
2020-09-19 21:14:51 +00:00
this . _purgeConfirmModal . setVisible ( true ) ;
2020-09-04 21:54:34 +00:00
}
_onPurgeConfirmButtonClick ( e ) {
e . preventDefault ( ) ;
2020-09-19 21:14:51 +00:00
this . _purgeConfirmModal . setVisible ( false ) ;
2020-09-04 21:54:34 +00:00
this . _purgeDatabase ( ) ;
}
_onImportFileChange ( e ) {
const node = e . currentTarget ;
const files = [ ... node . files ] ;
node . value = null ;
this . _importDictionaries ( files ) ;
}
async _purgeDatabase ( ) {
if ( this . _modifying ) { return ; }
const purgeNotification = this . _purgeNotification ;
const prevention = this . _preventPageExit ( ) ;
try {
this . _setModifying ( true ) ;
this . _hideErrors ( ) ;
this . _setSpinnerVisible ( true ) ;
2020-10-28 01:20:26 +00:00
if ( purgeNotification !== null ) { purgeNotification . hidden = false ; }
2020-09-04 21:54:34 +00:00
2021-02-14 20:53:35 +00:00
await yomichan . api . purgeDatabase ( ) ;
2020-09-04 21:54:34 +00:00
const errors = await this . _clearDictionarySettings ( ) ;
if ( errors . length > 0 ) {
this . _showErrors ( errors ) ;
}
} catch ( error ) {
this . _showErrors ( [ error ] ) ;
} finally {
prevention . end ( ) ;
2020-10-28 01:20:26 +00:00
if ( purgeNotification !== null ) { purgeNotification . hidden = true ; }
2020-09-04 21:54:34 +00:00
this . _setSpinnerVisible ( false ) ;
this . _setModifying ( false ) ;
2021-03-03 01:27:51 +00:00
this . _triggerStorageChanged ( ) ;
2020-09-04 21:54:34 +00:00
}
}
async _importDictionaries ( files ) {
if ( this . _modifying ) { return ; }
2020-10-18 22:28:14 +00:00
const statusFooter = this . _statusFooter ;
const importInfo = document . querySelector ( '#dictionary-import-info' ) ;
const progressSelector = '.dictionary-import-progress' ;
2021-05-10 22:16:01 +00:00
const progressContainers = document . querySelectorAll ( ` #dictionaries-modal ${ progressSelector } ` ) ;
const progressBars = document . querySelectorAll ( ` ${ progressSelector } .progress-bar ` ) ;
2020-10-18 22:28:14 +00:00
const infoLabels = document . querySelectorAll ( ` ${ progressSelector } .progress-info ` ) ;
const statusLabels = document . querySelectorAll ( ` ${ progressSelector } .progress-status ` ) ;
2020-09-04 21:54:34 +00:00
const prevention = this . _preventPageExit ( ) ;
try {
this . _setModifying ( true ) ;
this . _hideErrors ( ) ;
this . _setSpinnerVisible ( true ) ;
2020-10-18 22:28:14 +00:00
for ( const progress of progressContainers ) { progress . hidden = false ; }
2020-09-04 21:54:34 +00:00
const optionsFull = await this . _settingsController . getOptionsFull ( ) ;
const importDetails = {
prefixWildcardsSupported : optionsFull . global . database . prefixWildcardsSupported
} ;
2021-07-31 23:13:41 +00:00
let statusPrefix = '' ;
let stepIndex = - 2 ;
const onProgress = ( data ) => {
const { stepIndex : stepIndex2 , index , count } = data ;
if ( stepIndex !== stepIndex2 ) {
stepIndex = stepIndex2 ;
const labelText = ` ${ statusPrefix } - Step ${ stepIndex2 + 1 } of ${ data . stepCount } : ${ this . _getImportLabel ( stepIndex2 ) } ... ` ;
for ( const label of infoLabels ) { label . textContent = labelText ; }
}
const percent = count > 0 ? ( index / count * 100.0 ) : 0.0 ;
2020-10-18 22:28:14 +00:00
const cssString = ` ${ percent } % ` ;
2021-07-31 23:13:41 +00:00
const statusString = ` ${ Math . floor ( percent ) . toFixed ( 0 ) } % ` ;
2020-10-18 22:28:14 +00:00
for ( const progressBar of progressBars ) { progressBar . style . width = cssString ; }
for ( const label of statusLabels ) { label . textContent = statusString ; }
2021-07-31 23:13:41 +00:00
switch ( stepIndex2 ) {
case - 2 : // Initialize
case 5 : // Data import
this . _triggerStorageChanged ( ) ;
break ;
}
2020-09-04 21:54:34 +00:00
} ;
const fileCount = files . length ;
for ( let i = 0 ; i < fileCount ; ++ i ) {
2020-10-18 22:28:14 +00:00
if ( importInfo !== null && fileCount > 1 ) {
2020-09-04 21:54:34 +00:00
importInfo . hidden = false ;
importInfo . textContent = ` ( ${ i + 1 } of ${ fileCount } ) ` ;
}
2021-07-31 23:13:41 +00:00
statusPrefix = ` Importing dictionary ${ fileCount > 1 ? ` ( ${ i + 1 } of ${ fileCount } ) ` : '' } ` ;
onProgress ( {
stepIndex : - 1 ,
stepCount : 6 ,
index : 0 ,
count : 0
} ) ;
2020-10-18 22:28:14 +00:00
if ( statusFooter !== null ) { statusFooter . setTaskActive ( progressSelector , true ) ; }
await this . _importDictionary ( files [ i ] , importDetails , onProgress ) ;
2020-09-04 21:54:34 +00:00
}
} catch ( err ) {
this . _showErrors ( [ err ] ) ;
} finally {
prevention . end ( ) ;
2020-10-18 22:28:14 +00:00
for ( const progress of progressContainers ) { progress . hidden = true ; }
if ( statusFooter !== null ) { statusFooter . setTaskActive ( progressSelector , false ) ; }
if ( importInfo !== null ) {
importInfo . textContent = '' ;
importInfo . hidden = true ;
}
2020-09-04 21:54:34 +00:00
this . _setSpinnerVisible ( false ) ;
this . _setModifying ( false ) ;
2021-03-03 01:27:51 +00:00
this . _triggerStorageChanged ( ) ;
2020-09-04 21:54:34 +00:00
}
}
2021-07-31 23:13:41 +00:00
_getImportLabel ( stepIndex ) {
switch ( stepIndex ) {
case - 1 :
case 0 : return 'Loading dictionary' ;
case 1 : return 'Loading schemas' ;
case 2 : return 'Validating data' ;
2021-09-27 00:08:16 +00:00
case 3 : return 'Formatting data' ;
case 4 : return 'Importing media' ;
2021-07-31 23:13:41 +00:00
case 5 : return 'Importing data' ;
default : return '' ;
}
}
2020-09-04 21:54:34 +00:00
async _importDictionary ( file , importDetails , onProgress ) {
2021-07-31 18:46:09 +00:00
const archiveContent = await this . _readFile ( file ) ;
2021-08-28 18:22:16 +00:00
const { result , errors } = await new DictionaryWorker ( ) . importDictionary ( archiveContent , importDetails , onProgress ) ;
2021-07-31 18:46:09 +00:00
yomichan . api . triggerDatabaseUpdated ( 'dictionary' , 'import' ) ;
const errors2 = await this . _addDictionarySettings ( result . sequenced , result . title ) ;
if ( errors . length > 0 ) {
const allErrors = [ ... errors , ... errors2 ] ;
allErrors . push ( new Error ( ` Dictionary may not have been imported properly: ${ allErrors . length } error ${ allErrors . length === 1 ? '' : 's' } reported. ` ) ) ;
this . _showErrors ( allErrors ) ;
2020-09-04 21:54:34 +00:00
}
}
async _addDictionarySettings ( sequenced , title ) {
const optionsFull = await this . _settingsController . getOptionsFull ( ) ;
const targets = [ ] ;
const profileCount = optionsFull . profiles . length ;
for ( let i = 0 ; i < profileCount ; ++ i ) {
const { options } = optionsFull . profiles [ i ] ;
2021-04-03 17:02:49 +00:00
const value = DictionaryController . createDefaultDictionarySettings ( title , true ) ;
const path1 = ` profiles[ ${ i } ].options.dictionaries ` ;
targets . push ( { action : 'push' , path : path1 , items : [ value ] } ) ;
2020-09-04 21:54:34 +00:00
if ( sequenced && options . general . mainDictionary === '' ) {
2021-04-03 17:02:49 +00:00
const path2 = ` profiles[ ${ i } ].options.general.mainDictionary ` ;
2020-09-04 21:54:34 +00:00
targets . push ( { action : 'set' , path : path2 , value : title } ) ;
}
}
return await this . _modifyGlobalSettings ( targets ) ;
}
async _clearDictionarySettings ( ) {
const optionsFull = await this . _settingsController . getOptionsFull ( ) ;
const targets = [ ] ;
const profileCount = optionsFull . profiles . length ;
for ( let i = 0 ; i < profileCount ; ++ i ) {
2021-04-03 17:02:49 +00:00
const path1 = ` profiles[ ${ i } ].options.dictionaries ` ;
targets . push ( { action : 'set' , path : path1 , value : [ ] } ) ;
const path2 = ` profiles[ ${ i } ].options.general.mainDictionary ` ;
2020-09-04 21:54:34 +00:00
targets . push ( { action : 'set' , path : path2 , value : '' } ) ;
}
return await this . _modifyGlobalSettings ( targets ) ;
}
_setSpinnerVisible ( visible ) {
2020-10-19 21:25:15 +00:00
if ( this . _spinner !== null ) {
this . _spinner . hidden = ! visible ;
}
2020-09-04 21:54:34 +00:00
}
_preventPageExit ( ) {
return this . _settingsController . preventPageExit ( ) ;
}
_showErrors ( errors ) {
const uniqueErrors = new Map ( ) ;
for ( const error of errors ) {
2021-02-14 22:52:01 +00:00
log . error ( error ) ;
2020-09-04 21:54:34 +00:00
const errorString = this . _errorToString ( error ) ;
let count = uniqueErrors . get ( errorString ) ;
if ( typeof count === 'undefined' ) {
count = 0 ;
}
uniqueErrors . set ( errorString , count + 1 ) ;
}
const fragment = document . createDocumentFragment ( ) ;
for ( const [ e , count ] of uniqueErrors . entries ( ) ) {
const div = document . createElement ( 'p' ) ;
if ( count > 1 ) {
div . textContent = ` ${ e } ` ;
const em = document . createElement ( 'em' ) ;
em . textContent = ` ( ${ count } ) ` ;
div . appendChild ( em ) ;
} else {
div . textContent = ` ${ e } ` ;
}
fragment . appendChild ( div ) ;
}
this . _errorContainer . appendChild ( fragment ) ;
this . _errorContainer . hidden = false ;
}
_hideErrors ( ) {
this . _errorContainer . textContent = '' ;
this . _errorContainer . hidden = true ;
}
_readFile ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . onload = ( ) => resolve ( reader . result ) ;
reader . onerror = ( ) => reject ( reader . error ) ;
2021-07-31 14:53:35 +00:00
reader . readAsArrayBuffer ( file ) ;
2020-09-04 21:54:34 +00:00
} ) ;
}
_errorToString ( error ) {
error = ( typeof error . toString === 'function' ? error . toString ( ) : ` ${ error } ` ) ;
for ( const [ match , newErrorString ] of this . _errorToStringOverrides ) {
if ( error . includes ( match ) ) {
return newErrorString ;
}
}
return error ;
}
_setModifying ( value ) {
this . _modifying = value ;
this . _setButtonsEnabled ( ! value ) ;
}
_setButtonsEnabled ( value ) {
value = ! value ;
2020-10-11 21:31:58 +00:00
for ( const node of document . querySelectorAll ( '.dictionary-database-mutating-input' ) ) {
2020-09-15 23:35:44 +00:00
node . disabled = value ;
}
2020-09-04 21:54:34 +00:00
}
async _modifyGlobalSettings ( targets ) {
const results = await this . _settingsController . modifyGlobalSettings ( targets ) ;
const errors = [ ] ;
for ( const { error } of results ) {
if ( typeof error !== 'undefined' ) {
2021-01-08 02:36:20 +00:00
errors . push ( deserializeError ( error ) ) ;
2020-09-04 21:54:34 +00:00
}
}
return errors ;
}
2021-03-03 01:27:51 +00:00
_triggerStorageChanged ( ) {
yomichan . trigger ( 'storageChanged' ) ;
}
2020-09-04 21:54:34 +00:00
}