2020-09-04 21:54:34 +00:00
/ *
* Copyright ( C ) 2020 Yomichan Authors
*
* 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
* DictionaryDatabase
* DictionaryImporter
* ObjectPropertyAccessor
* api
* /
class DictionaryImportController {
2020-12-13 16:29:32 +00:00
constructor ( settingsController , modalController , storageController , statusFooter ) {
2020-09-04 21:54:34 +00:00
this . _settingsController = settingsController ;
2020-10-11 00:58:38 +00:00
this . _modalController = modalController ;
2020-09-04 21:54:34 +00:00
this . _storageController = storageController ;
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 ;
2020-12-13 17:32:43 +00:00
const storageController = this . _storageController ;
2020-09-04 21:54:34 +00:00
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
await api . purgeDatabase ( ) ;
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 ) ;
2020-12-13 17:32:43 +00:00
if ( storageController !== null ) { storageController . updateStats ( ) ; }
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 ;
2020-09-04 21:54:34 +00:00
const storageController = this . _storageController ;
2020-10-18 22:28:14 +00:00
const importInfo = document . querySelector ( '#dictionary-import-info' ) ;
const progressSelector = '.dictionary-import-progress' ;
const progressContainers = [
... document . querySelectorAll ( '#dictionary-import-progress-container' ) ,
... document . querySelectorAll ( ` #dictionaries ${ progressSelector } ` )
] ;
const progressBars = [
... document . querySelectorAll ( '#dictionary-import-progress-container .progress-bar' ) ,
... document . querySelectorAll ( ` ${ progressSelector } .progress-bar ` )
] ;
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
} ;
2020-10-18 22:28:14 +00:00
const onProgress = ( total , current ) => {
2020-09-04 21:54:34 +00:00
const percent = ( current / total * 100.0 ) ;
2020-10-18 22:28:14 +00:00
const cssString = ` ${ percent } % ` ;
const statusString = ` ${ percent . toFixed ( 0 ) } % ` ;
for ( const progressBar of progressBars ) { progressBar . style . width = cssString ; }
for ( const label of statusLabels ) { label . textContent = statusString ; }
2020-12-13 17:32:43 +00:00
if ( storageController !== null ) { storageController . updateStats ( ) ; }
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 } ) ` ;
}
2020-10-18 22:28:14 +00:00
onProgress ( 1 , 0 ) ;
const labelText = ` Importing dictionary ${ fileCount > 1 ? ` ( ${ i + 1 } of ${ fileCount } ) ` : '' } ... ` ;
for ( const label of infoLabels ) { label . textContent = labelText ; }
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 ) ;
2020-12-13 17:32:43 +00:00
if ( storageController !== null ) { storageController . updateStats ( ) ; }
2020-09-04 21:54:34 +00:00
}
}
async _importDictionary ( file , importDetails , onProgress ) {
const dictionaryDatabase = await this . _getPreparedDictionaryDatabase ( ) ;
try {
const dictionaryImporter = new DictionaryImporter ( ) ;
const archiveContent = await this . _readFile ( file ) ;
const { result , errors } = await dictionaryImporter . importDictionary ( dictionaryDatabase , archiveContent , importDetails , onProgress ) ;
2020-09-13 22:43:44 +00:00
api . triggerDatabaseUpdated ( 'dictionary' , 'import' ) ;
2020-09-04 21:54:34 +00:00
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 ) ;
}
} finally {
dictionaryDatabase . close ( ) ;
}
}
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 ] ;
const value = this . _createDictionaryOptions ( ) ;
const path1 = ObjectPropertyAccessor . getPathString ( [ 'profiles' , i , 'options' , 'dictionaries' , title ] ) ;
targets . push ( { action : 'set' , path : path1 , value } ) ;
if ( sequenced && options . general . mainDictionary === '' ) {
const path2 = ObjectPropertyAccessor . getPathString ( [ 'profiles' , i , 'options' , 'general' , 'mainDictionary' ] ) ;
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 ) {
const path1 = ObjectPropertyAccessor . getPathString ( [ 'profiles' , i , 'options' , 'dictionaries' ] ) ;
targets . push ( { action : 'set' , path : path1 , value : { } } ) ;
const path2 = ObjectPropertyAccessor . getPathString ( [ 'profiles' , i , 'options' , 'general' , 'mainDictionary' ] ) ;
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 ) {
yomichan . logError ( error ) ;
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 ) ;
reader . readAsBinaryString ( file ) ;
} ) ;
}
_createDictionaryOptions ( ) {
return {
priority : 0 ,
enabled : true ,
allowSecondarySearches : false
} ;
}
_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 _getPreparedDictionaryDatabase ( ) {
const dictionaryDatabase = new DictionaryDatabase ( ) ;
await dictionaryDatabase . prepare ( ) ;
return dictionaryDatabase ;
}
async _modifyGlobalSettings ( targets ) {
const results = await this . _settingsController . modifyGlobalSettings ( targets ) ;
const errors = [ ] ;
for ( const { error } of results ) {
if ( typeof error !== 'undefined' ) {
errors . push ( jsonToError ( error ) ) ;
}
}
return errors ;
}
}