2017-12-14 20:12:14 +02:00
const BaseModel = require ( 'lib/BaseModel.js' ) ;
2017-11-03 02:09:34 +02:00
const { Database } = require ( 'lib/database.js' ) ;
const { Logger } = require ( 'lib/logger.js' ) ;
2017-11-24 21:21:30 +02:00
const SyncTargetRegistry = require ( 'lib/SyncTargetRegistry.js' ) ;
2017-11-28 20:02:54 +02:00
const { time } = require ( 'lib/time-utils.js' ) ;
2017-11-03 02:09:34 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2018-02-21 21:58:28 +02:00
const ObjectUtils = require ( 'lib/ObjectUtils' ) ;
2018-02-22 20:58:15 +02:00
const { toTitleCase } = require ( 'lib/string-utils.js' ) ;
2018-05-08 12:29:25 +02:00
const { rtrimSlashes } = require ( 'lib/path-utils.js' ) ;
2017-11-03 02:09:34 +02:00
const { _ , supportedLocalesToLanguages , defaultLocale } = require ( 'lib/locale.js' ) ;
2018-04-16 19:18:28 +02:00
const { shim } = require ( 'lib/shim' ) ;
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-11-24 21:21:30 +02:00
static metadata ( ) {
if ( this . metadata _ ) return this . metadata _ ;
2018-04-16 19:18:28 +02:00
const platform = shim . platformName ( ) ;
2018-02-22 20:58:15 +02:00
// A "public" setting means that it will show up in the various config screens (or config command for the CLI tool), however
// if if private a setting might still be handled and modified by the app. For instance, the settings related to sorting notes are not
// public for the mobile and desktop apps because they are handled separately in menus.
2017-11-24 21:21:30 +02:00
this . metadata _ = {
'activeFolderId' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'firstStart' : { value : true , type : Setting . TYPE _BOOL , public : false } ,
'locale' : { value : defaultLocale ( ) , type : Setting . TYPE _STRING , isEnum : true , public : true , label : ( ) => _ ( 'Language' ) , options : ( ) => {
2018-02-21 21:58:28 +02:00
return ObjectUtils . sortByValue ( supportedLocalesToLanguages ( ) ) ;
2017-11-24 21:21:30 +02:00
} } ,
2017-11-28 20:02:54 +02:00
'dateFormat' : { value : Setting . DATE _FORMAT _1 , type : Setting . TYPE _STRING , isEnum : true , public : true , label : ( ) => _ ( 'Date format' ) , options : ( ) => {
let options = { }
const now = ( new Date ( '2017-01-30T12:00:00' ) ) . getTime ( ) ;
options [ Setting . DATE _FORMAT _1 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _1 ) ;
options [ Setting . DATE _FORMAT _2 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _2 ) ;
options [ Setting . DATE _FORMAT _3 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _3 ) ;
options [ Setting . DATE _FORMAT _4 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _4 ) ;
options [ Setting . DATE _FORMAT _5 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _5 ) ;
2018-04-19 17:03:16 +02:00
options [ Setting . DATE _FORMAT _6 ] = time . formatMsToLocal ( now , Setting . DATE _FORMAT _6 ) ;
2017-11-28 20:02:54 +02:00
return options ;
} } ,
'timeFormat' : { value : Setting . TIME _FORMAT _1 , type : Setting . TYPE _STRING , isEnum : true , public : true , label : ( ) => _ ( 'Time format' ) , options : ( ) => {
let options = { }
const now = ( new Date ( '2017-01-30T20:30:00' ) ) . getTime ( ) ;
options [ Setting . TIME _FORMAT _1 ] = time . formatMsToLocal ( now , Setting . TIME _FORMAT _1 ) ;
options [ Setting . TIME _FORMAT _2 ] = time . formatMsToLocal ( now , Setting . TIME _FORMAT _2 ) ;
return options ;
} } ,
2018-11-08 00:37:13 +02:00
'theme' : { value : Setting . THEME _LIGHT , type : Setting . TYPE _INT , public : true , appTypes : [ 'mobile' , 'desktop' ] , isEnum : true , label : ( ) => _ ( 'Theme' ) , options : ( ) => {
2017-11-24 21:21:30 +02:00
let output = { } ;
output [ Setting . THEME _LIGHT ] = _ ( 'Light' ) ;
output [ Setting . THEME _DARK ] = _ ( 'Dark' ) ;
return output ;
} } ,
2018-02-22 20:58:15 +02:00
'uncompletedTodosOnTop' : { value : true , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'cli' ] , label : ( ) => _ ( 'Uncompleted to-dos on top' ) } ,
2018-05-09 22:00:05 +02:00
'showCompletedTodos' : { value : true , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'cli' ] , label : ( ) => _ ( 'Show completed to-dos' ) } ,
2018-02-22 20:58:15 +02:00
'notes.sortOrder.field' : { value : 'user_updated_time' , type : Setting . TYPE _STRING , isEnum : true , public : true , appTypes : [ 'cli' ] , label : ( ) => _ ( 'Sort notes by' ) , options : ( ) => {
const Note = require ( 'lib/models/Note' ) ;
const noteSortFields = [ 'user_updated_time' , 'user_created_time' , 'title' ] ;
const options = { } ;
for ( let i = 0 ; i < noteSortFields . length ; i ++ ) {
options [ noteSortFields [ i ] ] = toTitleCase ( Note . fieldToLabel ( noteSortFields [ i ] ) ) ;
}
return options ;
} } ,
'notes.sortOrder.reverse' : { value : true , type : Setting . TYPE _BOOL , public : true , label : ( ) => _ ( 'Reverse sort order' ) , appTypes : [ 'cli' ] } ,
2017-11-24 21:21:30 +02:00
'trackLocation' : { value : true , type : Setting . TYPE _BOOL , public : true , label : ( ) => _ ( 'Save geo-location with notes' ) } ,
2018-01-30 23:49:22 +02:00
'newTodoFocus' : { value : 'title' , type : Setting . TYPE _STRING , isEnum : true , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'When creating a new to-do:' ) , options : ( ) => {
return {
'title' : _ ( 'Focus title' ) ,
'body' : _ ( 'Focus body' ) ,
} ;
} } ,
'newNoteFocus' : { value : 'body' , type : Setting . TYPE _STRING , isEnum : true , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'When creating a new note:' ) , options : ( ) => {
return {
'title' : _ ( 'Focus title' ) ,
'body' : _ ( 'Focus body' ) ,
} ;
} } ,
2018-04-16 19:18:28 +02:00
// Tray icon (called AppIndicator) doesn't work in Ubuntu
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
// Might be fixed in Electron 18.x but no non-beta release yet. So for now
// by default we disable it on Linux.
'showTrayIcon' : { value : platform !== 'linux' , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'Show tray icon' ) , description : ( ) => {
2018-10-24 19:47:04 +02:00
return platform === 'linux' ? _ ( 'Note: Does not work in all desktop environments.' ) : _ ( 'This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.' ) ;
2018-04-16 19:18:28 +02:00
} } ,
2018-05-09 11:49:31 +02:00
2018-09-06 19:56:23 +02:00
'startMinimized' : { value : false , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'Start application minimised in the tray icon' ) } ,
2018-05-09 11:49:31 +02:00
'collapsedFolderIds' : { value : [ ] , type : Setting . TYPE _ARRAY , public : false } ,
2018-04-16 19:18:28 +02:00
2017-12-12 23:58:57 +02:00
'encryption.enabled' : { value : false , type : Setting . TYPE _BOOL , public : false } ,
2017-12-14 02:23:32 +02:00
'encryption.activeMasterKeyId' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2018-11-02 21:22:49 +02:00
'encryption.passwordCache' : { value : { } , type : Setting . TYPE _OBJECT , public : false , secure : true } ,
2018-03-02 20:16:48 +02:00
'style.zoom' : { value : "100" , type : Setting . TYPE _INT , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'Global zoom percentage' ) , minimum : "50" , maximum : "500" , step : "10" } ,
2018-05-20 13:20:15 +02:00
'style.editor.fontFamily' : { value : "" , type : Setting . TYPE _STRING , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'Editor font family' ) , description : ( ) => _ ( 'This must be *monospace* font or it will not work properly. If the font is incorrect or empty, it will default to a generic monospace font.' ) } ,
2018-01-23 20:31:49 +02:00
'autoUpdateEnabled' : { value : true , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'desktop' ] , label : ( ) => _ ( 'Automatically update the application' ) } ,
2018-05-25 14:30:27 +02:00
'clipperServer.autoStart' : { value : false , type : Setting . TYPE _BOOL , public : false } ,
2017-11-24 21:21:30 +02:00
'sync.interval' : { value : 300 , type : Setting . TYPE _INT , isEnum : true , public : true , label : ( ) => _ ( 'Synchronisation interval' ) , options : ( ) => {
return {
0 : _ ( 'Disabled' ) ,
300 : _ ( '%d minutes' , 5 ) ,
600 : _ ( '%d minutes' , 10 ) ,
1800 : _ ( '%d minutes' , 30 ) ,
3600 : _ ( '%d hour' , 1 ) ,
43200 : _ ( '%d hours' , 12 ) ,
86400 : _ ( '%d hours' , 24 ) ,
} ;
} } ,
'noteVisiblePanes' : { value : [ 'editor' , 'viewer' ] , type : Setting . TYPE _ARRAY , public : false , appTypes : [ 'desktop' ] } ,
2018-04-15 17:50:39 +02:00
'sidebarVisibility' : { value : true , type : Setting . TYPE _BOOL , public : false , appTypes : [ 'desktop' ] } ,
2018-06-27 22:34:41 +02:00
'editor' : { value : '' , type : Setting . TYPE _STRING , public : true , appTypes : [ 'cli' , 'desktop' ] , label : ( ) => _ ( 'Text editor command' ) , description : ( ) => _ ( 'The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.' ) } ,
2017-11-24 21:21:30 +02:00
'showAdvancedOptions' : { value : false , type : Setting . TYPE _BOOL , public : true , appTypes : [ 'mobile' ] , label : ( ) => _ ( 'Show advanced options' ) } ,
2018-03-26 19:33:55 +02:00
'sync.target' : { value : SyncTargetRegistry . nameToId ( 'dropbox' ) , type : Setting . TYPE _INT , isEnum : true , public : true , label : ( ) => _ ( 'Synchronisation target' ) , description : ( appType ) => { return appType !== 'cli' ? null : _ ( 'The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).' ) } , options : ( ) => {
2017-11-24 21:47:24 +02:00
return SyncTargetRegistry . idAndLabelPlainObject ( ) ;
} } ,
2018-01-25 21:01:14 +02:00
'sync.2.path' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => {
try {
return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'filesystem' )
} catch ( error ) {
return false ;
}
2018-05-08 12:29:25 +02:00
} , filter : ( value ) => {
return value ? rtrimSlashes ( value ) : '' ;
2018-03-02 20:16:48 +02:00
} , public : true , label : ( ) => _ ( 'Directory to synchronise with (absolute path)' ) , description : ( appType ) => { return appType !== 'cli' ? null : _ ( 'The path to synchronise with when file system synchronisation is enabled. See `sync.target`.' ) ; } } ,
2018-01-25 21:01:14 +02:00
2018-09-05 12:20:26 +02:00
'sync.5.path' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'nextcloud' ) } , public : true , label : ( ) => _ ( 'Nextcloud WebDAV URL' ) , description : ( ) => _ ( 'Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: %s' , 'https://joplin.cozic.net/faq/' ) } ,
2018-02-17 02:09:00 +02:00
'sync.5.username' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'nextcloud' ) } , public : true , label : ( ) => _ ( 'Nextcloud username' ) } ,
'sync.5.password' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'nextcloud' ) } , public : true , label : ( ) => _ ( 'Nextcloud password' ) , secure : true } ,
2018-02-02 01:40:05 +02:00
2018-09-05 12:20:26 +02:00
'sync.6.path' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'webdav' ) } , public : true , label : ( ) => _ ( 'WebDAV URL' ) , description : ( ) => _ ( 'Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: %s' , 'https://joplin.cozic.net/faq/' ) } ,
2018-02-02 01:40:05 +02:00
'sync.6.username' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'webdav' ) } , public : true , label : ( ) => _ ( 'WebDAV username' ) } ,
'sync.6.password' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return settings [ 'sync.target' ] == SyncTargetRegistry . nameToId ( 'webdav' ) } , public : true , label : ( ) => _ ( 'WebDAV password' ) , secure : true } ,
2017-12-08 00:29:02 +02:00
'sync.3.auth' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
'sync.4.auth' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2018-03-26 19:33:55 +02:00
'sync.7.auth' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2017-12-08 00:29:02 +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 } ,
2018-03-26 19:33:55 +02:00
'sync.7.context' : { value : '' , type : Setting . TYPE _STRING , public : false } ,
2018-06-20 01:28:50 +02:00
2018-06-26 00:54:28 +02:00
'net.customCertificates' : { value : '' , type : Setting . TYPE _STRING , show : ( settings ) => { return [ SyncTargetRegistry . nameToId ( 'nextcloud' ) , SyncTargetRegistry . nameToId ( 'webdav' ) ] . indexOf ( settings [ 'sync.target' ] ) >= 0 } , public : true , appTypes : [ 'desktop' , 'cli' ] , label : ( ) => _ ( 'Custom TLS certificates' ) , description : ( ) => _ ( 'Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on "Check synchronisation configuration".' ) } ,
2018-06-20 01:28:50 +02:00
'net.ignoreTlsErrors' : { value : false , type : Setting . TYPE _BOOL , show : ( settings ) => { return [ SyncTargetRegistry . nameToId ( 'nextcloud' ) , SyncTargetRegistry . nameToId ( 'webdav' ) ] . indexOf ( settings [ 'sync.target' ] ) >= 0 } , public : true , appTypes : [ 'desktop' , 'cli' ] , label : ( ) => _ ( 'Ignore TLS certificate errors' ) } ,
2018-09-28 20:24:57 +02:00
'api.token' : { value : null , type : Setting . TYPE _STRING , public : false } ,
2017-11-24 21:21:30 +02:00
} ;
return this . metadata _ ;
}
2017-07-24 20:58:11 +02:00
static settingMetadata ( key ) {
2017-11-24 21:21:30 +02:00
const metadata = this . metadata ( ) ;
if ( ! ( key in metadata ) ) throw new Error ( 'Unknown key: ' + key ) ;
let output = Object . assign ( { } , 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 ) {
2017-11-24 21:21:30 +02:00
return key in this . metadata ( ) ;
2017-10-30 02:29:10 +02:00
}
2018-03-02 20:16:48 +02:00
static keyDescription ( key , appType = null ) {
const md = this . settingMetadata ( key ) ;
if ( ! md . description ) return null ;
return md . description ( appType ) ;
}
2017-08-22 19:57:35 +02:00
static keys ( publicOnly = false , appType = null ) {
if ( ! this . keys _ ) {
2017-11-24 21:21:30 +02:00
const metadata = this . metadata ( ) ;
2017-08-22 19:57:35 +02:00
this . keys _ = [ ] ;
2017-11-24 21:21:30 +02:00
for ( let n in metadata ) {
if ( ! metadata . hasOwnProperty ( n ) ) continue ;
2017-08-22 19:57:35 +02:00
this . keys _ . push ( n ) ;
}
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
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 ;
2017-07-25 23:55:26 +02:00
c . value = this . formatValue ( c . key , c . value ) ;
2018-05-08 12:29:25 +02:00
c . value = this . filterValue ( c . key , c . value ) ;
2017-07-25 23:55:26 +02:00
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 ( ) ;
} ) ;
}
2017-11-24 01:10:55 +02:00
static toPlainObject ( ) {
2017-10-21 18:53:43 +02:00
const keys = this . keys ( ) ;
let keyToValues = { } ;
for ( let i = 0 ; i < keys . length ; i ++ ) {
keyToValues [ keys [ i ] ] = this . value ( keys [ i ] ) ;
}
2017-11-24 01:10:55 +02:00
return keyToValues ;
}
2017-07-26 19:49:01 +02:00
2017-11-24 01:10:55 +02:00
static dispatchUpdateAll ( ) {
2017-10-21 18:53:43 +02:00
this . dispatch ( {
2017-11-08 23:22:24 +02:00
type : 'SETTING_UPDATE_ALL' ,
2017-11-24 01:10:55 +02:00
settings : this . toPlainObject ( ) ,
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 ) ;
2018-05-08 12:29:25 +02:00
value = this . filterValue ( key , value ) ;
2018-04-15 17:50:39 +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
2018-01-17 23:17:40 +02:00
// Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs
// this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
2017-07-25 23:55:26 +02:00
2017-11-14 20:02:58 +02:00
c . value = value ;
2017-07-26 19:49:01 +02:00
this . dispatch ( {
2017-11-08 23:22:24 +02:00
type : 'SETTING_UPDATE_ONE' ,
2017-07-26 19:49:01 +02:00
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 ( {
2017-11-08 23:22:24 +02:00
type : 'SETTING_UPDATE_ONE' ,
2017-07-26 19:49:01 +02:00
key : key ,
value : this . formatValue ( key , value ) ,
} ) ;
this . scheduleSave ( ) ;
}
2017-12-17 21:51:45 +02:00
static setObjectKey ( settingKey , objectKey , value ) {
2017-12-28 21:57:21 +02:00
let o = this . value ( settingKey ) ;
2017-12-17 21:51:45 +02:00
if ( typeof o !== 'object' ) o = { } ;
o [ objectKey ] = value ;
this . setValue ( settingKey , o ) ;
}
static deleteObjectKey ( settingKey , objectKey ) {
const o = this . value ( settingKey ) ;
if ( typeof o !== 'object' ) return ;
delete o [ objectKey ] ;
this . setValue ( settingKey , o ) ;
}
2017-07-26 19:49:01 +02:00
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' ;
2017-11-12 19:02:20 +02:00
if ( md . type == Setting . TYPE _ARRAY ) return value ? JSON . stringify ( value ) : '[]' ;
2017-11-14 20:02:58 +02:00
if ( md . type == Setting . TYPE _OBJECT ) return value ? JSON . stringify ( value ) : '{}' ;
2018-01-07 21:20:10 +02:00
if ( md . type == Setting . TYPE _STRING ) return value ? value + '' : '' ;
2018-04-15 17:50:39 +02:00
throw new Error ( 'Unhandled value type: ' + md . type ) ;
2017-05-12 22:17:23 +02:00
}
2018-05-08 12:29:25 +02:00
static filterValue ( key , value ) {
const md = this . settingMetadata ( key ) ;
return md . filter ? md . filter ( value ) : value ;
}
2017-07-25 23:55:26 +02:00
static formatValue ( key , value ) {
const md = this . settingMetadata ( key ) ;
2017-11-12 19:02:20 +02:00
2018-01-07 21:29:57 +02:00
if ( md . type == Setting . TYPE _INT ) return ! value ? 0 : Math . floor ( Number ( value ) ) ;
2017-11-12 19:02:20 +02:00
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-11-12 19:02:20 +02:00
if ( md . type === Setting . TYPE _ARRAY ) {
2017-11-14 20:02:58 +02:00
if ( ! value ) return [ ] ;
2017-11-12 19:02:20 +02:00
if ( Array . isArray ( value ) ) return value ;
if ( typeof value === 'string' ) return JSON . parse ( value ) ;
return [ ] ;
}
2017-11-14 20:02:58 +02:00
if ( md . type === Setting . TYPE _OBJECT ) {
if ( ! value ) return { } ;
if ( typeof value === 'object' ) return value ;
if ( typeof value === 'string' ) return JSON . parse ( value ) ;
return { } ;
}
2018-01-07 21:20:10 +02:00
if ( md . type === Setting . TYPE _STRING ) {
if ( ! value ) return '' ;
return value + '' ;
}
throw new Error ( 'Unhandled value type: ' + md . type ) ;
2017-07-25 20:57:06 +02:00
}
2017-05-12 22:17:23 +02:00
static value ( key ) {
2017-12-14 21:39:13 +02:00
// Need to copy arrays and objects since in setValue(), the old value and new one is compared
// with strict equality and the value is updated only if changed. However if the caller acquire
// and object and change a key, the objects will be detected as equal. By returning a copy
// we avoid this problem.
function copyIfNeeded ( value ) {
2018-01-07 21:20:10 +02:00
if ( value === null || value === undefined ) return value ;
2017-12-14 21:39:13 +02:00
if ( Array . isArray ( value ) ) return value . slice ( ) ;
if ( typeof value === 'object' ) return Object . assign ( { } , value ) ;
return value ;
}
2017-07-07 00:15:31 +02:00
if ( key in this . constants _ ) {
2017-11-20 00:08:58 +02:00
const v = this . constants _ [ key ] ;
const output = typeof v === 'function' ? v ( ) : v ;
2017-07-07 00:15:31 +02:00
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-12-14 21:39:13 +02:00
return copyIfNeeded ( 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 ) ;
2017-12-14 21:39:13 +02:00
return copyIfNeeded ( 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 ) {
2017-11-24 21:21:30 +02:00
const metadata = this . metadata ( ) ;
if ( ! metadata [ key ] ) throw new Error ( 'Unknown key: ' + key ) ;
if ( ! metadata [ key ] . options ) throw new Error ( 'No options for: ' + key ) ;
return metadata [ key ] . options ( ) ;
2017-07-24 20:58:11 +02:00
}
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 ] ;
}
2018-02-06 20:59:36 +02:00
// For example, if settings is:
// { sync.5.path: 'http://example', sync.5.username: 'testing' }
// and baseKey is 'sync.5', the function will return
// { path: 'http://example', username: 'testing' }
static subValues ( baseKey , settings ) {
2017-05-16 23:46:21 +02:00
let output = { } ;
2018-02-06 20:59:36 +02:00
for ( let key in settings ) {
if ( ! settings . hasOwnProperty ( key ) ) continue ;
if ( key . indexOf ( baseKey ) === 0 ) {
const subKey = key . substr ( baseKey . length + 1 ) ;
output [ subKey ] = settings [ key ] ;
2017-05-16 23:46:21 +02:00
}
}
return output ;
}
// Currently only supports objects with properties one level deep
2018-02-06 20:59:36 +02:00
// 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-16 23:46:21 +02:00
2018-02-15 19:12:09 +02:00
static async 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
}
2018-02-15 19:12:09 +02:00
await BaseModel . db ( ) . transactionExecBatch ( queries ) ;
2018-04-15 17:50:39 +02:00
2018-02-15 19:12:09 +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 ( ) {
2018-02-15 19:12:09 +02:00
if ( ! Setting . autoSaveEnabled ) return ;
2017-07-26 19:49:01 +02:00
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' ) ;
2017-11-24 21:21:30 +02:00
const metadata = this . metadata ( ) ;
2017-07-23 20:26:50 +02:00
let output = { } ;
2017-11-24 21:21:30 +02:00
for ( let key in metadata ) {
if ( ! metadata . hasOwnProperty ( key ) ) continue ;
let s = Object . assign ( { } , 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-11-12 19:02:20 +02:00
if ( typeId === Setting . TYPE _ARRAY ) return 'array' ;
2017-11-14 20:02:58 +02:00
if ( typeId === Setting . TYPE _OBJECT ) return 'object' ;
2017-10-20 00:02:13 +02:00
}
2017-05-12 22:17:23 +02:00
}
2017-07-25 23:55:26 +02:00
Setting . TYPE _INT = 1 ;
Setting . TYPE _STRING = 2 ;
Setting . TYPE _BOOL = 3 ;
2017-11-12 19:02:20 +02:00
Setting . TYPE _ARRAY = 4 ;
2017-11-14 20:02:58 +02:00
Setting . TYPE _OBJECT = 5 ;
2017-07-25 23:55:26 +02:00
2017-07-31 22:51:24 +02:00
Setting . THEME _LIGHT = 1 ;
Setting . THEME _DARK = 2 ;
2017-11-28 20:02:54 +02:00
Setting . DATE _FORMAT _1 = 'DD/MM/YYYY'
Setting . DATE _FORMAT _2 = 'DD/MM/YY' ;
Setting . DATE _FORMAT _3 = 'MM/DD/YYYY' ;
Setting . DATE _FORMAT _4 = 'MM/DD/YY' ;
Setting . DATE _FORMAT _5 = 'YYYY-MM-DD' ;
2018-04-19 17:03:16 +02:00
Setting . DATE _FORMAT _6 = 'DD.MM.YYYY' ;
2017-11-28 20:02:54 +02:00
Setting . TIME _FORMAT _1 = 'HH:mm' ;
Setting . TIME _FORMAT _2 = 'h:mm A' ;
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-11-17 20:02:01 +02:00
openDevTools : false ,
2017-06-24 20:51:43 +02:00
}
2018-02-15 19:12:09 +02:00
Setting . autoSaveEnabled = true ;
2018-11-08 00:37:13 +02:00
module . exports = Setting ;