2017-06-24 20:06:28 +02:00
import { BaseModel } from 'lib/base-model.js' ;
import { Database } from 'lib/database.js' ;
2017-08-20 22:11:32 +02:00
import { Logger } from 'lib/logger.js' ;
2017-10-20 00:02:13 +02:00
import { sprintf } from 'sprintf-js' ;
2017-07-24 23:29:40 +02:00
import { _ , supportedLocalesToLanguages , defaultLocale } from 'lib/locale.js' ;
2017-05-12 22:17:23 +02:00
class Setting extends BaseModel {
static tableName ( ) {
return 'settings' ;
}
2017-07-03 21:50:45 +02:00
static modelType ( ) {
return BaseModel . TYPE _SETTING ;
2017-06-19 21:26:27 +02:00
}
2017-07-24 20:58:11 +02:00
static settingMetadata ( key ) {
if ( ! ( key in this . metadata _ ) ) throw new Error ( 'Unknown key: ' + key ) ;
let output = Object . assign ( { } , this . metadata _ [ key ] ) ;
2017-05-12 22:17:23 +02:00
output . key = key ;
return output ;
}
2017-10-30 02:29:10 +02:00
static keyExists ( key ) {
return key in this . metadata _ ;
}
2017-08-22 19:57:35 +02:00
static keys ( publicOnly = false , appType = null ) {
if ( ! this . keys _ ) {
this . keys _ = [ ] ;
for ( let n in this . metadata _ ) {
if ( ! this . metadata _ . hasOwnProperty ( n ) ) continue ;
this . keys _ . push ( n ) ;
}
this . keys _ . sort ( ) ;
2017-05-16 23:46:21 +02:00
}
2017-08-22 19:57:35 +02:00
if ( appType || publicOnly ) {
let output = [ ] ;
for ( let i = 0 ; i < this . keys _ . length ; i ++ ) {
const md = this . settingMetadata ( this . keys _ [ i ] ) ;
if ( publicOnly && ! md . public ) continue ;
if ( appType && md . appTypes && md . appTypes . indexOf ( appType ) < 0 ) continue ;
output . push ( md . key ) ;
}
return output ;
} else {
return this . keys _ ;
2017-06-27 22:16:03 +02:00
}
}
2017-07-31 22:51:24 +02:00
static isPublic ( key ) {
2017-08-22 19:57:35 +02:00
return this . keys ( true ) . indexOf ( key ) >= 0 ;
2017-07-31 22:51:24 +02:00
}
2017-05-12 22:17:23 +02:00
static load ( ) {
2017-07-26 19:49:01 +02:00
this . cancelScheduleSave ( ) ;
2017-05-12 22:17:23 +02:00
this . cache _ = [ ] ;
2017-06-19 21:26:27 +02:00
return this . modelSelectAll ( 'SELECT * FROM settings' ) . then ( ( rows ) => {
2017-07-26 20:36:16 +02:00
this . cache _ = [ ] ;
2017-07-24 22:36:49 +02:00
2017-07-26 20:36:16 +02:00
// Old keys - can be removed later
2017-10-30 02:29:10 +02:00
//const ignore = ['clientId', 'sync.onedrive.auth', 'syncInterval', 'todoOnTop', 'todosOnTop', 'todoFilter'];
2017-07-25 23:55:26 +02:00
2017-07-26 20:36:16 +02:00
for ( let i = 0 ; i < rows . length ; i ++ ) {
let c = rows [ i ] ;
2017-10-30 02:29:10 +02:00
if ( ! this . keyExists ( c . key ) ) continue ;
//if (ignore.indexOf(c.key) >= 0) continue;
2017-07-26 19:49:01 +02:00
// console.info(c.key + ' = ' + c.value);
2017-07-25 23:55:26 +02:00
c . value = this . formatValue ( c . key , c . value ) ;
2017-07-26 20:36:16 +02:00
this . cache _ . push ( c ) ;
2017-07-24 22:36:49 +02:00
}
2017-07-26 19:49:01 +02:00
2017-10-21 18:53:43 +02:00
this . dispatchUpdateAll ( ) ;
} ) ;
}
static dispatchUpdateAll ( ) {
const keys = this . keys ( ) ;
let keyToValues = { } ;
for ( let i = 0 ; i < keys . length ; i ++ ) {
keyToValues [ keys [ i ] ] = this . value ( keys [ i ] ) ;
}
2017-07-26 19:49:01 +02:00
2017-10-21 18:53:43 +02:00
this . dispatch ( {
type : 'SETTINGS_UPDATE_ALL' ,
settings : keyToValues ,
2017-05-12 22:17:23 +02:00
} ) ;
}
2017-06-24 20:51:43 +02:00
static setConstant ( key , value ) {
2017-07-06 23:30:45 +02:00
if ( ! ( key in this . constants _ ) ) throw new Error ( 'Unknown constant key: ' + key ) ;
2017-06-24 20:51:43 +02:00
this . constants _ [ key ] = value ;
}
2017-05-12 22:17:23 +02:00
static setValue ( key , value ) {
2017-06-19 20:58:49 +02:00
if ( ! this . cache _ ) throw new Error ( 'Settings have not been initialized!' ) ;
2017-07-31 22:51:24 +02:00
value = this . formatValue ( key , value ) ;
2017-06-19 20:58:49 +02:00
2017-05-12 22:17:23 +02:00
for ( let i = 0 ; i < this . cache _ . length ; i ++ ) {
2017-07-24 20:58:11 +02:00
let c = this . cache _ [ i ] ;
if ( c . key == key ) {
const md = this . settingMetadata ( key ) ;
2017-07-25 23:55:26 +02:00
if ( md . isEnum === true ) {
2017-07-24 20:58:11 +02:00
if ( ! this . isAllowedEnumOption ( key , value ) ) {
throw new Error ( _ ( 'Invalid option value: "%s". Possible values are: %s.' , value , this . enumOptionsDoc ( key ) ) ) ;
}
}
if ( c . value === value ) return ;
2017-07-25 23:55:26 +02:00
2017-07-31 22:51:24 +02:00
this . logger ( ) . info ( 'Setting: ' + key + ' = ' + c . value + ' => ' + value ) ;
2017-07-25 23:55:26 +02:00
c . value = this . formatValue ( key , value ) ;
2017-07-26 19:49:01 +02:00
this . dispatch ( {
type : 'SETTINGS_UPDATE_ONE' ,
key : key ,
value : c . value ,
} ) ;
this . scheduleSave ( ) ;
2017-05-12 22:17:23 +02:00
return ;
}
}
2017-07-25 23:55:26 +02:00
this . cache _ . push ( {
key : key ,
value : this . formatValue ( key , value ) ,
} ) ;
2017-07-26 19:49:01 +02:00
this . dispatch ( {
type : 'SETTINGS_UPDATE_ONE' ,
key : key ,
value : this . formatValue ( key , value ) ,
} ) ;
this . scheduleSave ( ) ;
}
static valueToString ( key , value ) {
const md = this . settingMetadata ( key ) ;
value = this . formatValue ( key , value ) ;
if ( md . type == Setting . TYPE _INT ) return value . toFixed ( 0 ) ;
if ( md . type == Setting . TYPE _BOOL ) return value ? '1' : '0' ;
return value ;
2017-05-12 22:17:23 +02:00
}
2017-07-25 23:55:26 +02:00
static formatValue ( key , value ) {
const md = this . settingMetadata ( key ) ;
if ( md . type == Setting . TYPE _INT ) return Math . floor ( Number ( value ) ) ;
2017-07-26 19:49:01 +02:00
if ( md . type == Setting . TYPE _BOOL ) {
2017-07-26 23:07:27 +02:00
if ( typeof value === 'string' ) {
value = value . toLowerCase ( ) ;
if ( value === 'true' ) return true ;
if ( value === 'false' ) return false ;
value = Number ( value ) ;
}
2017-07-26 19:49:01 +02:00
return ! ! value ;
}
2017-07-25 20:57:06 +02:00
return value ;
}
2017-05-12 22:17:23 +02:00
static value ( key ) {
2017-07-07 00:15:31 +02:00
if ( key in this . constants _ ) {
let output = this . constants _ [ key ] ;
if ( output == 'SET_ME' ) throw new Error ( 'Setting constant has not been set: ' + key ) ;
return output ;
}
2017-06-24 20:51:43 +02:00
2017-06-19 20:58:49 +02:00
if ( ! this . cache _ ) throw new Error ( 'Settings have not been initialized!' ) ;
2017-05-12 22:17:23 +02:00
for ( let i = 0 ; i < this . cache _ . length ; i ++ ) {
if ( this . cache _ [ i ] . key == key ) {
2017-07-25 23:55:26 +02:00
return this . cache _ [ i ] . value ;
2017-05-12 22:17:23 +02:00
}
}
2017-07-25 23:55:26 +02:00
const md = this . settingMetadata ( key ) ;
return md . value ;
2017-05-12 22:17:23 +02:00
}
2017-07-24 20:58:11 +02:00
static isEnum ( key ) {
const md = this . settingMetadata ( key ) ;
2017-07-25 23:55:26 +02:00
return md . isEnum === true ;
2017-07-24 20:58:11 +02:00
}
static enumOptionValues ( key ) {
const options = this . enumOptions ( key ) ;
let output = [ ] ;
for ( let n in options ) {
if ( ! options . hasOwnProperty ( n ) ) continue ;
output . push ( n ) ;
}
return output ;
}
static enumOptionLabel ( key , value ) {
const options = this . enumOptions ( key ) ;
for ( let n in options ) {
if ( n == value ) return options [ n ] ;
}
return '' ;
}
static enumOptions ( key ) {
if ( ! this . metadata _ [ key ] ) throw new Error ( 'Unknown key: ' + key ) ;
if ( ! this . metadata _ [ key ] . options ) throw new Error ( 'No options for: ' + key ) ;
return this . metadata _ [ key ] . options ( ) ;
}
2017-10-20 00:02:13 +02:00
static enumOptionsDoc ( key , templateString = null ) {
if ( templateString === null ) templateString = '%s: %s' ;
2017-07-24 20:58:11 +02:00
const options = this . enumOptions ( key ) ;
let output = [ ] ;
for ( let n in options ) {
if ( ! options . hasOwnProperty ( n ) ) continue ;
2017-10-20 00:02:13 +02:00
output . push ( sprintf ( templateString , n , options [ n ] ) ) ;
2017-07-24 20:58:11 +02:00
}
return output . join ( ', ' ) ;
}
static isAllowedEnumOption ( key , value ) {
const options = this . enumOptions ( key ) ;
return ! ! options [ value ] ;
}
2017-05-16 23:46:21 +02:00
// Currently only supports objects with properties one level deep
static object ( key ) {
let output = { } ;
let keys = this . keys ( ) ;
for ( let i = 0 ; i < keys . length ; i ++ ) {
let k = keys [ i ] . split ( '.' ) ;
if ( k [ 0 ] == key ) {
output [ k [ 1 ] ] = this . value ( keys [ i ] ) ;
}
}
return output ;
}
// Currently only supports objects with properties one level deep
static setObject ( key , object ) {
for ( let n in object ) {
if ( ! object . hasOwnProperty ( n ) ) continue ;
this . setValue ( key + '.' + n , object [ n ] ) ;
}
}
2017-05-19 21:12:09 +02:00
static saveAll ( ) {
2017-07-26 19:49:01 +02:00
if ( ! this . saveTimeoutId _ ) return Promise . resolve ( ) ;
2017-05-19 21:12:09 +02:00
2017-06-25 12:41:03 +02:00
this . logger ( ) . info ( 'Saving settings...' ) ;
2017-07-26 19:49:01 +02:00
clearTimeout ( this . saveTimeoutId _ ) ;
this . saveTimeoutId _ = null ;
2017-05-19 21:12:09 +02:00
2017-06-11 23:11:14 +02:00
let queries = [ ] ;
queries . push ( 'DELETE FROM settings' ) ;
for ( let i = 0 ; i < this . cache _ . length ; i ++ ) {
2017-07-26 19:49:01 +02:00
let s = Object . assign ( { } , this . cache _ [ i ] ) ;
s . value = this . valueToString ( s . key , s . value ) ;
queries . push ( Database . insertQuery ( this . tableName ( ) , s ) ) ;
2017-06-11 23:11:14 +02:00
}
return BaseModel . db ( ) . transactionExecBatch ( queries ) . then ( ( ) => {
2017-06-25 12:41:03 +02:00
this . logger ( ) . info ( 'Settings have been saved.' ) ;
2017-05-19 21:12:09 +02:00
} ) ;
}
2017-07-26 19:49:01 +02:00
static scheduleSave ( ) {
if ( this . saveTimeoutId _ ) clearTimeout ( this . saveTimeoutId _ ) ;
2017-05-19 21:12:09 +02:00
2017-07-26 19:49:01 +02:00
this . saveTimeoutId _ = setTimeout ( ( ) => {
2017-05-19 21:12:09 +02:00
this . saveAll ( ) ;
2017-05-12 22:17:23 +02:00
} , 500 ) ;
}
2017-07-26 19:49:01 +02:00
static cancelScheduleSave ( ) {
if ( this . saveTimeoutId _ ) clearTimeout ( this . saveTimeoutId _ ) ;
this . saveTimeoutId _ = null ;
2017-06-19 21:26:27 +02:00
}
2017-07-23 20:26:50 +02:00
static publicSettings ( appType ) {
if ( ! appType ) throw new Error ( 'appType is required' ) ;
let output = { } ;
2017-07-24 20:58:11 +02:00
for ( let key in Setting . metadata _ ) {
if ( ! Setting . metadata _ . hasOwnProperty ( key ) ) continue ;
let s = Object . assign ( { } , Setting . metadata _ [ key ] ) ;
2017-07-23 20:26:50 +02:00
if ( ! s . public ) continue ;
if ( s . appTypes && s . appTypes . indexOf ( appType ) < 0 ) continue ;
s . value = this . value ( key ) ;
output [ key ] = s ;
}
return output ;
}
2017-10-20 00:02:13 +02:00
static typeToString ( typeId ) {
if ( typeId === Setting . TYPE _INT ) return 'int' ;
if ( typeId === Setting . TYPE _STRING ) return 'string' ;
if ( typeId === Setting . TYPE _BOOL ) return 'bool' ;
}
2017-05-12 22:17:23 +02:00
}
2017-07-24 20:58:11 +02:00
Setting . SYNC _TARGET _MEMORY = 1 ;
Setting . SYNC _TARGET _FILESYSTEM = 2 ;
Setting . SYNC _TARGET _ONEDRIVE = 3 ;
2017-07-25 23:55:26 +02:00
Setting . TYPE _INT = 1 ;
Setting . TYPE _STRING = 2 ;
Setting . TYPE _BOOL = 3 ;
2017-07-31 22:51:24 +02:00
Setting . THEME _LIGHT = 1 ;
Setting . THEME _DARK = 2 ;
2017-07-24 20:58:11 +02:00
Setting . metadata _ = {
2017-07-25 23:55:26 +02:00
'activeFolderId' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'firstStart' : { value : true , type : Setting . TYPE _BOOL , public : false } ,
2017-10-24 22:22:57 +02:00
'sync.2.path' : { value : '' , type : Setting . TYPE _STRING , public : true , appTypes : [ 'cli' ] , label : ( ) => _ ( 'File system synchronisation target directory' ) , description : ( ) => _ ( 'The path to synchronise with when file system synchronisation is enabled. See `sync.target`.' ) } ,
2017-07-25 23:55:26 +02:00
'sync.3.auth' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2017-10-24 22:22:57 +02:00
'sync.target' : { value : Setting . SYNC _TARGET _ONEDRIVE , type : Setting . TYPE _INT , isEnum : true , public : true , label : ( ) => _ ( 'Synchronisation target' ) , description : ( ) => _ ( 'The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.' ) , options : ( ) => {
2017-07-24 20:58:11 +02:00
let output = { } ;
output [ Setting . SYNC _TARGET _MEMORY ] = 'Memory' ;
output [ Setting . SYNC _TARGET _FILESYSTEM ] = _ ( 'File system' ) ;
output [ Setting . SYNC _TARGET _ONEDRIVE ] = _ ( 'OneDrive' ) ;
return output ;
} } ,
2017-08-19 22:56:28 +02:00
'sync.1.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.2.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.3.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.4.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.5.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.6.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2017-10-24 22:22:57 +02:00
'editor' : { value : '' , type : Setting . TYPE _STRING , public : true , appTypes : [ 'cli' ] , label : ( ) => _ ( 'Text editor' ) , description : ( ) => _ ( 'The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.' ) } ,
2017-07-25 23:55:26 +02:00
'locale' : { value : defaultLocale ( ) , type : Setting . TYPE _STRING , isEnum : true , public : true , label : ( ) => _ ( 'Language' ) , options : ( ) => {
2017-07-24 23:29:40 +02:00
return supportedLocalesToLanguages ( ) ;
} } ,
2017-08-20 22:11:32 +02:00
// 'logLevel': { value: Logger.LEVEL_INFO, type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Log level'), options: () => {
// return Logger.levelEnum();
// }},
2017-07-28 19:57:01 +02:00
// Not used for now:
2017-10-24 22:22:57 +02:00
// 'todoFilter': { value: 'all', type: Setting.TYPE_STRING, isEnum: true, public: false, appTypes: ['mobile'], label: () => _('Todo filter'), options: () => ({
// all: _('Show all'),
// recent: _('Non-completed and recently completed ones'),
// nonCompleted: _('Non-completed ones only'),
// })},
2017-07-26 20:36:16 +02:00
'uncompletedTodosOnTop' : { value : true , type : Setting . TYPE _BOOL , public : true , label : ( ) => _ ( 'Show uncompleted todos on top of the lists' ) } ,
2017-09-24 16:48:23 +02:00
'showAdvancedOptions' : { value : false , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'mobile' ] , label : ( ) => _ ( 'Show advanced options' ) } ,
2017-10-24 22:22:57 +02:00
'trackLocation' : { value : true , type : Setting . TYPE _BOOL , public : true , label : ( ) => _ ( 'Save geo-location with notes' ) } ,
2017-10-20 00:02:13 +02:00
'sync.interval' : { value : 300 , type : Setting . TYPE _INT , isEnum : true , public : true , label : ( ) => _ ( 'Synchronisation interval' ) , options : ( ) => {
2017-07-26 19:49:01 +02:00
return {
2017-08-20 16:29:18 +02:00
0 : _ ( 'Disabled' ) ,
2017-07-26 19:49:01 +02:00
300 : _ ( '%d minutes' , 5 ) ,
600 : _ ( '%d minutes' , 10 ) ,
1800 : _ ( '%d minutes' , 30 ) ,
3600 : _ ( '%d hour' , 1 ) ,
2017-07-31 21:02:21 +02:00
43200 : _ ( '%d hours' , 12 ) ,
2017-07-26 19:49:01 +02:00
86400 : _ ( '%d hours' , 24 ) ,
} ;
} } ,
2017-07-31 22:51:24 +02:00
'theme' : { value : Setting . THEME _LIGHT , type : Setting . TYPE _INT , public : true , appTypes : [ 'mobile' ] , isEnum : true , label : ( ) => _ ( 'Theme' ) , options : ( ) => {
let output = { } ;
output [ Setting . THEME _LIGHT ] = _ ( 'Light' ) ;
output [ Setting . THEME _DARK ] = _ ( 'Dark' ) ;
return output ;
} } ,
2017-06-06 22:01:43 +02:00
} ;
2017-06-24 20:51:43 +02:00
// Contains constants that are set by the application and
// cannot be modified by the user:
Setting . constants _ = {
2017-10-30 23:29:36 +02:00
env : 'SET_ME' ,
isDemo : false ,
appName : 'joplin' ,
appId : 'SET_ME' , // Each app should set this identifier
appType : 'SET_ME' , // 'cli' or 'mobile'
resourceDir : '' ,
profileDir : '' ,
tempDir : '' ,
2017-06-24 20:51:43 +02:00
}
2017-05-12 22:17:23 +02:00
export { Setting } ;