mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop, Cli: Save user settings to JSON file
This commit is contained in:
parent
3a8aea1aa4
commit
71f976f6a5
@ -67,6 +67,9 @@ readme/
|
||||
packages/app-cli/app/LinkSelector.d.ts
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/LinkSelector.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@ -919,6 +922,9 @@ packages/lib/models/SmartFilter.js.map
|
||||
packages/lib/models/Tag.d.ts
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/Tag.js.map
|
||||
packages/lib/models/settings/FileHandler.d.ts
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/FileHandler.js.map
|
||||
packages/lib/models/utils/paginatedFeed.d.ts
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginatedFeed.js.map
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -54,6 +54,9 @@ lerna-debug.log
|
||||
packages/app-cli/app/LinkSelector.d.ts
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/LinkSelector.js.map
|
||||
packages/app-cli/app/command-settingschema.d.ts
|
||||
packages/app-cli/app/command-settingschema.js
|
||||
packages/app-cli/app/command-settingschema.js.map
|
||||
packages/app-cli/app/services/plugins/PluginRunner.d.ts
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||
packages/app-cli/app/services/plugins/PluginRunner.js.map
|
||||
@ -906,6 +909,9 @@ packages/lib/models/SmartFilter.js.map
|
||||
packages/lib/models/Tag.d.ts
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/Tag.js.map
|
||||
packages/lib/models/settings/FileHandler.d.ts
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/FileHandler.js.map
|
||||
packages/lib/models/utils/paginatedFeed.d.ts
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginatedFeed.js.map
|
||||
|
772
docs/schema/settings.json
Normal file
772
docs/schema/settings.json
Normal file
@ -0,0 +1,772 @@
|
||||
{
|
||||
"title": "JSON schema for Joplin setting files",
|
||||
"$id": "https://joplinapp.org/schema/settings.json",
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clientId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"editor.codeView": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.target": {
|
||||
"type": "integer",
|
||||
"default": 7,
|
||||
"enum": [
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9
|
||||
]
|
||||
},
|
||||
"sync.upgradeState": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.2.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.5.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.5.username": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"sync.5.password": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.6.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.6.username": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"sync.6.password": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.8.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.8.url": {
|
||||
"type": "string",
|
||||
"default": "https://s3.amazonaws.com/"
|
||||
},
|
||||
"sync.8.username": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"sync.8.password": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.9.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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: https://joplinapp.org/faq/"
|
||||
},
|
||||
"sync.9.directory": {
|
||||
"type": "string",
|
||||
"default": "Apps/Joplin"
|
||||
},
|
||||
"sync.9.username": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"sync.9.password": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.5.syncTargets": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.resourceDownloadMode": {
|
||||
"type": "string",
|
||||
"default": "always",
|
||||
"description": "In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.",
|
||||
"enum": [
|
||||
"always",
|
||||
"manual",
|
||||
"auto"
|
||||
]
|
||||
},
|
||||
"sync.3.auth": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.4.auth": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.7.auth": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.9.auth": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.1.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.2.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.3.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.4.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.5.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.6.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.7.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.8.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.9.context": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.maxConcurrentConnections": {
|
||||
"type": "integer",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 20
|
||||
},
|
||||
"activeFolderId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"richTextBannerDismissed": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"firstStart": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"default": "en_GB",
|
||||
"enum": [
|
||||
"ar",
|
||||
"eu",
|
||||
"bs_BA",
|
||||
"bg_BG",
|
||||
"ca",
|
||||
"hr_HR",
|
||||
"cs_CZ",
|
||||
"da_DK",
|
||||
"de_DE",
|
||||
"et_EE",
|
||||
"en_GB",
|
||||
"en_US",
|
||||
"es_ES",
|
||||
"eo",
|
||||
"fi_FI",
|
||||
"fr_FR",
|
||||
"gl_ES",
|
||||
"id_ID",
|
||||
"it_IT",
|
||||
"nl_BE",
|
||||
"nl_NL",
|
||||
"nb_NO",
|
||||
"fa",
|
||||
"pl_PL",
|
||||
"pt_BR",
|
||||
"pt_PT",
|
||||
"ro",
|
||||
"sl_SI",
|
||||
"sv",
|
||||
"th_TH",
|
||||
"vi",
|
||||
"tr_TR",
|
||||
"el_GR",
|
||||
"ru_RU",
|
||||
"sr_RS",
|
||||
"zh_CN",
|
||||
"zh_TW",
|
||||
"ja_JP",
|
||||
"ko"
|
||||
]
|
||||
},
|
||||
"dateFormat": {
|
||||
"type": "string",
|
||||
"default": "DD/MM/YYYY",
|
||||
"enum": [
|
||||
"DD/MM/YYYY",
|
||||
"DD/MM/YY",
|
||||
"MM/DD/YYYY",
|
||||
"MM/DD/YY",
|
||||
"YYYY-MM-DD",
|
||||
"DD.MM.YYYY",
|
||||
"YYYY.MM.DD"
|
||||
]
|
||||
},
|
||||
"timeFormat": {
|
||||
"type": "string",
|
||||
"default": "HH:mm",
|
||||
"enum": [
|
||||
"HH:mm",
|
||||
"h:mm A"
|
||||
]
|
||||
},
|
||||
"theme": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
22
|
||||
]
|
||||
},
|
||||
"themeAutoDetect": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"preferredLightTheme": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
22
|
||||
]
|
||||
},
|
||||
"preferredDarkTheme": {
|
||||
"type": "integer",
|
||||
"default": 2,
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
22
|
||||
]
|
||||
},
|
||||
"notificationPermission": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"showNoteCounts": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"layoutButtonSequence": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"$comment": "private"
|
||||
},
|
||||
"uncompletedTodosOnTop": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"showCompletedTodos": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"notes.sortOrder.field": {
|
||||
"type": "string",
|
||||
"default": "user_updated_time",
|
||||
"enum": [
|
||||
"user_updated_time",
|
||||
"user_created_time",
|
||||
"title",
|
||||
"order"
|
||||
]
|
||||
},
|
||||
"editor.autoMatchingBraces": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"notes.sortOrder.reverse": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"folders.sortOrder.field": {
|
||||
"type": "string",
|
||||
"default": "title",
|
||||
"enum": [
|
||||
"title",
|
||||
"last_note_user_updated_time"
|
||||
]
|
||||
},
|
||||
"folders.sortOrder.reverse": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"trackLocation": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"editor.beta": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "This beta adds list continuation, Markdown preview, and Markdown shortcuts. If you find bugs, please report them in the Discourse forum.",
|
||||
"$comment": "private"
|
||||
},
|
||||
"newTodoFocus": {
|
||||
"type": "string",
|
||||
"default": "title",
|
||||
"enum": [
|
||||
"title",
|
||||
"body"
|
||||
]
|
||||
},
|
||||
"newNoteFocus": {
|
||||
"type": "string",
|
||||
"default": "body",
|
||||
"enum": [
|
||||
"title",
|
||||
"body"
|
||||
]
|
||||
},
|
||||
"plugins.states": {
|
||||
"type": "object",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"plugins.devPluginPaths": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect."
|
||||
},
|
||||
"markdown.softbreaks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"markdown.typographer": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"markdown.plugin.softbreaks": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.typographer": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.linkify": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.katex": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.fountain": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.mermaid": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.audioPlayer": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.videoPlayer": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.pdfViewer": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.mark": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.footnote": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.toc": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"markdown.plugin.sub": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.sup": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.deflist": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.abbr": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.emoji": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.insert": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"markdown.plugin.multitable": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"showTrayIcon": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "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."
|
||||
},
|
||||
"startMinimized": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"collapsedFolderIds": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"$comment": "private"
|
||||
},
|
||||
"keychain.supported": {
|
||||
"type": "integer",
|
||||
"default": -1,
|
||||
"$comment": "private"
|
||||
},
|
||||
"db.ftsEnabled": {
|
||||
"type": "integer",
|
||||
"default": -1,
|
||||
"$comment": "private"
|
||||
},
|
||||
"db.fuzzySearchEnabled": {
|
||||
"type": "integer",
|
||||
"default": -1,
|
||||
"$comment": "private"
|
||||
},
|
||||
"encryption.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"encryption.activeMasterKeyId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"encryption.passwordCache": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"$comment": "private"
|
||||
},
|
||||
"encryption.shouldReencrypt": {
|
||||
"type": "integer",
|
||||
"default": -1,
|
||||
"$comment": "private"
|
||||
},
|
||||
"style.zoom": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"minimum": 50,
|
||||
"maximum": 500,
|
||||
"$comment": "private"
|
||||
},
|
||||
"style.editor.fontSize": {
|
||||
"type": "integer",
|
||||
"default": 13,
|
||||
"minimum": 4,
|
||||
"maximum": 50
|
||||
},
|
||||
"style.editor.fontFamily": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "This should be a *monospace* font or some elements will render incorrectly. If the font is incorrect or empty, it will default to a generic monospace font."
|
||||
},
|
||||
"ui.layout": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"$comment": "private"
|
||||
},
|
||||
"autoUpdateEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"autoUpdate.includePreReleases": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "See the pre-release page for more details: https://joplinapp.org/prereleases"
|
||||
},
|
||||
"clipperServer.autoStart": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"sync.interval": {
|
||||
"type": "integer",
|
||||
"default": 300,
|
||||
"enum": [
|
||||
0,
|
||||
300,
|
||||
600,
|
||||
1800,
|
||||
3600,
|
||||
43200,
|
||||
86400
|
||||
]
|
||||
},
|
||||
"noteVisiblePanes": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
"editor",
|
||||
"viewer"
|
||||
],
|
||||
"$comment": "private"
|
||||
},
|
||||
"tagHeaderIsExpanded": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"folderHeaderIsExpanded": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"editor": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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."
|
||||
},
|
||||
"export.pdfPageSize": {
|
||||
"type": "string",
|
||||
"default": "A4",
|
||||
"enum": [
|
||||
"A4",
|
||||
"Letter",
|
||||
"A3",
|
||||
"A5",
|
||||
"Tabloid",
|
||||
"Legal"
|
||||
]
|
||||
},
|
||||
"export.pdfPageOrientation": {
|
||||
"type": "string",
|
||||
"default": "portrait",
|
||||
"enum": [
|
||||
"portrait",
|
||||
"landscape"
|
||||
]
|
||||
},
|
||||
"editor.keyboardMode": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"enum": [
|
||||
"",
|
||||
"emacs",
|
||||
"vim"
|
||||
]
|
||||
},
|
||||
"editor.spellcheckBeta": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)",
|
||||
"$comment": "private"
|
||||
},
|
||||
"net.customCertificates": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"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\"."
|
||||
},
|
||||
"net.ignoreTlsErrors": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"sync.wipeOutFailSafe": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)"
|
||||
},
|
||||
"api.token": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"$comment": "private"
|
||||
},
|
||||
"api.port": {
|
||||
"type": "integer",
|
||||
"default": null,
|
||||
"description": "Specify the port that should be used by the API server. If not set, a default will be used."
|
||||
},
|
||||
"resourceService.lastProcessedChangeId": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"$comment": "private"
|
||||
},
|
||||
"searchEngine.lastProcessedChangeId": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"$comment": "private"
|
||||
},
|
||||
"revisionService.lastProcessedChangeId": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"$comment": "private"
|
||||
},
|
||||
"searchEngine.initialIndexingDone": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"revisionService.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"revisionService.ttlDays": {
|
||||
"type": "integer",
|
||||
"default": 90,
|
||||
"minimum": 1,
|
||||
"maximum": 730
|
||||
},
|
||||
"revisionService.intervalBetweenRevisions": {
|
||||
"type": "integer",
|
||||
"default": 600000,
|
||||
"$comment": "private"
|
||||
},
|
||||
"revisionService.oldNoteInterval": {
|
||||
"type": "integer",
|
||||
"default": 604800000,
|
||||
"$comment": "private"
|
||||
},
|
||||
"welcome.wasBuilt": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"$comment": "private"
|
||||
},
|
||||
"welcome.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"camera.type": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"$comment": "private"
|
||||
},
|
||||
"camera.ratio": {
|
||||
"type": "string",
|
||||
"default": "4:3",
|
||||
"$comment": "private"
|
||||
},
|
||||
"spellChecker.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"$comment": "private"
|
||||
},
|
||||
"spellChecker.language": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"$comment": "private"
|
||||
},
|
||||
"windowContentZoomFactor": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"minimum": 30,
|
||||
"maximum": 300,
|
||||
"$comment": "private"
|
||||
},
|
||||
"layout.folderList.factor": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
|
||||
},
|
||||
"layout.noteList.factor": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
|
||||
},
|
||||
"layout.note.factor": {
|
||||
"type": "integer",
|
||||
"default": 2,
|
||||
"description": "The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
|
||||
}
|
||||
}
|
||||
}
|
@ -14,9 +14,10 @@
|
||||
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildDoc": "./packages/tools/build-all.sh",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
"buildSettingJsonSchema": "npm start --prefix=packages/app-cli -- settingschema ../../docs/schema/settings.json",
|
||||
"buildTranslations": "npm run tsc && node packages/tools/build-translation.js",
|
||||
"buildTranslationsNoTsc": "node packages/tools/build-translation.js",
|
||||
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc",
|
||||
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc && npm run buildSettingJsonSchema",
|
||||
"circularDependencyCheck": "madge --warning --circular --extensions js ./",
|
||||
"clean": "lerna clean -y && lerna run clean",
|
||||
"dependencyTree": "madge",
|
||||
|
72
packages/app-cli/app/command-settingschema.ts
Normal file
72
packages/app-cli/app/command-settingschema.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import Setting, { SettingStorage } from '@joplin/lib/models/Setting';
|
||||
import { SettingItemType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
|
||||
function settingTypeToSchemaType(type: SettingItemType): string {
|
||||
const map: Record<SettingItemType, string> = {
|
||||
[SettingItemType.Int]: 'integer',
|
||||
[SettingItemType.String]: 'string',
|
||||
[SettingItemType.Bool]: 'boolean',
|
||||
[SettingItemType.Array]: 'array',
|
||||
[SettingItemType.Object]: 'object',
|
||||
[SettingItemType.Button]: '',
|
||||
};
|
||||
|
||||
const r = map[type];
|
||||
if (r === '') return '';
|
||||
|
||||
if (!r) throw new Error(`Unsupported type: ${type}`);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
class Command extends BaseCommand {
|
||||
usage() {
|
||||
return 'settingschema <file>';
|
||||
}
|
||||
|
||||
description() {
|
||||
return 'Build the setting schema file';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async action(args: any) {
|
||||
const schema: Record<string, any> = {
|
||||
title: 'JSON schema for Joplin setting files',
|
||||
'$id': Setting.schemaUrl,
|
||||
'$schema': 'https://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties: {},
|
||||
};
|
||||
|
||||
const metadata = Setting.metadata();
|
||||
|
||||
for (const key of Object.keys(metadata)) {
|
||||
const md = metadata[key];
|
||||
|
||||
const type = settingTypeToSchemaType(md.type);
|
||||
if (!type) continue;
|
||||
|
||||
const props: Record<string, any> = {};
|
||||
props.type = type;
|
||||
props.default = md.value;
|
||||
if (md.description && md.description('desktop')) props.description = md.description('desktop');
|
||||
if (md.isEnum) props.enum = Object.keys(md.options()).map((v: any) => Setting.formatValue(key, v));
|
||||
if ('minimum' in md) props.minimum = md.minimum;
|
||||
if ('maximum' in md) props.maximum = md.maximum;
|
||||
if (!md.public || md.storage !== SettingStorage.File) props['$comment'] = 'private';
|
||||
schema.properties[key] = props;
|
||||
}
|
||||
|
||||
const outFilePath = args['file'];
|
||||
|
||||
await shim.fsDriver().writeFile(outFilePath, JSON.stringify(schema, null, '\t'), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
@ -1,6 +1,11 @@
|
||||
import Setting, { SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from './test-utils';
|
||||
import * as fs from 'fs-extra';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('./test-utils.js');
|
||||
async function loadSettingsFromFile(): Promise<any> {
|
||||
return JSON.parse(await fs.readFile(Setting.settingFilePath, 'utf8'));
|
||||
}
|
||||
|
||||
describe('models_Setting', function() {
|
||||
|
||||
@ -127,4 +132,70 @@ describe('models_Setting', function() {
|
||||
|
||||
expect(Setting.sectionNameToLabel('mySection')).toBe('My section');
|
||||
}));
|
||||
|
||||
it('should save and load settings from file', (async () => {
|
||||
Setting.setValue('sync.target', 9); // Saved to file
|
||||
Setting.setValue('encryption.passwordCache', {}); // Saved to keychain or db
|
||||
Setting.setValue('plugins.states', { test: true }); // Always saved to db
|
||||
await Setting.saveAll();
|
||||
|
||||
{
|
||||
const settings = await loadSettingsFromFile();
|
||||
expect(settings['sync.target']).toBe(9);
|
||||
expect(settings).not.toContain('encryption.passwordCache');
|
||||
expect(settings).not.toContain('plugins.states');
|
||||
}
|
||||
|
||||
Setting.setValue('sync.target', 8);
|
||||
await Setting.saveAll();
|
||||
|
||||
{
|
||||
const settings = await loadSettingsFromFile();
|
||||
expect(settings['sync.target']).toBe(8);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should not save to file if nothing has changed', (async () => {
|
||||
Setting.setValue('sync.target', 9);
|
||||
await Setting.saveAll();
|
||||
|
||||
{
|
||||
// Double-check that timestamp is indeed changed when the content is
|
||||
// changed.
|
||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
||||
await msleep(1001);
|
||||
Setting.setValue('sync.target', 8);
|
||||
await Setting.saveAll();
|
||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
||||
expect(afterStat.mtime.getTime()).toBeGreaterThan(beforeStat.mtime.getTime());
|
||||
}
|
||||
|
||||
{
|
||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
||||
await msleep(1001);
|
||||
Setting.setValue('sync.target', 8);
|
||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
||||
await Setting.saveAll();
|
||||
expect(afterStat.mtime.getTime()).toBe(beforeStat.mtime.getTime());
|
||||
}
|
||||
}));
|
||||
|
||||
it('should handle invalid JSON', (async () => {
|
||||
const badContent = '{ oopsIforgotTheQuotes: true}';
|
||||
await fs.writeFile(Setting.settingFilePath, badContent, 'utf8');
|
||||
await Setting.reset();
|
||||
|
||||
Logger.globalLogger.enabled = false;
|
||||
await Setting.load();
|
||||
Logger.globalLogger.enabled = true;
|
||||
|
||||
// Invalid JSON file has been moved to .bak file
|
||||
expect(await fs.pathExists(Setting.settingFilePath)).toBe(false);
|
||||
|
||||
const files = await fs.readdir(Setting.value('profileDir'));
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0].endsWith('.bak')).toBe(true);
|
||||
expect(await fs.readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -109,10 +109,12 @@ const supportDir = `${__dirname}/support`;
|
||||
// We add a space in the data directory path as that will help uncover
|
||||
// various space-in-path issues.
|
||||
const dataDir = `${__dirname}/test data/${suiteName_}`;
|
||||
const profileDir = `${dataDir}/profile`;
|
||||
|
||||
fs.mkdirpSync(logDir, 0o755);
|
||||
fs.mkdirpSync(baseTempDir, 0o755);
|
||||
fs.mkdirpSync(dataDir);
|
||||
fs.mkdirpSync(profileDir);
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetMemory);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
@ -182,7 +184,8 @@ Setting.setConstant('appId', 'net.cozic.joplintest-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
Setting.setConstant('tempDir', baseTempDir);
|
||||
Setting.setConstant('cacheDir', baseTempDir);
|
||||
Setting.setConstant('pluginDataDir', `${dataDir}/plugin-data`);
|
||||
Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`);
|
||||
Setting.setConstant('profileDir', profileDir);
|
||||
Setting.setConstant('env', 'dev');
|
||||
|
||||
BaseService.logger_ = logger;
|
||||
|
@ -58,17 +58,26 @@ class Logger {
|
||||
private targets_: Target[] = [];
|
||||
private level_: LogLevel = LogLevel.Info;
|
||||
private lastDbCleanup_: number = time.unixMs();
|
||||
private enabled_: boolean = true;
|
||||
|
||||
static fsDriver() {
|
||||
if (!Logger.fsDriver_) Logger.fsDriver_ = new FsDriverDummy();
|
||||
return Logger.fsDriver_;
|
||||
}
|
||||
|
||||
public get enabled(): boolean {
|
||||
return this.enabled_;
|
||||
}
|
||||
|
||||
public set enabled(v: boolean) {
|
||||
this.enabled_ = v;
|
||||
}
|
||||
|
||||
public static initializeGlobalLogger(logger: Logger) {
|
||||
this.globalLogger_ = logger;
|
||||
}
|
||||
|
||||
private static get globalLogger(): Logger {
|
||||
public static get globalLogger(): Logger {
|
||||
if (!this.globalLogger_) throw new Error('Global logger has not been initialized!!');
|
||||
return this.globalLogger_;
|
||||
}
|
||||
@ -169,7 +178,7 @@ class Logger {
|
||||
}
|
||||
|
||||
public log(level: LogLevel, prefix: string, ...object: any[]) {
|
||||
if (!this.targets_.length) return;
|
||||
if (!this.targets_.length || !this.enabled) return;
|
||||
|
||||
for (let i = 0; i < this.targets_.length; i++) {
|
||||
const target = this.targets_[i];
|
||||
|
@ -6,6 +6,7 @@ import BaseModel from '../BaseModel';
|
||||
import Database from '../database';
|
||||
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
|
||||
import time from '../time';
|
||||
import FileHandler, { SettingValues } from './settings/FileHandler';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const ObjectUtils = require('../ObjectUtils');
|
||||
const { toTitleCase } = require('../string-utils.js');
|
||||
@ -24,6 +25,11 @@ interface KeysOptions {
|
||||
secureOnly?: boolean;
|
||||
}
|
||||
|
||||
export enum SettingStorage {
|
||||
Database = 1,
|
||||
File = 2,
|
||||
}
|
||||
|
||||
// This is the definition of a setting item
|
||||
export interface SettingItem {
|
||||
value: any;
|
||||
@ -49,6 +55,7 @@ export interface SettingItem {
|
||||
unitLabel?: Function;
|
||||
needRestart?: boolean;
|
||||
autoSave?: boolean;
|
||||
storage?: SettingStorage;
|
||||
}
|
||||
|
||||
interface SettingItems {
|
||||
@ -81,6 +88,8 @@ interface SettingSections {
|
||||
|
||||
class Setting extends BaseModel {
|
||||
|
||||
public static schemaUrl = 'https://joplinapp.org/schema/settings.json';
|
||||
|
||||
// For backward compatibility
|
||||
public static TYPE_INT = SettingItemType.Int;
|
||||
public static TYPE_STRING = SettingItemType.String;
|
||||
@ -133,7 +142,6 @@ class Setting extends BaseModel {
|
||||
RENDERED_MARKDOWN: 'userstyle.css',
|
||||
};
|
||||
|
||||
|
||||
// Contains constants that are set by the application and
|
||||
// cannot be modified by the user:
|
||||
public static constants_: any = {
|
||||
@ -166,6 +174,7 @@ class Setting extends BaseModel {
|
||||
private static customMetadata_: SettingItems = {};
|
||||
private static customSections_: SettingSections = {};
|
||||
private static changedKeys_: string[] = [];
|
||||
private static fileHandler_: FileHandler = null;
|
||||
|
||||
static tableName() {
|
||||
return 'settings';
|
||||
@ -185,6 +194,18 @@ class Setting extends BaseModel {
|
||||
this.keys_ = null;
|
||||
this.cache_ = [];
|
||||
this.customMetadata_ = {};
|
||||
this.fileHandler_ = null;
|
||||
}
|
||||
|
||||
public static get settingFilePath(): string {
|
||||
return `${this.value('profileDir')}/settings.json`;
|
||||
}
|
||||
|
||||
private static get fileHandler(): FileHandler {
|
||||
if (!this.fileHandler_) {
|
||||
this.fileHandler_ = new FileHandler(this.settingFilePath);
|
||||
}
|
||||
return this.fileHandler_;
|
||||
}
|
||||
|
||||
static keychainService() {
|
||||
@ -239,6 +260,7 @@ class Setting extends BaseModel {
|
||||
type: SettingItemType.Bool,
|
||||
public: false,
|
||||
appTypes: ['desktop'],
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.target': {
|
||||
value: SyncTargetRegistry.nameToId('dropbox'),
|
||||
@ -253,6 +275,7 @@ class Setting extends BaseModel {
|
||||
options: () => {
|
||||
return SyncTargetRegistry.idAndLabelPlainObject(platform);
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'sync.upgradeState': {
|
||||
@ -278,6 +301,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('Directory to synchronise with (absolute path)'),
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'sync.5.path': {
|
||||
@ -290,6 +314,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('Nextcloud WebDAV URL'),
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.5.username': {
|
||||
value: '',
|
||||
@ -300,6 +325,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('Nextcloud username'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.5.password': {
|
||||
value: '',
|
||||
@ -323,6 +349,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('WebDAV URL'),
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.6.username': {
|
||||
value: '',
|
||||
@ -333,6 +360,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('WebDAV username'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.6.password': {
|
||||
value: '',
|
||||
@ -363,6 +391,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('AWS S3 bucket'),
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.8.url': {
|
||||
value: 'https://s3.amazonaws.com/',
|
||||
@ -373,7 +402,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('AWS S3 URL'),
|
||||
secure: false,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.8.username': {
|
||||
value: '',
|
||||
@ -384,6 +413,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('AWS key'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.8.password': {
|
||||
value: '',
|
||||
@ -407,6 +437,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
label: () => _('Joplin Server URL'),
|
||||
description: () => emptyDirWarning,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.9.directory': {
|
||||
value: 'Apps/Joplin',
|
||||
@ -420,6 +451,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('Joplin Server Directory'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.9.username': {
|
||||
value: '',
|
||||
@ -430,6 +462,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
public: true,
|
||||
label: () => _('Joplin Server username'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'sync.9.password': {
|
||||
value: '',
|
||||
@ -462,6 +495,7 @@ class Setting extends BaseModel {
|
||||
auto: _('Auto'),
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
|
||||
@ -478,7 +512,7 @@ class Setting extends BaseModel {
|
||||
'sync.8.context': { value: '', type: SettingItemType.String, public: false },
|
||||
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
|
||||
// The active folder ID is guaranteed to be valid as long as there's at least one
|
||||
// existing folder, so it is a good default in contexts where there's no currently
|
||||
@ -498,6 +532,7 @@ class Setting extends BaseModel {
|
||||
options: () => {
|
||||
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
dateFormat: {
|
||||
value: Setting.DATE_FORMAT_1,
|
||||
@ -517,6 +552,7 @@ class Setting extends BaseModel {
|
||||
options[Setting.DATE_FORMAT_7] = time.formatMsToLocal(now, Setting.DATE_FORMAT_7);
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
timeFormat: {
|
||||
value: Setting.TIME_FORMAT_1,
|
||||
@ -531,6 +567,7 @@ class Setting extends BaseModel {
|
||||
options[Setting.TIME_FORMAT_2] = time.formatMsToLocal(now, Setting.TIME_FORMAT_2);
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
theme: {
|
||||
@ -545,6 +582,7 @@ class Setting extends BaseModel {
|
||||
label: () => _('Theme'),
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
themeAutoDetect: {
|
||||
@ -554,6 +592,7 @@ class Setting extends BaseModel {
|
||||
appTypes: ['desktop'],
|
||||
public: true,
|
||||
label: () => _('Automatically switch theme to match system theme'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
preferredLightTheme: {
|
||||
@ -568,6 +607,7 @@ class Setting extends BaseModel {
|
||||
label: () => _('Preferred light theme'),
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
preferredDarkTheme: {
|
||||
@ -582,6 +622,7 @@ class Setting extends BaseModel {
|
||||
label: () => _('Preferred dark theme'),
|
||||
section: 'appearance',
|
||||
options: () => themeOptions(),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
notificationPermission: {
|
||||
@ -590,7 +631,7 @@ class Setting extends BaseModel {
|
||||
public: false,
|
||||
},
|
||||
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, public: false, advanced: true, appTypes: ['desktop'], label: () => _('Show note counts') },
|
||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false, advanced: true, appTypes: ['desktop'], label: () => _('Show note counts') },
|
||||
|
||||
layoutButtonSequence: {
|
||||
value: Setting.LAYOUT_ALL,
|
||||
@ -604,9 +645,10 @@ class Setting extends BaseModel {
|
||||
[Setting.LAYOUT_EDITOR_SPLIT]: _('%s / %s', _('Editor'), _('Split View')),
|
||||
[Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')),
|
||||
}),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, section: 'note', public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') },
|
||||
showCompletedTodos: { value: true, type: SettingItemType.Bool, section: 'note', public: true, appTypes: ['cli'], label: () => _('Show completed to-dos') },
|
||||
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') },
|
||||
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: ['cli'], label: () => _('Show completed to-dos') },
|
||||
'notes.sortOrder.field': {
|
||||
value: 'user_updated_time',
|
||||
type: SettingItemType.String,
|
||||
@ -624,6 +666,7 @@ class Setting extends BaseModel {
|
||||
}
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'editor.autoMatchingBraces': {
|
||||
value: true,
|
||||
@ -632,8 +675,9 @@ class Setting extends BaseModel {
|
||||
section: 'note',
|
||||
appTypes: ['desktop'],
|
||||
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||
'folders.sortOrder.field': {
|
||||
value: 'title',
|
||||
type: SettingItemType.String,
|
||||
@ -650,9 +694,10 @@ class Setting extends BaseModel {
|
||||
}
|
||||
return options;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', public: true, label: () => _('Save geo-location with notes') },
|
||||
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, public: true, label: () => _('Save geo-location with notes') },
|
||||
|
||||
// 2020-10-29: For now disable the beta editor due to
|
||||
// underlying bugs in the TextInput component which we cannot
|
||||
@ -683,6 +728,7 @@ class Setting extends BaseModel {
|
||||
body: _('Focus body'),
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
newNoteFocus: {
|
||||
value: 'body',
|
||||
@ -698,6 +744,7 @@ class Setting extends BaseModel {
|
||||
body: _('Focus body'),
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'plugins.states': {
|
||||
@ -719,34 +766,35 @@ class Setting extends BaseModel {
|
||||
appTypes: ['desktop'],
|
||||
label: () => 'Development plugins',
|
||||
description: () => 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.',
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
// Deprecated - use markdown.plugin.*
|
||||
'markdown.softbreaks': { value: false, type: SettingItemType.Bool, public: false, appTypes: ['mobile', 'desktop'] },
|
||||
'markdown.typographer': { value: false, type: SettingItemType.Bool, public: false, appTypes: ['mobile', 'desktop'] },
|
||||
'markdown.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: ['mobile', 'desktop'] },
|
||||
'markdown.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: ['mobile', 'desktop'] },
|
||||
// Deprecated
|
||||
|
||||
'markdown.plugin.softbreaks': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
||||
'markdown.plugin.typographer': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
||||
'markdown.plugin.linkify': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
||||
'markdown.plugin.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
||||
'markdown.plugin.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
||||
'markdown.plugin.linkify': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
||||
|
||||
'markdown.plugin.katex': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
||||
'markdown.plugin.fountain': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
||||
'markdown.plugin.mermaid': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
||||
'markdown.plugin.katex': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
||||
'markdown.plugin.fountain': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
||||
'markdown.plugin.mermaid': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
||||
|
||||
'markdown.plugin.audioPlayer': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
||||
'markdown.plugin.videoPlayer': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
||||
'markdown.plugin.pdfViewer': { value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['desktop'], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
||||
'markdown.plugin.mark': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ==mark== syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.footnote': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
||||
'markdown.plugin.toc': { value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.sub': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ~sub~ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.sup': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ^sup^ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.deflist': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.abbr': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.emoji': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
||||
'markdown.plugin.insert': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ++insert++ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.multitable': { value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
||||
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
||||
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['desktop'], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
||||
'markdown.plugin.mark': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ==mark== syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.footnote': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
||||
'markdown.plugin.toc': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
||||
'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ~sub~ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ^sup^ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.deflist': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.abbr': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.emoji': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
||||
'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ++insert++ syntax')}${wysiwygNo}` },
|
||||
'markdown.plugin.multitable': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||
|
||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||
@ -762,9 +810,10 @@ class Setting extends BaseModel {
|
||||
description: () => {
|
||||
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.');
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
startMinimized: { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Start application minimised in the tray icon') },
|
||||
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Start application minimised in the tray icon') },
|
||||
|
||||
collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false },
|
||||
|
||||
@ -781,9 +830,9 @@ class Setting extends BaseModel {
|
||||
},
|
||||
|
||||
// Deprecated in favour of windowContentZoomFactor
|
||||
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, appTypes: ['desktop'], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
||||
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, appTypes: ['desktop'], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
||||
|
||||
'style.editor.fontSize': { value: 13, type: SettingItemType.Int, public: true, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
||||
'style.editor.fontSize': { value: 13, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: ['desktop'], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
||||
'style.editor.fontFamily':
|
||||
(mobilePlatform) ?
|
||||
({
|
||||
@ -809,6 +858,7 @@ class Setting extends BaseModel {
|
||||
[Setting.FONT_MONOSPACE]: 'Monospace',
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
}) : {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
@ -819,9 +869,10 @@ class Setting extends BaseModel {
|
||||
description: () =>
|
||||
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
|
||||
'is incorrect or empty, it will default to a generic monospace font.'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'ui.layout': { value: {}, type: SettingItemType.Object, public: false, appTypes: ['desktop'] },
|
||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },
|
||||
|
||||
// TODO: Is there a better way to do this? The goal here is to simply have
|
||||
// a way to display a link to the customizable stylesheets, not for it to
|
||||
@ -864,9 +915,9 @@ class Setting extends BaseModel {
|
||||
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
|
||||
},
|
||||
|
||||
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') },
|
||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, public: false },
|
||||
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') },
|
||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
||||
'sync.interval': {
|
||||
value: 300,
|
||||
type: SettingItemType.Int,
|
||||
@ -885,12 +936,13 @@ class Setting extends BaseModel {
|
||||
86400: _('%d hours', 24),
|
||||
};
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, public: false, appTypes: ['desktop'] },
|
||||
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: ['desktop'] },
|
||||
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },
|
||||
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: ['desktop'] },
|
||||
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', 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.') },
|
||||
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page size for PDF export'), options: () => {
|
||||
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, 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.') },
|
||||
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page size for PDF export'), options: () => {
|
||||
return {
|
||||
'A4': _('A4'),
|
||||
'Letter': _('Letter'),
|
||||
@ -900,7 +952,7 @@ class Setting extends BaseModel {
|
||||
'Legal': _('Legal'),
|
||||
};
|
||||
} },
|
||||
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, advanced: true, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page orientation for PDF export'), options: () => {
|
||||
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, advanced: true, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page orientation for PDF export'), options: () => {
|
||||
return {
|
||||
'portrait': _('Portrait'),
|
||||
'landscape': _('Landscape'),
|
||||
@ -922,6 +974,7 @@ class Setting extends BaseModel {
|
||||
output['vim'] = _('Vim');
|
||||
return output;
|
||||
},
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'editor.spellcheckBeta': {
|
||||
@ -945,6 +998,7 @@ class Setting extends BaseModel {
|
||||
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".'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'net.ignoreTlsErrors': {
|
||||
value: false,
|
||||
@ -957,6 +1011,7 @@ class Setting extends BaseModel {
|
||||
public: true,
|
||||
appTypes: ['desktop', 'cli'],
|
||||
label: () => _('Ignore TLS certificate errors'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'sync.wipeOutFailSafe': {
|
||||
@ -967,10 +1022,11 @@ class Setting extends BaseModel {
|
||||
section: 'sync',
|
||||
label: () => _('Fail-safe'),
|
||||
description: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'api.token': { value: null, type: SettingItemType.String, public: false },
|
||||
'api.port': { value: null, type: SettingItemType.Int, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File },
|
||||
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||
|
||||
'resourceService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||
'searchEngine.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||
@ -978,7 +1034,7 @@ class Setting extends BaseModel {
|
||||
|
||||
'searchEngine.initialIndexingDone': { value: false, type: SettingItemType.Bool, public: false },
|
||||
|
||||
'revisionService.enabled': { section: 'revisionService', value: true, type: SettingItemType.Bool, public: true, label: () => _('Enable note history') },
|
||||
'revisionService.enabled': { section: 'revisionService', storage: SettingStorage.File, value: true, type: SettingItemType.Bool, public: true, label: () => _('Enable note history') },
|
||||
'revisionService.ttlDays': {
|
||||
section: 'revisionService',
|
||||
value: 90,
|
||||
@ -991,6 +1047,7 @@ class Setting extends BaseModel {
|
||||
return value === null ? _('days') : _('%d days', value);
|
||||
},
|
||||
label: () => _('Keep note history for'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'revisionService.intervalBetweenRevisions': { section: 'revisionService', value: 1000 * 60 * 10, type: SettingItemType.Int, public: false },
|
||||
'revisionService.oldNoteInterval': { section: 'revisionService', value: 1000 * 60 * 60 * 24 * 7, type: SettingItemType.Int, public: false },
|
||||
@ -1001,8 +1058,8 @@ class Setting extends BaseModel {
|
||||
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: ['mobile'] },
|
||||
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: ['mobile'] },
|
||||
|
||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, public: false },
|
||||
'spellChecker.language': { value: '', type: SettingItemType.String, public: false },
|
||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
||||
'spellChecker.language': { value: '', type: SettingItemType.String, storage: SettingStorage.File, public: false },
|
||||
|
||||
windowContentZoomFactor: {
|
||||
value: 100,
|
||||
@ -1012,6 +1069,7 @@ class Setting extends BaseModel {
|
||||
minimum: 30,
|
||||
maximum: 300,
|
||||
step: 10,
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'layout.folderList.factor': {
|
||||
@ -1026,6 +1084,7 @@ class Setting extends BaseModel {
|
||||
'to fit the available space in its container with respect to the other items. ' +
|
||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||
'Restart app to see changes.'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'layout.noteList.factor': {
|
||||
value: 1,
|
||||
@ -1039,6 +1098,7 @@ class Setting extends BaseModel {
|
||||
'to fit the available space in its container with respect to the other items. ' +
|
||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||
'Restart app to see changes.'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
'layout.note.factor': {
|
||||
value: 2,
|
||||
@ -1052,6 +1112,7 @@ class Setting extends BaseModel {
|
||||
'to fit the available space in its container with respect to the other items. ' +
|
||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||
'Restart app to see changes.'),
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1151,8 +1212,13 @@ class Setting extends BaseModel {
|
||||
}
|
||||
|
||||
// Low-level method to load a setting directly from the database. Should not be used in most cases.
|
||||
static loadOne(key: string) {
|
||||
return this.modelSelectOne('SELECT * FROM settings WHERE key = ?', [key]);
|
||||
public static async loadOne(key: string) {
|
||||
if (this.keyStorage(key) === SettingStorage.File) {
|
||||
const fromFile = await this.fileHandler.load();
|
||||
return fromFile[key];
|
||||
} else {
|
||||
return this.modelSelectOne('SELECT * FROM settings WHERE key = ?', [key]);
|
||||
}
|
||||
}
|
||||
|
||||
static load() {
|
||||
@ -1160,10 +1226,10 @@ class Setting extends BaseModel {
|
||||
this.cancelScheduleChangeEvent();
|
||||
|
||||
this.cache_ = [];
|
||||
return this.modelSelectAll('SELECT * FROM settings').then(async (rows: any[]) => {
|
||||
return this.modelSelectAll('SELECT * FROM settings').then(async (rows: CacheItem[]) => {
|
||||
this.cache_ = [];
|
||||
|
||||
const pushItemsToCache = (items: any[]) => {
|
||||
const pushItemsToCache = (items: CacheItem[]) => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const c = items[i];
|
||||
|
||||
@ -1183,7 +1249,7 @@ class Setting extends BaseModel {
|
||||
|
||||
const rowKeys = rows.map((r: any) => r.key);
|
||||
const secureKeys = this.keys(false, null, { secureOnly: true });
|
||||
const secureItems = [];
|
||||
const secureItems: CacheItem[] = [];
|
||||
for (const key of secureKeys) {
|
||||
if (rowKeys.includes(key)) continue;
|
||||
|
||||
@ -1196,13 +1262,36 @@ class Setting extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
const itemsFromFile: CacheItem[] = [];
|
||||
|
||||
if (this.canUseFileStorage()) {
|
||||
const fromFile = await this.fileHandler.load();
|
||||
for (const k of Object.keys(fromFile)) {
|
||||
itemsFromFile.push({
|
||||
key: k,
|
||||
value: this.filterValue(k, this.formatValue(k, fromFile[k])),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pushItemsToCache(rows);
|
||||
pushItemsToCache(secureItems);
|
||||
pushItemsToCache(itemsFromFile);
|
||||
|
||||
this.dispatchUpdateAll();
|
||||
});
|
||||
}
|
||||
|
||||
private static canUseFileStorage(): boolean {
|
||||
return !shim.mobilePlatform();
|
||||
}
|
||||
|
||||
private static keyStorage(key: string): SettingStorage {
|
||||
if (!this.canUseFileStorage()) return SettingStorage.Database;
|
||||
const md = this.settingMetadata(key);
|
||||
return md.storage || SettingStorage.Database;
|
||||
}
|
||||
|
||||
static toPlainObject() {
|
||||
const keys = this.keys();
|
||||
const keyToValues: any = {};
|
||||
@ -1475,12 +1564,14 @@ class Setting extends BaseModel {
|
||||
|
||||
const keys = this.keys();
|
||||
|
||||
const valuesForFile: SettingValues = {};
|
||||
|
||||
const queries = [];
|
||||
queries.push(`DELETE FROM settings WHERE key IN ("${keys.join('","')}")`);
|
||||
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
const s = Object.assign({}, this.cache_[i]);
|
||||
s.value = this.valueToString(s.key, s.value);
|
||||
const valueAsString = this.valueToString(s.key, s.value);
|
||||
|
||||
if (this.isSecureKey(s.key)) {
|
||||
// We need to be careful here because there's a bug in the macOS keychain that can
|
||||
@ -1497,8 +1588,8 @@ class Setting extends BaseModel {
|
||||
try {
|
||||
const passwordName = `setting.${s.key}`;
|
||||
const currentValue = await this.keychainService().password(passwordName);
|
||||
if (currentValue !== s.value) {
|
||||
const wasSet = await this.keychainService().setPassword(passwordName, s.value);
|
||||
if (currentValue !== valueAsString) {
|
||||
const wasSet = await this.keychainService().setPassword(passwordName, valueAsString);
|
||||
if (wasSet) continue;
|
||||
} else {
|
||||
// The value is already in the keychain - so nothing to do
|
||||
@ -1511,11 +1602,20 @@ class Setting extends BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
queries.push(Database.insertQuery(this.tableName(), s));
|
||||
if (this.keyStorage(s.key) === SettingStorage.File) {
|
||||
valuesForFile[s.key] = s.value;
|
||||
} else {
|
||||
queries.push(Database.insertQuery(this.tableName(), {
|
||||
key: s.key,
|
||||
value: valueAsString,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
await BaseModel.db().transactionExecBatch(queries);
|
||||
|
||||
if (this.canUseFileStorage()) await this.fileHandler.save(valuesForFile);
|
||||
|
||||
this.logger().debug('Settings have been saved.');
|
||||
}
|
||||
|
||||
|
54
packages/lib/models/settings/FileHandler.ts
Normal file
54
packages/lib/models/settings/FileHandler.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import Logger from '../../Logger';
|
||||
import shim from '../../shim';
|
||||
import Setting from '../Setting';
|
||||
|
||||
const logger = Logger.create('Settings');
|
||||
|
||||
export type SettingValues = Record<string, any>;
|
||||
|
||||
export default class FileHandler {
|
||||
|
||||
private filePath_: string;
|
||||
private valueJsonCache_: string = null;
|
||||
|
||||
public constructor(filePath: string) {
|
||||
this.filePath_ = filePath;
|
||||
}
|
||||
|
||||
public async load(): Promise<SettingValues> {
|
||||
if (this.valueJsonCache_) return JSON.parse(this.valueJsonCache_);
|
||||
|
||||
if (!(await shim.fsDriver().exists(this.filePath_))) {
|
||||
this.valueJsonCache_ = '{}';
|
||||
} else {
|
||||
this.valueJsonCache_ = await shim.fsDriver().readFile(this.filePath_, 'utf8');
|
||||
}
|
||||
|
||||
try {
|
||||
const values = JSON.parse(this.valueJsonCache_);
|
||||
delete values['$id'];
|
||||
delete values['$schema'];
|
||||
return values;
|
||||
} catch (error) {
|
||||
// Most likely the user entered invalid JSON - in this case we move
|
||||
// the broken file to a new name (otherwise it would be overwritten
|
||||
// by valid JSON and user will lose all their settings).
|
||||
logger.error(`Could not parse JSON file: ${this.filePath_}`, error);
|
||||
await shim.fsDriver().move(this.filePath_, `${this.filePath_}-${Date.now()}-invalid.bak`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public async save(values: SettingValues) {
|
||||
const json = `${JSON.stringify({
|
||||
'$schema': Setting.schemaUrl,
|
||||
...values,
|
||||
}, null, '\t')}\n`;
|
||||
|
||||
if (json === this.valueJsonCache_) return;
|
||||
|
||||
await shim.fsDriver().writeFile(this.filePath_, json, 'utf8');
|
||||
this.valueJsonCache_ = json;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user