2017-12-14 18:12:14 +00:00
const BaseModel = require ( 'lib/BaseModel.js' );
2017-11-03 00:09:34 +00:00
const { Database } = require ( 'lib/database.js' );
const { Logger } = require ( 'lib/logger.js' );
2017-11-24 19:21:30 +00:00
const SyncTargetRegistry = require ( 'lib/SyncTargetRegistry.js' );
2017-11-28 18:02:54 +00:00
const { time } = require ( 'lib/time-utils.js' );
2017-11-03 00:09:34 +00:00
const { sprintf } = require ( 'sprintf-js' );
2018-02-21 19:58:28 +00:00
const ObjectUtils = require ( 'lib/ObjectUtils' );
2018-02-22 18:58:15 +00:00
const { toTitleCase } = require ( 'lib/string-utils.js' );
2018-05-08 11:29:25 +01:00
const { rtrimSlashes } = require ( 'lib/path-utils.js' );
2017-11-03 00:09:34 +00:00
const { _ , supportedLocalesToLanguages , defaultLocale } = require ( 'lib/locale.js' );
2018-04-16 19:18:28 +02:00
const { shim } = require ( 'lib/shim' );
2017-05-12 20:17:23 +00:00
class Setting extends BaseModel {
static tableName () {
return 'settings' ;
}
2017-07-03 20:50:45 +01:00
static modelType () {
return BaseModel . TYPE_SETTING ;
2017-06-19 20:26:27 +01:00
}
2017-11-24 19:21:30 +00:00
static metadata () {
if ( this . metadata_ ) return this . metadata_ ;
2018-04-16 19:18:28 +02:00
const platform = shim . platformName ();
2018-02-22 18:58:15 +00: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 19:21:30 +00: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 19:58:28 +00:00
return ObjectUtils . sortByValue ( supportedLocalesToLanguages ());
2017-11-24 19:21:30 +00:00
}},
2017-11-28 18:02:54 +00: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 18:02:54 +00: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 ;
}},
2017-11-24 19:21:30 +00: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 ;
}},
2018-02-22 18:58:15 +00:00
'uncompletedTodosOnTop' : { value : true , type : Setting . TYPE_BOOL , public : true , appTypes : [ 'cli' ], label : () => _ ( 'Uncompleted to-dos on top' ) },
2018-05-09 21:00:05 +01:00
'showCompletedTodos' : { value : true , type : Setting . TYPE_BOOL , public : true , appTypes : [ 'cli' ], label : () => _ ( 'Show completed to-dos' ) },
2018-02-22 18:58:15 +00: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 19:21:30 +00:00
'trackLocation' : { value : true , type : Setting . TYPE_BOOL , public : true , label : () => _ ( 'Save geo-location with notes' ) },
2018-01-30 21:49:22 +00: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 : () => {
return platform === 'linux' ? _ ( 'Note: Does not work in all desktop environments.' ) : null ;
}},
2018-05-09 10:49:31 +01:00
2018-09-06 18:56:23 +01:00
'startMinimized' : { value : false , type : Setting . TYPE_BOOL , public : true , appTypes : [ 'desktop' ], label : () => _ ( 'Start application minimised in the tray icon' ) },
2018-05-09 10:49:31 +01:00
'collapsedFolderIds' : { value : [], type : Setting . TYPE_ARRAY , public : false },
2018-04-16 19:18:28 +02:00
2017-12-12 21:58:57 +00:00
'encryption.enabled' : { value : false , type : Setting . TYPE_BOOL , public : false },
2017-12-14 00:23:32 +00:00
'encryption.activeMasterKeyId' : { value : '' , type : Setting . TYPE_STRING , public : false },
'encryption.passwordCache' : { value : {}, type : Setting . TYPE_OBJECT , public : false },
2018-03-02 18:16:48 +00: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 12:20:15 +01: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 18:31:49 +00:00
'autoUpdateEnabled' : { value : true , type : Setting . TYPE_BOOL , public : true , appTypes : [ 'desktop' ], label : () => _ ( 'Automatically update the application' ) },
2018-05-25 13:30:27 +01:00
'clipperServer.autoStart' : { value : false , type : Setting . TYPE_BOOL , public : false },
2017-11-24 19:21:30 +00: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 21:20:39 +05:30
'sidebarVisibility' : { value : true , type : Setting . TYPE_BOOL , public : false , appTypes : [ 'desktop' ] },
2018-06-27 21:34:41 +01: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 19:21:30 +00:00
'showAdvancedOptions' : { value : false , type : Setting . TYPE_BOOL , public : true , appTypes : [ 'mobile' ], label : () => _ ( 'Show advanced options' ) },
2018-03-26 18:33:55 +01: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 19:47:24 +00:00
return SyncTargetRegistry . idAndLabelPlainObject ();
}},
2018-01-25 19:01:14 +00: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 11:29:25 +01:00
}, filter : ( value ) => {
return value ? rtrimSlashes ( value ) : '' ;
2018-03-02 18:16:48 +00: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 19:01:14 +00:00
2018-09-05 11:20:26 +01: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 01:09:00 +01: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-01 23:40:05 +00:00
2018-09-05 11:20:26 +01: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-01 23:40:05 +00: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-07 22:29:02 +00:00
'sync.3.auth' : { value : '' , type : Setting . TYPE_STRING , public : false },
'sync.4.auth' : { value : '' , type : Setting . TYPE_STRING , public : false },
2018-03-26 18:33:55 +01:00
'sync.7.auth' : { value : '' , type : Setting . TYPE_STRING , public : false },
2017-12-07 22:29:02 +00: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 18:33:55 +01:00
'sync.7.context' : { value : '' , type : Setting . TYPE_STRING , public : false },
2018-06-20 00:28:50 +01:00
2018-06-25 23:54:28 +01: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 00:28:50 +01: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' ) },
2017-11-24 19:21:30 +00:00
};
return this . metadata_ ;
}
2017-07-24 18:58:11 +00:00
static settingMetadata ( key ) {
2017-11-24 19:21:30 +00:00
const metadata = this . metadata ();
if ( ! ( key in metadata )) throw new Error ( 'Unknown key: ' + key );
let output = Object . assign ({}, metadata [ key ]);
2017-05-12 20:17:23 +00:00
output . key = key ;
return output ;
}
2017-10-30 00:29:10 +00:00
static keyExists ( key ) {
2017-11-24 19:21:30 +00:00
return key in this . metadata ();
2017-10-30 00:29:10 +00:00
}
2018-03-02 18:16:48 +00: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 19:21:30 +00:00
const metadata = this . metadata ();
2017-08-22 19:57:35 +02:00
this . keys_ = [];
2017-11-24 19:21:30 +00: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 20:16:03 +00: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 20:17:23 +00:00
static load () {
2017-07-26 17:49:01 +00:00
this . cancelScheduleSave ();
2017-05-12 20:17:23 +00:00
this . cache_ = [];
2017-06-19 20:26:27 +01:00
return this . modelSelectAll ( 'SELECT * FROM settings' ). then (( rows ) => {
2017-07-26 19:36:16 +01:00
this . cache_ = [];
2017-07-24 21:36:49 +01:00
2017-07-26 19:36:16 +01:00
for ( let i = 0 ; i < rows . length ; i ++ ) {
let c = rows [ i ];
2017-10-30 00:29:10 +00:00
if ( ! this . keyExists ( c . key )) continue ;
2017-07-25 22:55:26 +01:00
c . value = this . formatValue ( c . key , c . value );
2018-05-08 11:29:25 +01:00
c . value = this . filterValue ( c . key , c . value );
2017-07-25 22:55:26 +01:00
2017-07-26 19:36:16 +01:00
this . cache_ . push ( c );
2017-07-24 21:36:49 +01:00
}
2017-07-26 17:49:01 +00:00
2017-10-21 17:53:43 +01:00
this . dispatchUpdateAll ();
});
}
2017-07-26 17:49:01 +00:00
2017-11-23 23:10:55 +00:00
static toPlainObject () {
2017-10-21 17:53:43 +01:00
const keys = this . keys ();
let keyToValues = {};
for ( let i = 0 ; i < keys . length ; i ++ ) {
keyToValues [ keys [ i ]] = this . value ( keys [ i ]);
}
2017-11-23 23:10:55 +00:00
return keyToValues ;
}
2017-10-21 17:53:43 +01:00
2017-11-23 23:10:55 +00:00
static dispatchUpdateAll () {
2017-10-21 17:53:43 +01:00
this . dispatch ({
2017-11-08 21:22:24 +00:00
type : 'SETTING_UPDATE_ALL' ,
2017-11-23 23:10:55 +00:00
settings : this . toPlainObject (),
2017-05-12 20:17:23 +00:00
});
}
2017-06-24 19:51:43 +01:00
static setConstant ( key , value ) {
2017-07-06 22:30:45 +01:00
if ( ! ( key in this . constants_ )) throw new Error ( 'Unknown constant key: ' + key );
2017-06-24 19:51:43 +01:00
this . constants_ [ key ] = value ;
}
2017-05-12 20:17:23 +00:00
static setValue ( key , value ) {
2017-06-19 18:58:49 +00: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 11:29:25 +01:00
value = this . filterValue ( key , value );
2018-04-15 21:20:39 +05:30
2017-05-12 20:17:23 +00:00
for ( let i = 0 ; i < this . cache_ . length ; i ++ ) {
2017-07-24 18:58:11 +00:00
let c = this . cache_ [ i ];
if ( c . key == key ) {
const md = this . settingMetadata ( key );
2017-07-25 22:55:26 +01:00
if ( md . isEnum === true ) {
2017-07-24 18:58:11 +00: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 22:55:26 +01:00
2018-01-17 21:17:40 +00: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 22:55:26 +01:00
2017-11-14 18:02:58 +00:00
c . value = value ;
2017-07-26 17:49:01 +00:00
this . dispatch ({
2017-11-08 21:22:24 +00:00
type : 'SETTING_UPDATE_ONE' ,
2017-07-26 17:49:01 +00:00
key : key ,
value : c . value ,
});
this . scheduleSave ();
2017-05-12 20:17:23 +00:00
return ;
}
}
2017-07-25 22:55:26 +01:00
this . cache_ . push ({
key : key ,
value : this . formatValue ( key , value ),
});
2017-07-26 17:49:01 +00:00
this . dispatch ({
2017-11-08 21:22:24 +00:00
type : 'SETTING_UPDATE_ONE' ,
2017-07-26 17:49:01 +00:00
key : key ,
value : this . formatValue ( key , value ),
});
this . scheduleSave ();
}
2017-12-17 20:51:45 +01:00
static setObjectKey ( settingKey , objectKey , value ) {
2017-12-28 19:57:21 +00:00
let o = this . value ( settingKey );
2017-12-17 20:51:45 +01: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 17:49:01 +00: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 17:02:20 +00:00
if ( md . type == Setting . TYPE_ARRAY ) return value ? JSON . stringify ( value ) : '[]' ;
2017-11-14 18:02:58 +00:00
if ( md . type == Setting . TYPE_OBJECT ) return value ? JSON . stringify ( value ) : '{}' ;
2018-01-07 19:20:10 +00:00
if ( md . type == Setting . TYPE_STRING ) return value ? value + '' : '' ;
2018-04-15 21:20:39 +05:30
throw new Error ( 'Unhandled value type: ' + md . type );
2017-05-12 20:17:23 +00:00
}
2018-05-08 11:29:25 +01:00
static filterValue ( key , value ) {
const md = this . settingMetadata ( key );
return md . filter ? md . filter ( value ) : value ;
}
2017-07-25 22:55:26 +01:00
static formatValue ( key , value ) {
const md = this . settingMetadata ( key );
2017-11-12 17:02:20 +00:00
2018-01-07 19:29:57 +00:00
if ( md . type == Setting . TYPE_INT ) return ! value ? 0 : Math . floor ( Number ( value ));
2017-11-12 17:02:20 +00:00
2017-07-26 17:49:01 +00:00
if ( md . type == Setting . TYPE_BOOL ) {
2017-07-26 22:07:27 +01:00
if ( typeof value === 'string' ) {
value = value . toLowerCase ();
if ( value === 'true' ) return true ;
if ( value === 'false' ) return false ;
value = Number ( value );
}
2017-07-26 17:49:01 +00:00
return !! value ;
}
2017-11-12 17:02:20 +00:00
if ( md . type === Setting . TYPE_ARRAY ) {
2017-11-14 18:02:58 +00:00
if ( ! value ) return [];
2017-11-12 17:02:20 +00:00
if ( Array . isArray ( value )) return value ;
if ( typeof value === 'string' ) return JSON . parse ( value );
return [];
}
2017-11-14 18:02:58 +00: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 19:20:10 +00:00
if ( md . type === Setting . TYPE_STRING ) {
if ( ! value ) return '' ;
return value + '' ;
}
throw new Error ( 'Unhandled value type: ' + md . type );
2017-07-25 18:57:06 +00:00
}
2017-05-12 20:17:23 +00:00
static value ( key ) {
2017-12-14 19:39:13 +00: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 19:20:10 +00:00
if ( value === null || value === undefined ) return value ;
2017-12-14 19:39:13 +00:00
if ( Array . isArray ( value )) return value . slice ();
if ( typeof value === 'object' ) return Object . assign ({}, value );
return value ;
}
2017-07-06 23:15:31 +01:00
if ( key in this . constants_ ) {
2017-11-19 22:08:58 +00:00
const v = this . constants_ [ key ];
const output = typeof v === 'function' ? v () : v ;
2017-07-06 23:15:31 +01:00
if ( output == 'SET_ME' ) throw new Error ( 'Setting constant has not been set: ' + key );
return output ;
}
2017-06-24 19:51:43 +01:00
2017-06-19 18:58:49 +00:00
if ( ! this . cache_ ) throw new Error ( 'Settings have not been initialized!' );
2017-05-12 20:17:23 +00:00
for ( let i = 0 ; i < this . cache_ . length ; i ++ ) {
if ( this . cache_ [ i ]. key == key ) {
2017-12-14 19:39:13 +00:00
return copyIfNeeded ( this . cache_ [ i ]. value );
2017-05-12 20:17:23 +00:00
}
}
2017-07-25 22:55:26 +01:00
const md = this . settingMetadata ( key );
2017-12-14 19:39:13 +00:00
return copyIfNeeded ( md . value );
2017-05-12 20:17:23 +00:00
}
2017-07-24 18:58:11 +00:00
static isEnum ( key ) {
const md = this . settingMetadata ( key );
2017-07-25 22:55:26 +01:00
return md . isEnum === true ;
2017-07-24 18:58:11 +00: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 19:21:30 +00: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 18:58:11 +00:00
}
2017-10-19 23:02:13 +01:00
static enumOptionsDoc ( key , templateString = null ) {
if ( templateString === null ) templateString = '%s: %s' ;
2017-07-24 18:58:11 +00:00
const options = this . enumOptions ( key );
let output = [];
for ( let n in options ) {
if ( ! options . hasOwnProperty ( n )) continue ;
2017-10-19 23:02:13 +01:00
output . push ( sprintf ( templateString , n , options [ n ]));
2017-07-24 18:58:11 +00:00
}
return output . join ( ', ' );
}
static isAllowedEnumOption ( key , value ) {
const options = this . enumOptions ( key );
return !! options [ value ];
}
2018-02-06 18:59:36 +00: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 18:59:36 +00: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 18:59:36 +00: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 17:12:09 +00:00
static async saveAll () {
2017-07-26 17:49:01 +00:00
if ( ! this . saveTimeoutId_ ) return Promise . resolve ();
2017-05-12 20:17:23 +00:00
2017-06-25 11:41:03 +01:00
this . logger (). info ( 'Saving settings...' );
2017-07-26 17:49:01 +00:00
clearTimeout ( this . saveTimeoutId_ );
this . saveTimeoutId_ = null ;
2017-05-19 19:12:09 +00:00
2017-06-11 22:11:14 +01:00
let queries = [];
queries . push ( 'DELETE FROM settings' );
for ( let i = 0 ; i < this . cache_ . length ; i ++ ) {
2017-07-26 17:49:01 +00: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 22:11:14 +01:00
}
2018-02-15 17:12:09 +00:00
await BaseModel . db (). transactionExecBatch ( queries );
2018-04-15 21:20:39 +05:30
2018-02-15 17:12:09 +00:00
this . logger (). info ( 'Settings have been saved.' );
2017-05-19 19:12:09 +00:00
}
2017-07-26 17:49:01 +00:00
static scheduleSave () {
2018-02-15 17:12:09 +00:00
if ( ! Setting . autoSaveEnabled ) return ;
2017-07-26 17:49:01 +00:00
if ( this . saveTimeoutId_ ) clearTimeout ( this . saveTimeoutId_ );
2017-05-19 19:12:09 +00:00
2017-07-26 17:49:01 +00:00
this . saveTimeoutId_ = setTimeout (() => {
2017-05-19 19:12:09 +00:00
this . saveAll ();
2017-05-12 20:17:23 +00:00
}, 500 );
}
2017-07-26 17:49:01 +00:00
static cancelScheduleSave () {
if ( this . saveTimeoutId_ ) clearTimeout ( this . saveTimeoutId_ );
this . saveTimeoutId_ = null ;
2017-06-19 20:26:27 +01:00
}
2017-07-23 19:26:50 +01:00
static publicSettings ( appType ) {
if ( ! appType ) throw new Error ( 'appType is required' );
2017-11-24 19:21:30 +00:00
const metadata = this . metadata ();
2017-07-23 19:26:50 +01:00
let output = {};
2017-11-24 19:21:30 +00:00
for ( let key in metadata ) {
if ( ! metadata . hasOwnProperty ( key )) continue ;
let s = Object . assign ({}, metadata [ key ]);
2017-07-23 19:26:50 +01: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-19 23:02:13 +01: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 17:02:20 +00:00
if ( typeId === Setting . TYPE_ARRAY ) return 'array' ;
2017-11-14 18:02:58 +00:00
if ( typeId === Setting . TYPE_OBJECT ) return 'object' ;
2017-10-19 23:02:13 +01:00
}
2017-05-12 20:17:23 +00:00
}
2017-07-25 22:55:26 +01:00
Setting . TYPE_INT = 1 ;
Setting . TYPE_STRING = 2 ;
Setting . TYPE_BOOL = 3 ;
2017-11-12 17:02:20 +00:00
Setting . TYPE_ARRAY = 4 ;
2017-11-14 18:02:58 +00:00
Setting . TYPE_OBJECT = 5 ;
2017-07-25 22:55:26 +01:00
2017-07-31 22:51:24 +02:00
Setting . THEME_LIGHT = 1 ;
Setting . THEME_DARK = 2 ;
2017-11-28 18:02:54 +00: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 18:02:54 +00:00
Setting . TIME_FORMAT_1 = 'HH:mm' ;
Setting . TIME_FORMAT_2 = 'h:mm A' ;
2017-06-24 19:51:43 +01:00
// Contains constants that are set by the application and
// cannot be modified by the user:
Setting . constants_ = {
2017-10-30 21:29:36 +00: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 18:02:01 +00:00
openDevTools : false ,
2017-06-24 19:51:43 +01:00
}
2018-02-15 17:12:09 +00:00
Setting . autoSaveEnabled = true ;
2017-12-14 18:12:14 +00:00
module . exports = Setting ;