diff --git a/.eslintignore b/.eslintignore index c9b9a36500..09e0330742 100644 --- a/.eslintignore +++ b/.eslintignore @@ -941,8 +941,10 @@ packages/lib/models/Tag.test.js packages/lib/models/Tag.js packages/lib/models/dateTimeFormats.test.js packages/lib/models/settings/FileHandler.js +packages/lib/models/settings/builtInMetadata.js packages/lib/models/settings/settingValidations.test.js packages/lib/models/settings/settingValidations.js +packages/lib/models/settings/types.js packages/lib/models/utils/getCollator.js packages/lib/models/utils/getConflictFolderId.js packages/lib/models/utils/isItemId.js diff --git a/.gitignore b/.gitignore index 192b9008e9..04ab9f7f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -920,8 +920,10 @@ packages/lib/models/Tag.test.js packages/lib/models/Tag.js packages/lib/models/dateTimeFormats.test.js packages/lib/models/settings/FileHandler.js +packages/lib/models/settings/builtInMetadata.js packages/lib/models/settings/settingValidations.test.js packages/lib/models/settings/settingValidations.js +packages/lib/models/settings/types.js packages/lib/models/utils/getCollator.js packages/lib/models/utils/getConflictFolderId.js packages/lib/models/utils/isItemId.js diff --git a/packages/app-cli/app/command-settingschema.ts b/packages/app-cli/app/command-settingschema.ts index 44e717b938..ad68059906 100644 --- a/packages/app-cli/app/command-settingschema.ts +++ b/packages/app-cli/app/command-settingschema.ts @@ -1,4 +1,4 @@ -import Setting, { SettingStorage } from '@joplin/lib/models/Setting'; +import Setting, { AppType, SettingStorage } from '@joplin/lib/models/Setting'; import { SettingItemType } from '@joplin/lib/services/plugins/api/types'; import shim from '@joplin/lib/shim'; @@ -61,7 +61,7 @@ class Command extends BaseCommand { const description: string[] = []; if (md.label && md.label()) description.push(md.label()); - if (md.description && md.description('desktop')) description.push(md.description('desktop')); + if (md.description && md.description(AppType.Desktop)) description.push(md.description(AppType.Desktop)); if (description.length) props.description = description.join('. '); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 96dd338482..3778e00e37 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -405,7 +405,7 @@ class ConfigScreenComponent extends React.Component { return (
{label} - {this.renderDescription(this.props.themeId, md.description ? md.description() : null)} + {this.renderDescription(this.props.themeId, md.description ? md.description(AppType.Desktop) : null)} { }; const label = [md.label()]; - if (md.unitLabel) label.push(`(${md.unitLabel()})`); + if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const inputStyle: any = { ...textInputBaseStyle }; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 6024a6e5d8..3abcdc436b 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -713,7 +713,7 @@ function useMenu(props: Props) { label: layoutButtonSequenceOptions[value], type: 'checkbox', click: () => { - Setting.setValue('layoutButtonSequence', value); + Setting.setValue('layoutButtonSequence', Number(value)); }, }); } diff --git a/packages/app-desktop/services/sortOrder/notesSortOrderUtils.ts b/packages/app-desktop/services/sortOrder/notesSortOrderUtils.ts index 139b954e26..6b9f499c61 100644 --- a/packages/app-desktop/services/sortOrder/notesSortOrderUtils.ts +++ b/packages/app-desktop/services/sortOrder/notesSortOrderUtils.ts @@ -60,7 +60,8 @@ export const setNotesSortOrder = (field?: string, reverse?: boolean) => { nextReverse = !!nextReverse; if (perFieldReverse[nextField] !== nextReverse) { perFieldReverse[nextField] = nextReverse; - Setting.setValue('notes.perFieldReverse', { ...perFieldReverse }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied + Setting.setValue('notes.perFieldReverse', { ...perFieldReverse } as any); } } }; diff --git a/packages/app-desktop/utils/restartInSafeModeFromMain.ts b/packages/app-desktop/utils/restartInSafeModeFromMain.ts index 7b65225b01..dd4b9d3cf7 100644 --- a/packages/app-desktop/utils/restartInSafeModeFromMain.ts +++ b/packages/app-desktop/utils/restartInSafeModeFromMain.ts @@ -1,4 +1,4 @@ -import Setting from '@joplin/lib/models/Setting'; +import Setting, { AppType } from '@joplin/lib/models/Setting'; import bridge from '../bridge'; import processStartFlags from '@joplin/lib/utils/processStartFlags'; import { safeModeFlagFilename } from '@joplin/lib/BaseApplication'; @@ -13,7 +13,7 @@ const restartInSafeModeFromMain = async () => { // a large amount of other code) to the database. const appName = bridge().appName(); Setting.setConstant('appId', `net.cozic.${appName}`); - Setting.setConstant('appType', 'desktop'); + Setting.setConstant('appType', AppType.Desktop); Setting.setConstant('appName', appName); // Load just enough for us to write a file in the profile directory diff --git a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx index db457a9099..83f0c8fb3f 100644 --- a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx +++ b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx @@ -50,7 +50,7 @@ interface Props { } function fontFamilyFromSettings() { - const font = editorFont(Setting.value('style.editor.fontFamily')); + const font = editorFont(Setting.value('style.editor.fontFamily') as number); return font ? `${font}, sans-serif` : 'sans-serif'; } diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index c78a89b1d3..5112c9c7eb 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -475,7 +475,7 @@ class ConfigScreenComponent extends BaseScreenComponent = props => { const output: any = null; const md = Setting.settingMetadata(props.settingId); - const settingDescription = md.description ? md.description() : ''; + const settingDescription = md.description ? md.description(AppType.Mobile) : ''; const styleSheet = props.styles.styleSheet; diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 455e53145e..3ac98c444e 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService'; import KvStore from '@joplin/lib/services/KvStore'; import NoteScreen from './components/screens/Note'; import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen'; -import Setting, { Env } from '@joplin/lib/models/Setting'; +import Setting, { AppType, Env } from '@joplin/lib/models/Setting'; import PoorManIntervals from '@joplin/lib/PoorManIntervals'; import reducer, { NotesParent, parseNotesParent, serializeNotesParent } from '@joplin/lib/reducer'; import ShareExtension from './utils/ShareExtension'; @@ -505,9 +505,9 @@ async function initialize(dispatch: Function) { value: profileConfig, }); - Setting.setConstant('env', __DEV__ ? 'dev' : 'prod'); + Setting.setConstant('env', __DEV__ ? Env.Dev : Env.Prod); Setting.setConstant('appId', 'net.cozic.joplin-mobile'); - Setting.setConstant('appType', 'mobile'); + Setting.setConstant('appType', AppType.Mobile); Setting.setConstant('tempDir', await initializeTempDir()); Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`); const resourceDir = getResourceDir(currentProfile, isSubProfile); @@ -619,7 +619,7 @@ async function initialize(dispatch: Function) { reg.logger().info(`First start: detected locale as ${detectedLocale}`); Setting.skipDefaultMigrations(); - Setting.setValue('firstStart', 0); + Setting.setValue('firstStart', false); } else { Setting.applyDefaultMigrations(); } @@ -788,7 +788,7 @@ async function initialize(dispatch: Function) { const pluginSettings = pluginService.unserializePluginSettings(Setting.value('plugins.states')); const updatedSettings = pluginService.clearUpdateState(pluginSettings); - Setting.setValue('plugins.states', pluginService.serializePluginSettings(updatedSettings)); + Setting.setValue('plugins.states', updatedSettings); // ---------------------------------------------------------------------------- // Keep this below to test react-native-rsa-native diff --git a/packages/generate-plugin-doc/package.json b/packages/generate-plugin-doc/package.json index 56817d95ef..724ddb4fee 100644 --- a/packages/generate-plugin-doc/package.json +++ b/packages/generate-plugin-doc/package.json @@ -3,7 +3,7 @@ "packageManager": "yarn@3.6.0", "private": true, "scripts": { - "buildPluginDoc_": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/" + "buildPluginDoc_": "typedoc --exclude '../lib/models/**' --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/" }, "dependencies": { "typedoc": "0.17.8", diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 4bbb667fd7..4a651a911a 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -687,7 +687,7 @@ export default class BaseApplication { const tempDir = `${profileDir}/tmp`; const cacheDir = `${profileDir}/cache`; - Setting.setConstant('env', initArgs.env); + Setting.setConstant('env', initArgs.env as Env); Setting.setConstant('resourceDirName', resourceDirName); Setting.setConstant('resourceDir', resourceDir); Setting.setConstant('tempDir', tempDir); @@ -789,12 +789,12 @@ export default class BaseApplication { Setting.skipDefaultMigrations(); if (Setting.value('env') === 'dev') { - Setting.setValue('showTrayIcon', 0); - Setting.setValue('autoUpdateEnabled', 0); + Setting.setValue('showTrayIcon', false); + Setting.setValue('autoUpdateEnabled', false); Setting.setValue('sync.interval', 3600); } - Setting.setValue('firstStart', 0); + Setting.setValue('firstStart', false); } else { Setting.applyDefaultMigrations(); Setting.applyUserSettingMigration(); diff --git a/packages/lib/models/Setting.test.ts b/packages/lib/models/Setting.test.ts index c0e1e1323c..f9085bf93e 100644 --- a/packages/lib/models/Setting.test.ts +++ b/packages/lib/models/Setting.test.ts @@ -5,6 +5,7 @@ import Logger from '@joplin/utils/Logger'; import { defaultProfileConfig } from '../services/profileConfig/types'; import { createNewProfile, saveProfileConfig } from '../services/profileConfig'; import initProfile from '../services/profileConfig/initProfile'; +import { defaultPluginSetting } from '../services/plugins/PluginService'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied async function loadSettingsFromFile(): Promise { @@ -206,7 +207,7 @@ describe('models/Setting', () => { 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 + Setting.setValue('plugins.states', { test: defaultPluginSetting() }); // Always saved to db await Setting.saveAll(); { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index f0ae8d062c..935811f2a5 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -1,106 +1,38 @@ import shim from '../shim'; -import { _, supportedLocalesToLanguages, defaultLocale } from '../locale'; +import { _ } from '../locale'; import eventManager, { EventName } from '../eventManager'; import BaseModel from '../BaseModel'; import Database from '../database'; -import SyncTargetRegistry from '../SyncTargetRegistry'; -import time from '../time'; import FileHandler, { SettingValues } from './settings/FileHandler'; import Logger from '@joplin/utils/Logger'; import mergeGlobalAndLocalSettings from '../services/profileConfig/mergeGlobalAndLocalSettings'; import splitGlobalAndLocalSettings from '../services/profileConfig/splitGlobalAndLocalSettings'; import JoplinError from '../JoplinError'; -import { defaultListColumns } from '../services/plugins/api/noteListType'; +import builtInMetadata, { BuiltInMetadataKeys, BuiltInMetadataValues } from './settings/builtInMetadata'; +import { toSystemSlashes } from '@joplin/utils/path'; +import { AppType, Env, SettingItem, SettingItemType, SettingItems, SettingSection, SettingSectionSource, SettingStorage, SettingsRecord } from './settings/types'; const { sprintf } = require('sprintf-js'); -const ObjectUtils = require('../ObjectUtils'); -const { toTitleCase } = require('../string-utils.js'); -const { rtrimSlashes, toSystemSlashes } = require('../path-utils'); const logger = Logger.create('models/Setting'); -export enum SettingItemType { - Int = 1, - String = 2, - Bool = 3, - Array = 4, - Object = 5, - Button = 6, -} +export * from './settings/types'; + +type SettingValueType = ( + T extends BuiltInMetadataKeys + ? BuiltInMetadataValues[T] + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied + : (T extends keyof Constants ? Constants[T] : any) +); interface OptionsToValueLabelsOptions { valueKey: string; labelKey: string; } -export enum SettingItemSubType { - FilePathAndArgs = 'file_path_and_args', - FilePath = 'file_path', // Not supported on mobile! - DirectoryPath = 'directory_path', // Not supported on mobile! - FontFamily = 'font_family', - MonospaceFontFamily = 'monospace_font_family', -} - interface KeysOptions { secureOnly?: boolean; } -export enum SettingStorage { - Database = 1, - File = 2, -} - -// This is the definition of a setting item -export interface SettingItem { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - value: any; - type: SettingItemType; - public: boolean; - - subType?: string; - key?: string; - isEnum?: boolean; - section?: string; - label?(): string; - // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied - description?: Function; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - options?(): any; - optionsOrder?(): string[]; - appTypes?: AppType[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show?(settings: any): boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - filter?(value: any): any; - secure?: boolean; - advanced?: boolean; - minimum?: number; - maximum?: number; - step?: number; - onClick?(): void; - // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied - unitLabel?: Function; - needRestart?: boolean; - autoSave?: boolean; - storage?: SettingStorage; - hideLabel?: boolean; - - // In a multi-profile context, all settings are by default local - they take - // their value from the current profile. This flag can be set to specify - // that the setting is global and that its value should come from the root - // profile. This flag only applies to sub-profiles. - // - // At the moment, all global settings must be saved to file (have the - // storage attribute set to "file") because it's simpler to load the root - // profile settings.json than load the whole SQLite database. This - // restriction is not an issue normally since all settings that are - // considered global are also the user-facing ones. - isGlobal?: boolean; -} - -export interface SettingItems { - [key: string]: SettingItem; -} - // This is where the actual setting values are stored. // They are saved to database at regular intervals. interface CacheItem { @@ -109,37 +41,6 @@ interface CacheItem { value: any; } -export enum SettingSectionSource { - Default = 1, - Plugin = 2, -} - -export interface SettingSection { - label: string; - iconName?: string; - description?: string; - name?: string; - source?: SettingSectionSource; -} - -export enum SyncStartupOperation { - None = 0, - ClearLocalSyncState = 1, - ClearLocalData = 2, -} - -export enum Env { - Undefined = 'SET_ME', - Dev = 'dev', - Prod = 'prod', -} - -export enum AppType { - Desktop = 'desktop', - Mobile = 'mobile', - Cli = 'cli', -} - export interface Constants { env: Env; isDemo: boolean; @@ -408,1535 +309,7 @@ class Setting extends BaseModel { public static metadata(): SettingItems { if (this.metadata_) return this.metadata_; - const platform = shim.platformName(); - const mobilePlatform = shim.mobilePlatform(); - - let wysiwygYes = ''; - let wysiwygNo = ''; - if (shim.isElectron()) { - wysiwygYes = ` ${_('(wysiwyg: %s)', _('yes'))}`; - wysiwygNo = ` ${_('(wysiwyg: %s)', _('no'))}`; - } - - const emptyDirWarning = _('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://joplinapp.org/help/faq'); - - // 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. - - const themeOptions = () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const output: any = {}; - output[Setting.THEME_LIGHT] = _('Light'); - output[Setting.THEME_DARK] = _('Dark'); - output[Setting.THEME_DRACULA] = _('Dracula'); - output[Setting.THEME_SOLARIZED_LIGHT] = _('Solarised Light'); - output[Setting.THEME_SOLARIZED_DARK] = _('Solarised Dark'); - output[Setting.THEME_NORD] = _('Nord'); - output[Setting.THEME_ARITIM_DARK] = _('Aritim Dark'); - output[Setting.THEME_OLED_DARK] = _('OLED Dark'); - return output; - }; - - this.buildInMetadata_ = { - 'clientId': { - value: '', - type: SettingItemType.String, - public: false, - }, - 'editor.codeView': { - value: true, - type: SettingItemType.Bool, - public: false, - appTypes: [AppType.Desktop], - storage: SettingStorage.File, - isGlobal: true, - }, - - 'sync.openSyncWizard': { - value: null, - type: SettingItemType.Button, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Open Sync Wizard...'), - hideLabel: true, - section: 'sync', - }, - - 'sync.target': { - value: 0, - type: SettingItemType.Int, - isEnum: true, - public: true, - section: 'sync', - label: () => _('Synchronisation target'), - description: (appType: AppType) => { - return appType !== 'cli' ? null : _('The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).'); - }, - options: () => { - return SyncTargetRegistry.idAndLabelPlainObject(platform); - }, - optionsOrder: () => { - return SyncTargetRegistry.optionsOrder(); - }, - storage: SettingStorage.File, - }, - - 'sync.upgradeState': { - value: Setting.SYNC_UPGRADE_STATE_IDLE, - type: SettingItemType.Int, - public: false, - }, - - 'sync.startupOperation': { - value: SyncStartupOperation.None, - type: SettingItemType.Int, - public: false, - }, - - 'sync.2.path': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - try { - return settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem'); - } catch (error) { - return false; - } - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - filter: (value: any) => { - return value ? rtrimSlashes(value) : ''; - }, - public: true, - label: () => _('Directory to synchronise with (absolute path)'), - description: () => emptyDirWarning, - storage: SettingStorage.File, - }, - - 'sync.5.path': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); - }, - public: true, - label: () => _('Nextcloud WebDAV URL'), - description: () => emptyDirWarning, - storage: SettingStorage.File, - }, - 'sync.5.username': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); - }, - public: true, - label: () => _('Nextcloud username'), - storage: SettingStorage.File, - }, - 'sync.5.password': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); - }, - public: true, - label: () => _('Nextcloud password'), - secure: true, - }, - - 'sync.6.path': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); - }, - public: true, - label: () => _('WebDAV URL'), - description: () => emptyDirWarning, - storage: SettingStorage.File, - }, - 'sync.6.username': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); - }, - public: true, - label: () => _('WebDAV username'), - storage: SettingStorage.File, - }, - 'sync.6.password': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); - }, - public: true, - label: () => _('WebDAV password'), - secure: true, - }, - - 'sync.8.path': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - try { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - } catch (error) { - return false; - } - }, - filter: value => { - return value ? rtrimSlashes(value) : ''; - }, - public: true, - label: () => _('S3 bucket'), - description: () => emptyDirWarning, - storage: SettingStorage.File, - }, - 'sync.8.url': { - value: 'https://s3.amazonaws.com/', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - }, - filter: value => { - return value ? value.trim() : ''; - }, - public: true, - label: () => _('S3 URL'), - storage: SettingStorage.File, - }, - 'sync.8.region': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - }, - filter: value => { - return value ? value.trim() : ''; - }, - public: true, - label: () => _('S3 region'), - storage: SettingStorage.File, - }, - 'sync.8.username': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - }, - public: true, - label: () => _('S3 access key'), - storage: SettingStorage.File, - }, - 'sync.8.password': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - }, - public: true, - label: () => _('S3 secret key'), - secure: true, - }, - 'sync.8.forcePathStyle': { - value: false, - type: SettingItemType.Bool, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); - }, - public: true, - label: () => _('Force path style'), - storage: SettingStorage.File, - }, - 'sync.9.path': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); - }, - public: true, - label: () => _('Joplin Server URL'), - description: () => emptyDirWarning, - storage: SettingStorage.File, - }, - 'sync.9.userContentPath': { - value: '', - type: SettingItemType.String, - public: false, - storage: SettingStorage.Database, - }, - 'sync.9.username': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); - }, - public: true, - label: () => _('Joplin Server email'), - storage: SettingStorage.File, - }, - 'sync.9.password': { - value: '', - type: SettingItemType.String, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); - }, - public: true, - label: () => _('Joplin Server password'), - secure: true, - }, - - // Although sync.10.path is essentially a constant, we still define - // it here so that both Joplin Server and Joplin Cloud can be - // handled in the same consistent way. Also having it a setting - // means it can be set to something else for development. - 'sync.10.path': { - value: 'https://api.joplincloud.com', - type: SettingItemType.String, - public: false, - storage: SettingStorage.Database, - }, - 'sync.10.userContentPath': { - value: 'https://joplinusercontent.com', - type: SettingItemType.String, - public: false, - storage: SettingStorage.Database, - }, - 'sync.10.website': { - value: 'https://joplincloud.com', - type: SettingItemType.String, - public: false, - storage: SettingStorage.Database, - }, - 'sync.10.username': { - value: '', - type: SettingItemType.String, - public: false, - storage: SettingStorage.File, - }, - 'sync.10.password': { - value: '', - type: SettingItemType.String, - public: false, - secure: true, - }, - - 'sync.10.inboxEmail': { value: '', type: SettingItemType.String, public: false }, - - 'sync.10.inboxId': { value: '', type: SettingItemType.String, public: false }, - - 'sync.10.canUseSharePermissions': { value: false, type: SettingItemType.Bool, public: false }, - - 'sync.10.accountType': { value: 0, type: SettingItemType.Int, public: false }, - - 'sync.10.userEmail': { value: '', type: SettingItemType.String, public: false }, - - 'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false }, - - 'sync.resourceDownloadMode': { - value: 'always', - type: SettingItemType.String, - section: 'sync', - public: true, - advanced: true, - isEnum: true, - appTypes: [AppType.Mobile, AppType.Desktop], - label: () => _('Attachment download behaviour'), - 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.'), - options: () => { - return { - always: _('Always'), - manual: _('Manual'), - auto: _('Auto'), - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - - 'sync.3.auth': { value: '', type: SettingItemType.String, public: false }, - 'sync.4.auth': { value: '', type: SettingItemType.String, public: false }, - 'sync.7.auth': { value: '', type: SettingItemType.String, public: false }, - 'sync.9.auth': { value: '', type: SettingItemType.String, public: false }, - 'sync.10.auth': { value: '', type: SettingItemType.String, public: false }, - 'sync.1.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.2.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.3.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.4.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.5.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.6.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.7.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.8.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.9.context': { value: '', type: SettingItemType.String, public: false }, - 'sync.10.context': { value: '', type: SettingItemType.String, public: false }, - - 'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, 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 - // selected folder. It corresponds in general to the currently selected folder or - // to the last folder that was selected. - activeFolderId: { value: '', type: SettingItemType.String, public: false }, - notesParent: { value: '', type: SettingItemType.String, public: false }, - - richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false }, - - firstStart: { value: true, type: SettingItemType.Bool, public: false }, - locale: { - value: defaultLocale(), - type: SettingItemType.String, - isEnum: true, - public: true, - label: () => _('Language'), - options: () => { - return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true })); - }, - storage: SettingStorage.File, - isGlobal: true, - }, - dateFormat: { - value: Setting.DATE_FORMAT_1, - type: SettingItemType.String, - isEnum: true, - public: true, - label: () => _('Date format'), - options: () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const options: any = {}; - 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); - options[Setting.DATE_FORMAT_6] = time.formatMsToLocal(now, Setting.DATE_FORMAT_6); - options[Setting.DATE_FORMAT_7] = time.formatMsToLocal(now, Setting.DATE_FORMAT_7); - options[Setting.DATE_FORMAT_8] = time.formatMsToLocal(now, Setting.DATE_FORMAT_8); - options[Setting.DATE_FORMAT_9] = time.formatMsToLocal(now, Setting.DATE_FORMAT_9); - return options; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - timeFormat: { - value: Setting.TIME_FORMAT_1, - type: SettingItemType.String, - isEnum: true, - public: true, - label: () => _('Time format'), - options: () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const options: any = {}; - 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); - options[Setting.TIME_FORMAT_3] = time.formatMsToLocal(now, Setting.TIME_FORMAT_3); - return options; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - - 'ocr.enabled': { - value: false, - type: SettingItemType.Bool, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Enable optical character recognition (OCR)'), - description: () => _('When enabled, the application will scan your attachments and extract the text from it. This will allow you to search for text in these attachments.'), - storage: SettingStorage.File, - isGlobal: true, - }, - - theme: { - value: Setting.THEME_LIGHT, - type: SettingItemType.Int, - public: true, - appTypes: [AppType.Mobile, AppType.Desktop], - show: (settings) => { - return !settings['themeAutoDetect']; - }, - isEnum: true, - label: () => _('Theme'), - section: 'appearance', - options: () => themeOptions(), - storage: SettingStorage.File, - isGlobal: true, - }, - - themeAutoDetect: { - value: false, - type: SettingItemType.Bool, - section: 'appearance', - appTypes: [AppType.Mobile, AppType.Desktop], - public: true, - label: () => _('Automatically switch theme to match system theme'), - storage: SettingStorage.File, - isGlobal: true, - }, - - preferredLightTheme: { - value: Setting.THEME_LIGHT, - type: SettingItemType.Int, - public: true, - show: (settings) => { - return settings['themeAutoDetect']; - }, - appTypes: [AppType.Mobile, AppType.Desktop], - isEnum: true, - label: () => _('Preferred light theme'), - section: 'appearance', - options: () => themeOptions(), - storage: SettingStorage.File, - isGlobal: true, - }, - - preferredDarkTheme: { - value: Setting.THEME_DARK, - type: SettingItemType.Int, - public: true, - show: (settings) => { - return settings['themeAutoDetect']; - }, - appTypes: [AppType.Mobile, AppType.Desktop], - isEnum: true, - label: () => _('Preferred dark theme'), - section: 'appearance', - options: () => themeOptions(), - storage: SettingStorage.File, - isGlobal: true, - }, - - notificationPermission: { - value: '', - type: SettingItemType.String, - public: false, - }, - - showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop, AppType.Cli], label: () => _('Show note counts') }, - - layoutButtonSequence: { - value: Setting.LAYOUT_ALL, - type: SettingItemType.Int, - public: false, - appTypes: [AppType.Desktop], - isEnum: true, - options: () => ({ - [Setting.LAYOUT_ALL]: _('%s / %s / %s', _('Editor'), _('Viewer'), _('Split View')), - [Setting.LAYOUT_EDITOR_VIEWER]: _('%s / %s', _('Editor'), _('Viewer')), - [Setting.LAYOUT_EDITOR_SPLIT]: _('%s / %s', _('Editor'), _('Split View')), - [Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')), - }), - storage: SettingStorage.File, - isGlobal: true, - }, - uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') }, - showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') }, - 'notes.sortOrder.field': { - value: 'user_updated_time', - type: SettingItemType.String, - section: 'note', - isEnum: true, - public: true, - appTypes: [AppType.Cli], - label: () => _('Sort notes by'), - options: () => { - const Note = require('./Note').default; - const noteSortFields = ['user_updated_time', 'user_created_time', 'title', 'order', 'todo_due', 'todo_completed']; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const options: any = {}; - for (let i = 0; i < noteSortFields.length; i++) { - options[noteSortFields[i]] = toTitleCase(Note.fieldToLabel(noteSortFields[i])); - } - return options; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - 'editor.autoMatchingBraces': { - value: true, - type: SettingItemType.Bool, - public: true, - section: 'note', - appTypes: [AppType.Desktop], - label: () => _('Auto-pair braces, parentheses, quotations, etc.'), - storage: SettingStorage.File, - isGlobal: true, - }, - 'notes.columns': { - value: defaultListColumns(), - public: false, - type: SettingItemType.Array, - storage: SettingStorage.File, - isGlobal: false, - }, - - 'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] }, - // NOTE: A setting whose name starts with 'notes.sortOrder' is special, - // which implies changing the setting automatically triggers the refresh of notes. - // See lib/BaseApplication.ts/generalMiddleware() for details. - 'notes.sortOrder.buttonsVisible': { - value: true, - type: SettingItemType.Bool, - storage: SettingStorage.File, - section: 'appearance', - public: true, - label: () => _('Show sort order buttons'), - // description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'), - appTypes: [AppType.Desktop], - isGlobal: true, - }, - 'notes.perFieldReversalEnabled': { - value: true, - type: SettingItemType.Bool, - storage: SettingStorage.File, - section: 'note', - public: false, - appTypes: [AppType.Cli, AppType.Desktop], - }, - 'notes.perFieldReverse': { - value: { - user_updated_time: true, - user_created_time: true, - title: false, - order: false, - }, - type: SettingItemType.Object, - storage: SettingStorage.File, - section: 'note', - public: false, - appTypes: [AppType.Cli, AppType.Desktop], - }, - 'notes.perFolderSortOrderEnabled': { - value: true, - type: SettingItemType.Bool, - storage: SettingStorage.File, - section: 'folder', - public: false, - appTypes: [AppType.Cli, AppType.Desktop], - }, - 'notes.perFolderSortOrders': { - value: {}, - type: SettingItemType.Object, - storage: SettingStorage.File, - section: 'folder', - public: false, - appTypes: [AppType.Cli, AppType.Desktop], - }, - 'notes.sharedSortOrder': { - value: {}, - type: SettingItemType.Object, - section: 'folder', - public: false, - appTypes: [AppType.Cli, AppType.Desktop], - }, - 'folders.sortOrder.field': { - value: 'title', - type: SettingItemType.String, - isEnum: true, - public: true, - appTypes: [AppType.Cli], - label: () => _('Sort notebooks by'), - options: () => { - const Folder = require('./Folder').default; - const folderSortFields = ['title', 'last_note_user_updated_time']; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const options: any = {}; - for (let i = 0; i < folderSortFields.length; i++) { - options[folderSortFields[i]] = toTitleCase(Folder.fieldToLabel(folderSortFields[i])); - } - return options; - }, - storage: SettingStorage.File, - }, - 'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] }, - trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Save geo-location with notes') }, - - 'editor.usePlainText': { - value: false, - type: SettingItemType.Bool, - section: 'note', - public: true, - appTypes: [AppType.Mobile], - label: () => 'Use the plain text editor', - description: () => 'The plain text editor has various issues and is no longer supported. If you are having issues with the new editor however you can revert to the old one using this setting.', - storage: SettingStorage.File, - isGlobal: true, - }, - - // Enables/disables spellcheck in the mobile markdown beta editor. - 'editor.mobile.spellcheckEnabled': { - value: true, - type: SettingItemType.Bool, - section: 'note', - public: true, - appTypes: [AppType.Mobile], - label: () => _('Enable spellcheck in the text editor'), - storage: SettingStorage.File, - isGlobal: true, - }, - - 'editor.mobile.toolbarEnabled': { - value: true, - type: SettingItemType.Bool, - section: 'note', - public: true, - appTypes: [AppType.Mobile], - label: () => _('Enable the Markdown toolbar'), - storage: SettingStorage.File, - isGlobal: true, - }, - - // Works around a bug in which additional space is visible beneath the toolbar on some devices. - // See https://github.com/laurent22/joplin/pull/6823 - 'editor.mobile.removeSpaceBelowToolbar': { - value: false, - type: SettingItemType.Bool, - section: 'note', - public: true, - appTypes: [AppType.Mobile], - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => settings['editor.mobile.removeSpaceBelowToolbar'], - label: () => 'Remove extra space below the markdown toolbar', - description: () => 'Works around bug on some devices where the markdown toolbar does not touch the bottom of the screen.', - storage: SettingStorage.File, - isGlobal: true, - }, - - newTodoFocus: { - value: 'title', - type: SettingItemType.String, - section: 'note', - isEnum: true, - public: true, - appTypes: [AppType.Desktop], - label: () => _('When creating a new to-do:'), - options: () => { - return { - title: _('Focus title'), - body: _('Focus body'), - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - newNoteFocus: { - value: 'body', - type: SettingItemType.String, - section: 'note', - isEnum: true, - public: true, - appTypes: [AppType.Desktop], - label: () => _('When creating a new note:'), - options: () => { - return { - title: _('Focus title'), - body: _('Focus body'), - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - imageResizing: { - value: 'alwaysAsk', - type: SettingItemType.String, - section: 'note', - isEnum: true, - public: true, - appTypes: [AppType.Mobile, AppType.Desktop], - label: () => _('Resize large images:'), - description: () => _('Shrink large images before adding them to notes.'), - options: () => { - return { - alwaysAsk: _('Always ask'), - alwaysResize: _('Always resize'), - neverResize: _('Never resize'), - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - - 'notes.listRendererId': { - value: 'compact', - type: SettingItemType.String, - public: false, - appTypes: [AppType.Desktop], - storage: SettingStorage.File, - isGlobal: true, - }, - - 'plugins.states': { - value: '', - type: SettingItemType.Object, - section: 'plugins', - public: true, - appTypes: [AppType.Desktop, AppType.Mobile], - needRestart: true, - autoSave: true, - }, - - 'plugins.enableWebviewDebugging': { - value: false, - type: SettingItemType.Bool, - section: 'plugins', - public: true, - appTypes: [AppType.Mobile], - show: (settings) => { - // Hide on iOS due to App Store guidelines. See - // https://github.com/laurent22/joplin/pull/10086 for details. - return shim.mobilePlatform() !== 'ios' && settings['plugins.pluginSupportEnabled']; - }, - needRestart: true, - advanced: true, - - label: () => _('Plugin WebView debugging'), - description: () => _('Allows debugging mobile plugins. See %s for details.', 'https://https://joplinapp.org/help/api/references/mobile_plugin_debugging/'), - }, - - 'plugins.pluginSupportEnabled': { - value: false, - public: true, - autoSave: true, - section: 'plugins', - advanced: true, - type: SettingItemType.Bool, - appTypes: [AppType.Mobile], - label: () => _('Enable plugin support'), - // On mobile, we have a screen that manages this setting when it's disabled. - show: (settings) => settings['plugins.pluginSupportEnabled'], - }, - - 'plugins.devPluginPaths': { - value: '', - type: SettingItemType.String, - section: 'plugins', - public: true, - advanced: true, - appTypes: [AppType.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': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] }, - 'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] }, - // Deprecated - - 'markdown.plugin.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` }, - 'markdown.plugin.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` }, - 'markdown.plugin.linkify': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` }, - - 'markdown.plugin.katex': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` }, - 'markdown.plugin.fountain': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` }, - 'markdown.plugin.mermaid': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` }, - - 'markdown.plugin.audioPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` }, - 'markdown.plugin.videoPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` }, - 'markdown.plugin.pdfViewer': { storage: SettingStorage.File, isGlobal: true, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` }, - 'markdown.plugin.mark': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` }, - 'markdown.plugin.footnote': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` }, - 'markdown.plugin.toc': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` }, - 'markdown.plugin.sub': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` }, - 'markdown.plugin.sup': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` }, - 'markdown.plugin.deflist': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` }, - 'markdown.plugin.abbr': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` }, - 'markdown.plugin.emoji': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` }, - 'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` }, - 'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.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 - // 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: SettingItemType.Bool, - section: 'application', - public: true, - appTypes: [AppType.Desktop], - label: () => _('Show tray icon'), - 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, - isGlobal: true, - }, - - showMenuBar: { - value: true, // Show the menu bar by default - type: SettingItemType.Bool, - public: false, - appTypes: [AppType.Desktop], - }, - - startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') }, - - collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false }, - - 'keychain.supported': { value: -1, type: SettingItemType.Int, public: false }, - 'db.ftsEnabled': { value: -1, type: SettingItemType.Int, public: false }, - 'db.fuzzySearchEnabled': { value: -1, type: SettingItemType.Int, public: false }, - 'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false }, - 'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false }, - 'encryption.passwordCache': { value: {}, type: SettingItemType.Object, public: false, secure: true }, - 'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true }, - 'encryption.shouldReencrypt': { - value: -1, // will be set on app startup - type: SettingItemType.Int, - public: false, - }, - - 'sync.userId': { - value: '', - type: SettingItemType.String, - public: false, - }, - - // Deprecated in favour of windowContentZoomFactor - 'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 }, - - 'style.editor.fontSize': { - value: 15, - type: SettingItemType.Int, - public: true, - storage: SettingStorage.File, - isGlobal: true, - appTypes: [AppType.Desktop, AppType.Mobile], - section: 'appearance', - label: () => _('Editor font size'), - minimum: 4, - maximum: 50, - step: 1, - }, - 'style.editor.fontFamily': - (mobilePlatform) ? - ({ - value: Setting.FONT_DEFAULT, - type: SettingItemType.String, - isEnum: true, - public: true, - label: () => _('Editor font'), - appTypes: [AppType.Mobile], - section: 'appearance', - options: () => { - // IMPORTANT: The font mapping must match the one in global-styles.js::editorFont() - if (mobilePlatform === 'ios') { - return { - [Setting.FONT_DEFAULT]: _('Default'), - [Setting.FONT_MENLO]: 'Menlo', - [Setting.FONT_COURIER_NEW]: 'Courier New', - [Setting.FONT_AVENIR]: 'Avenir', - }; - } - return { - [Setting.FONT_DEFAULT]: _('Default'), - [Setting.FONT_MONOSPACE]: 'Monospace', - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }) : { - value: '', - type: SettingItemType.String, - public: true, - appTypes: [AppType.Desktop], - section: 'appearance', - label: () => _('Editor font family'), - description: () => - _('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'), - storage: SettingStorage.File, - isGlobal: true, - subType: SettingItemSubType.FontFamily, - }, - 'style.editor.monospaceFontFamily': { - value: '', - type: SettingItemType.String, - public: true, - appTypes: [AppType.Desktop], - section: 'appearance', - label: () => _('Editor monospace font family'), - description: () => - _('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'), - storage: SettingStorage.File, - isGlobal: true, - subType: SettingItemSubType.MonospaceFontFamily, - }, - - 'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') }, - - 'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] }, - - 'ui.lastSelectedPluginPanel': { - value: '', - type: SettingItemType.String, - public: false, - description: () => 'The last selected plugin panel ID in pop-up mode (mobile).', - storage: SettingStorage.Database, - appTypes: [AppType.Mobile], - }, - - // 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 - // serve as a customizable Setting. But because the Setting page is auto- - // generated from this list of settings, there wasn't a really elegant way - // to do that directly in the React markup. - 'style.customCss.renderedMarkdown': { - value: null, - onClick: () => { - shim.openOrCreateFile( - this.customCssFilePath(Setting.customCssFilenames.RENDERED_MARKDOWN), - '/* For styling the rendered Markdown */', - ); - }, - type: SettingItemType.Button, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Custom stylesheet for rendered Markdown'), - section: 'appearance', - advanced: true, - storage: SettingStorage.File, - isGlobal: true, - }, - 'style.customCss.joplinApp': { - value: null, - onClick: () => { - shim.openOrCreateFile( - this.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP), - `/* For styling the entire Joplin app (except the rendered Markdown, which is defined in \`${Setting.customCssFilenames.RENDERED_MARKDOWN}\`) */`, - ); - }, - type: SettingItemType.Button, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Custom stylesheet for Joplin-wide app styles'), - section: 'appearance', - advanced: true, - 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.', - storage: SettingStorage.File, - isGlobal: true, - }, - - 'sync.clearLocalSyncStateButton': { - value: null, - type: SettingItemType.Button, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Re-upload local data to sync target'), - section: 'sync', - advanced: true, - description: () => 'If the data on the sync target is incorrect or empty, you can use this button to force a re-upload of your data to the sync target. Application will have to be restarted', - }, - - 'sync.clearLocalDataButton': { - value: null, - type: SettingItemType.Button, - public: true, - appTypes: [AppType.Desktop], - label: () => _('Delete local data and re-download from sync target'), - section: 'sync', - advanced: true, - description: () => 'If the data on the sync target is correct but your local data is not, you can use this button to clear your local data and force re-downloading everything from the sync target. As your local data will be deleted first, it is recommended to export your data as JEX first. Application will have to be restarted', - }, - - - autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, - 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') }, - - 'autoUploadCrashDumps': { - value: false, - section: 'application', - type: SettingItemType.Bool, - public: true, - appTypes: [AppType.Desktop], - label: () => 'Automatically upload crash reports', - description: () => 'If you experience a crash, please enable this option to automatically send crash reports. You will need to restart the application for this change to take effect.', - isGlobal: true, - storage: SettingStorage.File, - }, - - 'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false }, - 'sync.interval': { - value: 300, - type: SettingItemType.Int, - section: 'sync', - 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), - }; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - 'sync.mobileWifiOnly': { - value: false, - type: SettingItemType.Bool, - section: 'sync', - public: true, - label: () => _('Synchronise only over WiFi connection'), - storage: SettingStorage.File, - appTypes: [AppType.Mobile], - isGlobal: true, - }, - noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] }, - tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] }, - folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] }, - editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.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, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => { - return { - 'A4': _('A4'), - 'Letter': _('Letter'), - 'A3': _('A3'), - 'A5': _('A5'), - 'Tabloid': _('Tabloid'), - 'Legal': _('Legal'), - }; - } }, - 'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, isGlobal: true, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => { - return { - 'portrait': _('Portrait'), - 'landscape': _('Landscape'), - }; - } }, - - useCustomPdfViewer: { - value: false, - type: SettingItemType.Bool, - public: false, - advanced: true, - appTypes: [AppType.Desktop], - label: () => 'Use custom PDF viewer (Beta)', - description: () => 'The custom PDF viewer remembers the last page that was viewed, however it has some technical issues.', - storage: SettingStorage.File, - isGlobal: true, - }, - - 'editor.keyboardMode': { - value: '', - type: SettingItemType.String, - public: true, - appTypes: [AppType.Desktop], - isEnum: true, - advanced: true, - label: () => _('Keyboard Mode'), - options: () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const output: any = {}; - output[''] = _('Default'); - output['emacs'] = _('Emacs'); - output['vim'] = _('Vim'); - return output; - }, - storage: SettingStorage.File, - isGlobal: true, - }, - - 'editor.spellcheckBeta': { - value: false, - type: SettingItemType.Bool, - public: true, - appTypes: [AppType.Desktop], - label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)', - 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)', - storage: SettingStorage.File, - isGlobal: true, - }, - - 'imageeditor.jsdrawToolbar': { - value: '', - type: SettingItemType.String, - public: false, - appTypes: [AppType.Mobile], - label: () => '', - storage: SettingStorage.File, - }, - - 'imageeditor.imageTemplate': { - value: '{ }', - type: SettingItemType.String, - public: false, - appTypes: [AppType.Mobile], - label: () => 'Template for the image editor', - storage: SettingStorage.File, - }, - - // 2023-09-07: This setting is now used to track the desktop beta editor. It - // was used to track the mobile beta editor previously. - 'editor.beta': { - value: false, - type: SettingItemType.Bool, - section: 'general', - public: true, - appTypes: [AppType.Desktop], - label: () => 'Opt-in to the editor beta', - description: () => 'This beta adds improved accessibility and plugin API compatibility with the mobile editor. If you find bugs, please report them in the Discourse forum.', - storage: SettingStorage.File, - isGlobal: true, - }, - - 'linking.extraAllowedExtensions': { - value: [], - type: SettingItemType.Array, - public: false, - appTypes: [AppType.Desktop], - label: () => 'Additional file types that can be opened without confirmation.', - storage: SettingStorage.File, - }, - - 'net.customCertificates': { - value: '', - type: SettingItemType.String, - section: 'sync', - advanced: true, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return [ - SyncTargetRegistry.nameToId('amazon_s3'), - SyncTargetRegistry.nameToId('nextcloud'), - SyncTargetRegistry.nameToId('webdav'), - SyncTargetRegistry.nameToId('joplinServer'), - ].indexOf(settings['sync.target']) >= 0; - }, - public: true, - appTypes: [AppType.Desktop, AppType.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, - type: SettingItemType.Bool, - advanced: true, - section: 'sync', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => { - return (shim.isNode() || shim.mobilePlatform() === 'android') && - [ - SyncTargetRegistry.nameToId('amazon_s3'), - SyncTargetRegistry.nameToId('nextcloud'), - SyncTargetRegistry.nameToId('webdav'), - SyncTargetRegistry.nameToId('joplinServer'), - // Needs to be enabled for Joplin Cloud too because - // some companies filter all traffic and swap TLS - // certificates, which result in error - // UNABLE_TO_GET_ISSUER_CERT_LOCALLY - SyncTargetRegistry.nameToId('joplinCloud'), - ].indexOf(settings['sync.target']) >= 0; - }, - public: true, - label: () => _('Ignore TLS certificate errors'), - storage: SettingStorage.File, - }, - 'net.proxyEnabled': { - value: false, - type: SettingItemType.Bool, - advanced: true, - section: 'sync', - isGlobal: true, - public: true, - label: () => _('Proxy enabled'), - storage: SettingStorage.File, - }, - 'net.proxyUrl': { - value: '', - type: SettingItemType.String, - advanced: true, - section: 'sync', - isGlobal: true, - public: true, - label: () => _('Proxy URL'), - description: () => _('For example "%s"', 'http://my.proxy.com:80'), - storage: SettingStorage.File, - }, - 'net.proxyTimeout': { - value: 1, - type: SettingItemType.Int, - maximum: 60, - advanced: true, - section: 'sync', - isGlobal: true, - public: true, - label: () => _('Proxy timeout (seconds)'), - storage: SettingStorage.File, - }, - 'sync.wipeOutFailSafe': { - value: true, - type: SettingItemType.Bool, - advanced: true, - public: true, - 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, storage: SettingStorage.File, isGlobal: true }, - 'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.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 }, - 'revisionService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false }, - - 'searchEngine.initialIndexingDone': { value: false, type: SettingItemType.Bool, public: false }, - 'searchEngine.lastProcessedResource': { value: '', type: SettingItemType.String, public: false }, - - 'revisionService.enabled': { section: 'revisionService', storage: SettingStorage.File, value: true, type: SettingItemType.Bool, public: true, label: () => _('Enable note history') }, - 'revisionService.ttlDays': { - section: 'revisionService', - value: 90, - type: SettingItemType.Int, - public: true, - minimum: 1, - maximum: 365 * 2, - step: 1, - unitLabel: (value: number = null) => { - 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 }, - - 'welcome.wasBuilt': { value: false, type: SettingItemType.Bool, public: false }, - 'welcome.enabled': { value: true, type: SettingItemType.Bool, public: false }, - - 'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] }, - 'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] }, - - 'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false }, - 'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false }, // Depreciated in favour of spellChecker.languages. - 'spellChecker.languages': { value: [], type: SettingItemType.Array, isGlobal: true, storage: SettingStorage.File, public: false }, - - windowContentZoomFactor: { - value: 100, - type: SettingItemType.Int, - public: false, - appTypes: [AppType.Desktop], - minimum: 30, - maximum: 300, - step: 10, - storage: SettingStorage.File, - isGlobal: true, - }, - - 'layout.folderList.factor': { - value: 1, - type: SettingItemType.Int, - section: 'appearance', - public: true, - appTypes: [AppType.Cli], - label: () => _('Notebook list growth factor'), - 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.'), - storage: SettingStorage.File, - isGlobal: true, - }, - 'layout.noteList.factor': { - value: 1, - type: SettingItemType.Int, - section: 'appearance', - public: true, - appTypes: [AppType.Cli], - label: () => _('Note list growth factor'), - 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.'), - storage: SettingStorage.File, - isGlobal: true, - }, - 'layout.note.factor': { - value: 2, - type: SettingItemType.Int, - section: 'appearance', - public: true, - appTypes: [AppType.Cli], - label: () => _('Note area growth factor'), - 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.'), - storage: SettingStorage.File, - isGlobal: true, - }, - - 'syncInfoCache': { - value: '', - type: SettingItemType.String, - public: false, - }, - - isSafeMode: { - value: false, - type: SettingItemType.Bool, - public: false, - appTypes: [AppType.Desktop], - storage: SettingStorage.Database, - }, - - lastSettingDefaultMigration: { - value: -1, - type: SettingItemType.Int, - public: false, - }, - - wasClosedSuccessfully: { - value: true, - type: SettingItemType.Bool, - public: false, - }, - - installedDefaultPlugins: { - value: [], - type: SettingItemType.Array, - public: false, - storage: SettingStorage.Database, - }, - - // The biometrics feature is disabled by default and marked as beta - // because it seems to cause a freeze or slow down startup on - // certain devices. May be the reason for: - // - // - https://discourse.joplinapp.org/t/on-android-when-joplin-gets-started-offline/29951/1 - // - https://github.com/laurent22/joplin/issues/7956 - 'security.biometricsEnabled': { - value: false, - type: SettingItemType.Bool, - label: () => `${_('Use biometrics to secure access to the app')} (Beta)`, - description: () => 'Important: This is a beta feature and it is not compatible with certain devices. If the app no longer starts after enabling this or is very slow to start, please uninstall and reinstall the app.', - public: true, - appTypes: [AppType.Mobile], - }, - - 'security.biometricsSupportedSensors': { - value: '', - type: SettingItemType.String, - public: false, - appTypes: [AppType.Mobile], - }, - - 'security.biometricsInitialPromptDone': { - value: false, - type: SettingItemType.Bool, - public: false, - appTypes: [AppType.Mobile], - }, - - // 'featureFlag.syncAccurateTimestamps': { - // value: false, - // type: SettingItemType.Bool, - // public: false, - // storage: SettingStorage.File, - // }, - - // 'featureFlag.syncMultiPut': { - // value: false, - // type: SettingItemType.Bool, - // public: false, - // storage: SettingStorage.File, - // }, - - 'sync.allowUnsupportedProviders': { - value: -1, - type: SettingItemType.Int, - public: false, - }, - - 'sync.shareCache': { - value: null, - type: SettingItemType.String, - public: false, - }, - - 'voiceTypingBaseUrl': { - value: '', - type: SettingItemType.String, - public: true, - appTypes: [AppType.Mobile], - description: () => _('Leave it blank to download the language files from the default website'), - label: () => _('Voice typing language files (URL)'), - section: 'note', - }, - - 'trash.autoDeletionEnabled': { - value: true, - type: SettingItemType.Bool, - public: true, - label: () => _('Automatically delete notes in the trash after a number of days'), - storage: SettingStorage.File, - isGlobal: false, - }, - - 'trash.ttlDays': { - value: 90, - type: SettingItemType.Int, - public: true, - minimum: 1, - maximum: 300, - step: 1, - unitLabel: (value: number = null) => { - return value === null ? _('days') : _('%d days', value); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - show: (settings: any) => settings['trash.autoDeletionEnabled'], - label: () => _('Keep notes in the trash for'), - storage: SettingStorage.File, - isGlobal: false, - }, - }; - + this.buildInMetadata_ = builtInMetadata(this); this.metadata_ = { ...this.buildInMetadata_ }; this.metadata_ = { ...this.metadata_, ...this.customMetadata_ }; @@ -2259,15 +632,12 @@ class Setting extends BaseModel { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public static setConstant(key: string, value: any) { + public static setConstant(key: T, value: Constants[T]) { if (!(key in this.constants_)) throw new Error(`Unknown constant key: ${key}`); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - (this.constants_ as any)[key] = value; + this.constants_[key] = value; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public static setValue(key: string, value: any) { + public static setValue(key: T, value: SettingValueType) { if (!this.cache_) throw new Error('Settings have not been initialized!'); value = this.formatValue(key, value); @@ -2291,8 +661,10 @@ class Setting extends BaseModel { // Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs // logger.info('Setting: ' + key + ' = ' + c.value + ' => ' + value); - if ('minimum' in md && value < md.minimum) value = md.minimum; - if ('maximum' in md && value > md.maximum) value = md.maximum; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied + if ('minimum' in md && value < md.minimum) value = md.minimum as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied + if ('maximum' in md && value > md.maximum) value = md.maximum as any; c.value = value; @@ -2461,7 +833,7 @@ class Setting extends BaseModel { throw new Error(`Unhandled value type: ${type}`); } - public static value(key: string) { + public static value(key: T): SettingValueType { // 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 // an object and change a key, the objects will be detected as equal. By returning a copy @@ -2495,8 +867,7 @@ class Setting extends BaseModel { } // This function returns the default value if the setting key does not exist. - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public static valueNoThrow(key: string, defaultValue: any) { + public static valueNoThrow(key: T, defaultValue: SettingValueType): SettingValueType { if (!this.keyExists(key)) return defaultValue; return this.value(key); } @@ -2554,7 +925,7 @@ class Setting extends BaseModel { // and baseKey is 'sync.5', the function will return // { path: 'http://example', username: 'testing' } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public static subValues(baseKey: string, settings: any, options: any = null) { + public static subValues(baseKey: string, settings: Partial, options: any = null) { const includeBaseKeyInName = !!options && !!options.includeBaseKeyInName; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -2709,8 +1080,7 @@ class Setting extends BaseModel { const metadata = this.metadata(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const output: any = {}; + const output: Partial = {}; for (const key in metadata) { if (!metadata.hasOwnProperty(key)) continue; const s = { ...metadata[key] }; @@ -2719,10 +1089,10 @@ class Setting extends BaseModel { s.value = this.value(key); output[key] = s; } - return output; + return output as SettingsRecord; } - public static typeToString(typeId: number) { + public static typeToString(typeId: SettingItemType) { if (typeId === SettingItemType.Int) return 'int'; if (typeId === SettingItemType.String) return 'string'; if (typeId === SettingItemType.Bool) return 'bool'; diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts new file mode 100644 index 0000000000..4b14d6aeca --- /dev/null +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -0,0 +1,1573 @@ +import { rtrimSlashes } from '@joplin/utils/path'; +import SyncTargetRegistry from '../../SyncTargetRegistry'; +import { _, defaultLocale, supportedLocalesToLanguages } from '../../locale'; +import shim from '../../shim'; +import time from '../../time'; +import type SettingType from '../Setting'; +import { AppType, SettingItemSubType, SettingItemType, SettingStorage, SyncStartupOperation, SettingItem } from './types'; +import { defaultListColumns } from '../../services/plugins/api/noteListType'; +import type { PluginSettings } from '../../services/plugins/PluginService'; +const ObjectUtils = require('../../ObjectUtils'); +const { toTitleCase } = require('../../string-utils.js'); + +const customCssFilePath = (Setting: typeof SettingType, filename: string): string => { + return `${Setting.value('rootProfileDir')}/${filename}`; +}; + +const builtInMetadata = (Setting: typeof SettingType) => { + const platform = shim.platformName(); + const mobilePlatform = shim.mobilePlatform(); + + let wysiwygYes = ''; + let wysiwygNo = ''; + if (shim.isElectron()) { + wysiwygYes = ` ${_('(wysiwyg: %s)', _('yes'))}`; + wysiwygNo = ` ${_('(wysiwyg: %s)', _('no'))}`; + } + + const emptyDirWarning = _('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://joplinapp.org/help/faq'); + + // 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. + + const themeOptions = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const output: any = {}; + output[Setting.THEME_LIGHT] = _('Light'); + output[Setting.THEME_DARK] = _('Dark'); + output[Setting.THEME_DRACULA] = _('Dracula'); + output[Setting.THEME_SOLARIZED_LIGHT] = _('Solarised Light'); + output[Setting.THEME_SOLARIZED_DARK] = _('Solarised Dark'); + output[Setting.THEME_NORD] = _('Nord'); + output[Setting.THEME_ARITIM_DARK] = _('Aritim Dark'); + output[Setting.THEME_OLED_DARK] = _('OLED Dark'); + return output; + }; + + return { + 'clientId': { + value: '', + type: SettingItemType.String, + public: false, + }, + 'editor.codeView': { + value: true, + type: SettingItemType.Bool, + public: false, + appTypes: [AppType.Desktop], + storage: SettingStorage.File, + isGlobal: true, + }, + + 'sync.openSyncWizard': { + value: null as boolean, + type: SettingItemType.Button, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Open Sync Wizard...'), + hideLabel: true, + section: 'sync', + }, + + 'sync.target': { + value: 0, + type: SettingItemType.Int, + isEnum: true, + public: true, + section: 'sync', + label: () => _('Synchronisation target'), + description: (appType: AppType) => { + return appType !== 'cli' ? null : _('The target to synchronise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).'); + }, + options: () => { + return SyncTargetRegistry.idAndLabelPlainObject(platform); + }, + optionsOrder: () => { + return SyncTargetRegistry.optionsOrder(); + }, + storage: SettingStorage.File, + }, + + 'sync.upgradeState': { + value: Setting.SYNC_UPGRADE_STATE_IDLE, + type: SettingItemType.Int, + public: false, + }, + + 'sync.startupOperation': { + value: SyncStartupOperation.None, + type: SettingItemType.Int, + public: false, + }, + + 'sync.2.path': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + try { + return settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem'); + } catch (error) { + return false; + } + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + filter: (value: any) => { + return value ? rtrimSlashes(value) : ''; + }, + public: true, + label: () => _('Directory to synchronise with (absolute path)'), + description: () => emptyDirWarning, + storage: SettingStorage.File, + }, + + 'sync.5.path': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); + }, + public: true, + label: () => _('Nextcloud WebDAV URL'), + description: () => emptyDirWarning, + storage: SettingStorage.File, + }, + 'sync.5.username': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); + }, + public: true, + label: () => _('Nextcloud username'), + storage: SettingStorage.File, + }, + 'sync.5.password': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('nextcloud'); + }, + public: true, + label: () => _('Nextcloud password'), + secure: true, + }, + + 'sync.6.path': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); + }, + public: true, + label: () => _('WebDAV URL'), + description: () => emptyDirWarning, + storage: SettingStorage.File, + }, + 'sync.6.username': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); + }, + public: true, + label: () => _('WebDAV username'), + storage: SettingStorage.File, + }, + 'sync.6.password': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('webdav'); + }, + public: true, + label: () => _('WebDAV password'), + secure: true, + }, + + 'sync.8.path': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + try { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + } catch (error) { + return false; + } + }, + filter: value => { + return value ? rtrimSlashes(value) : ''; + }, + public: true, + label: () => _('S3 bucket'), + description: () => emptyDirWarning, + storage: SettingStorage.File, + }, + 'sync.8.url': { + value: 'https://s3.amazonaws.com/', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + }, + filter: value => { + return value ? value.trim() : ''; + }, + public: true, + label: () => _('S3 URL'), + storage: SettingStorage.File, + }, + 'sync.8.region': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + }, + filter: value => { + return value ? value.trim() : ''; + }, + public: true, + label: () => _('S3 region'), + storage: SettingStorage.File, + }, + 'sync.8.username': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + }, + public: true, + label: () => _('S3 access key'), + storage: SettingStorage.File, + }, + 'sync.8.password': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + }, + public: true, + label: () => _('S3 secret key'), + secure: true, + }, + 'sync.8.forcePathStyle': { + value: false, + type: SettingItemType.Bool, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('amazon_s3'); + }, + public: true, + label: () => _('Force path style'), + storage: SettingStorage.File, + }, + 'sync.9.path': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); + }, + public: true, + label: () => _('Joplin Server URL'), + description: () => emptyDirWarning, + storage: SettingStorage.File, + }, + 'sync.9.userContentPath': { + value: '', + type: SettingItemType.String, + public: false, + storage: SettingStorage.Database, + }, + 'sync.9.username': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); + }, + public: true, + label: () => _('Joplin Server email'), + storage: SettingStorage.File, + }, + 'sync.9.password': { + value: '', + type: SettingItemType.String, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServer'); + }, + public: true, + label: () => _('Joplin Server password'), + secure: true, + }, + + // Although sync.10.path is essentially a constant, we still define + // it here so that both Joplin Server and Joplin Cloud can be + // handled in the same consistent way. Also having it a setting + // means it can be set to something else for development. + 'sync.10.path': { + value: 'https://api.joplincloud.com', + type: SettingItemType.String, + public: false, + storage: SettingStorage.Database, + }, + 'sync.10.userContentPath': { + value: 'https://joplinusercontent.com', + type: SettingItemType.String, + public: false, + storage: SettingStorage.Database, + }, + 'sync.10.website': { + value: 'https://joplincloud.com', + type: SettingItemType.String, + public: false, + storage: SettingStorage.Database, + }, + 'sync.10.username': { + value: '', + type: SettingItemType.String, + public: false, + storage: SettingStorage.File, + }, + 'sync.10.password': { + value: '', + type: SettingItemType.String, + public: false, + secure: true, + }, + + 'sync.10.inboxEmail': { value: '', type: SettingItemType.String, public: false }, + + 'sync.10.inboxId': { value: '', type: SettingItemType.String, public: false }, + + 'sync.10.canUseSharePermissions': { value: false, type: SettingItemType.Bool, public: false }, + + 'sync.10.accountType': { value: 0, type: SettingItemType.Int, public: false }, + + 'sync.10.userEmail': { value: '', type: SettingItemType.String, public: false }, + + 'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false }, + + 'sync.resourceDownloadMode': { + value: 'always', + type: SettingItemType.String, + section: 'sync', + public: true, + advanced: true, + isEnum: true, + appTypes: [AppType.Mobile, AppType.Desktop], + label: () => _('Attachment download behaviour'), + 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.'), + options: () => { + return { + always: _('Always'), + manual: _('Manual'), + auto: _('Auto'), + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + + 'sync.3.auth': { value: '', type: SettingItemType.String, public: false }, + 'sync.4.auth': { value: '', type: SettingItemType.String, public: false }, + 'sync.7.auth': { value: '', type: SettingItemType.String, public: false }, + 'sync.9.auth': { value: '', type: SettingItemType.String, public: false }, + 'sync.10.auth': { value: '', type: SettingItemType.String, public: false }, + 'sync.1.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.2.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.3.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.4.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.5.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.6.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.7.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.8.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.9.context': { value: '', type: SettingItemType.String, public: false }, + 'sync.10.context': { value: '', type: SettingItemType.String, public: false }, + + 'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, 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 + // selected folder. It corresponds in general to the currently selected folder or + // to the last folder that was selected. + activeFolderId: { value: '', type: SettingItemType.String, public: false }, + notesParent: { value: '', type: SettingItemType.String, public: false }, + + richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false }, + + firstStart: { value: true, type: SettingItemType.Bool, public: false }, + locale: { + value: defaultLocale(), + type: SettingItemType.String, + isEnum: true, + public: true, + label: () => _('Language'), + options: () => { + return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true })); + }, + storage: SettingStorage.File, + isGlobal: true, + }, + dateFormat: { + value: Setting.DATE_FORMAT_1, + type: SettingItemType.String, + isEnum: true, + public: true, + label: () => _('Date format'), + options: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const options: any = {}; + 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); + options[Setting.DATE_FORMAT_6] = time.formatMsToLocal(now, Setting.DATE_FORMAT_6); + options[Setting.DATE_FORMAT_7] = time.formatMsToLocal(now, Setting.DATE_FORMAT_7); + options[Setting.DATE_FORMAT_8] = time.formatMsToLocal(now, Setting.DATE_FORMAT_8); + options[Setting.DATE_FORMAT_9] = time.formatMsToLocal(now, Setting.DATE_FORMAT_9); + return options; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + timeFormat: { + value: Setting.TIME_FORMAT_1, + type: SettingItemType.String, + isEnum: true, + public: true, + label: () => _('Time format'), + options: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const options: any = {}; + 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); + options[Setting.TIME_FORMAT_3] = time.formatMsToLocal(now, Setting.TIME_FORMAT_3); + return options; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + + 'ocr.enabled': { + value: false, + type: SettingItemType.Bool, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Enable optical character recognition (OCR)'), + description: () => _('When enabled, the application will scan your attachments and extract the text from it. This will allow you to search for text in these attachments.'), + storage: SettingStorage.File, + isGlobal: true, + }, + + theme: { + value: Setting.THEME_LIGHT, + type: SettingItemType.Int, + public: true, + appTypes: [AppType.Mobile, AppType.Desktop], + show: (settings) => { + return !settings['themeAutoDetect']; + }, + isEnum: true, + label: () => _('Theme'), + section: 'appearance', + options: () => themeOptions(), + storage: SettingStorage.File, + isGlobal: true, + }, + + themeAutoDetect: { + value: false, + type: SettingItemType.Bool, + section: 'appearance', + appTypes: [AppType.Mobile, AppType.Desktop], + public: true, + label: () => _('Automatically switch theme to match system theme'), + storage: SettingStorage.File, + isGlobal: true, + }, + + preferredLightTheme: { + value: Setting.THEME_LIGHT, + type: SettingItemType.Int, + public: true, + show: (settings) => { + return settings['themeAutoDetect']; + }, + appTypes: [AppType.Mobile, AppType.Desktop], + isEnum: true, + label: () => _('Preferred light theme'), + section: 'appearance', + options: () => themeOptions(), + storage: SettingStorage.File, + isGlobal: true, + }, + + preferredDarkTheme: { + value: Setting.THEME_DARK, + type: SettingItemType.Int, + public: true, + show: (settings) => { + return settings['themeAutoDetect']; + }, + appTypes: [AppType.Mobile, AppType.Desktop], + isEnum: true, + label: () => _('Preferred dark theme'), + section: 'appearance', + options: () => themeOptions(), + storage: SettingStorage.File, + isGlobal: true, + }, + + notificationPermission: { + value: '', + type: SettingItemType.String, + public: false, + }, + + showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop, AppType.Cli], label: () => _('Show note counts') }, + + layoutButtonSequence: { + value: Setting.LAYOUT_ALL, + type: SettingItemType.Int, + public: false, + appTypes: [AppType.Desktop], + isEnum: true, + options: () => ({ + [Setting.LAYOUT_ALL]: _('%s / %s / %s', _('Editor'), _('Viewer'), _('Split View')), + [Setting.LAYOUT_EDITOR_VIEWER]: _('%s / %s', _('Editor'), _('Viewer')), + [Setting.LAYOUT_EDITOR_SPLIT]: _('%s / %s', _('Editor'), _('Split View')), + [Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')), + }), + storage: SettingStorage.File, + isGlobal: true, + }, + uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') }, + showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') }, + 'notes.sortOrder.field': { + value: 'user_updated_time', + type: SettingItemType.String, + section: 'note', + isEnum: true, + public: true, + appTypes: [AppType.Cli], + label: () => _('Sort notes by'), + options: () => { + const Note = require('../Note').default; + const noteSortFields = ['user_updated_time', 'user_created_time', 'title', 'order', 'todo_due', 'todo_completed']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const options: any = {}; + for (let i = 0; i < noteSortFields.length; i++) { + options[noteSortFields[i]] = toTitleCase(Note.fieldToLabel(noteSortFields[i])); + } + return options; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + 'editor.autoMatchingBraces': { + value: true, + type: SettingItemType.Bool, + public: true, + section: 'note', + appTypes: [AppType.Desktop], + label: () => _('Auto-pair braces, parentheses, quotations, etc.'), + storage: SettingStorage.File, + isGlobal: true, + }, + 'notes.columns': { + value: defaultListColumns(), + public: false, + type: SettingItemType.Array, + storage: SettingStorage.File, + isGlobal: false, + }, + + 'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] }, + // NOTE: A setting whose name starts with 'notes.sortOrder' is special, + // which implies changing the setting automatically triggers the refresh of notes. + // See lib/BaseApplication.ts/generalMiddleware() for details. + 'notes.sortOrder.buttonsVisible': { + value: true, + type: SettingItemType.Bool, + storage: SettingStorage.File, + section: 'appearance', + public: true, + label: () => _('Show sort order buttons'), + // description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'), + appTypes: [AppType.Desktop], + isGlobal: true, + }, + 'notes.perFieldReversalEnabled': { + value: true, + type: SettingItemType.Bool, + storage: SettingStorage.File, + section: 'note', + public: false, + appTypes: [AppType.Cli, AppType.Desktop], + }, + 'notes.perFieldReverse': { + value: { + user_updated_time: true, + user_created_time: true, + title: false, + order: false, + }, + type: SettingItemType.Object, + storage: SettingStorage.File, + section: 'note', + public: false, + appTypes: [AppType.Cli, AppType.Desktop], + }, + 'notes.perFolderSortOrderEnabled': { + value: true, + type: SettingItemType.Bool, + storage: SettingStorage.File, + section: 'folder', + public: false, + appTypes: [AppType.Cli, AppType.Desktop], + }, + 'notes.perFolderSortOrders': { + value: {} as Record, + type: SettingItemType.Object, + storage: SettingStorage.File, + section: 'folder', + public: false, + appTypes: [AppType.Cli, AppType.Desktop], + }, + 'notes.sharedSortOrder': { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code from before rule was applied. + value: {} as Record, + type: SettingItemType.Object, + section: 'folder', + public: false, + appTypes: [AppType.Cli, AppType.Desktop], + }, + 'folders.sortOrder.field': { + value: 'title', + type: SettingItemType.String, + isEnum: true, + public: true, + appTypes: [AppType.Cli], + label: () => _('Sort notebooks by'), + options: () => { + const Folder = require('../Folder').default; + const folderSortFields = ['title', 'last_note_user_updated_time']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const options: any = {}; + for (let i = 0; i < folderSortFields.length; i++) { + options[folderSortFields[i]] = toTitleCase(Folder.fieldToLabel(folderSortFields[i])); + } + return options; + }, + storage: SettingStorage.File, + }, + 'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] }, + trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Save geo-location with notes') }, + + 'editor.usePlainText': { + value: false, + type: SettingItemType.Bool, + section: 'note', + public: true, + appTypes: [AppType.Mobile], + label: () => 'Use the plain text editor', + description: () => 'The plain text editor has various issues and is no longer supported. If you are having issues with the new editor however you can revert to the old one using this setting.', + storage: SettingStorage.File, + isGlobal: true, + }, + + // Enables/disables spellcheck in the mobile markdown beta editor. + 'editor.mobile.spellcheckEnabled': { + value: true, + type: SettingItemType.Bool, + section: 'note', + public: true, + appTypes: [AppType.Mobile], + label: () => _('Enable spellcheck in the text editor'), + storage: SettingStorage.File, + isGlobal: true, + }, + + 'editor.mobile.toolbarEnabled': { + value: true, + type: SettingItemType.Bool, + section: 'note', + public: true, + appTypes: [AppType.Mobile], + label: () => _('Enable the Markdown toolbar'), + storage: SettingStorage.File, + isGlobal: true, + }, + + // Works around a bug in which additional space is visible beneath the toolbar on some devices. + // See https://github.com/laurent22/joplin/pull/6823 + 'editor.mobile.removeSpaceBelowToolbar': { + value: false, + type: SettingItemType.Bool, + section: 'note', + public: true, + appTypes: [AppType.Mobile], + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => settings['editor.mobile.removeSpaceBelowToolbar'], + label: () => 'Remove extra space below the markdown toolbar', + description: () => 'Works around bug on some devices where the markdown toolbar does not touch the bottom of the screen.', + storage: SettingStorage.File, + isGlobal: true, + }, + + newTodoFocus: { + value: 'title', + type: SettingItemType.String, + section: 'note', + isEnum: true, + public: true, + appTypes: [AppType.Desktop], + label: () => _('When creating a new to-do:'), + options: () => { + return { + title: _('Focus title'), + body: _('Focus body'), + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + newNoteFocus: { + value: 'body', + type: SettingItemType.String, + section: 'note', + isEnum: true, + public: true, + appTypes: [AppType.Desktop], + label: () => _('When creating a new note:'), + options: () => { + return { + title: _('Focus title'), + body: _('Focus body'), + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + imageResizing: { + value: 'alwaysAsk', + type: SettingItemType.String, + section: 'note', + isEnum: true, + public: true, + appTypes: [AppType.Mobile, AppType.Desktop], + label: () => _('Resize large images:'), + description: () => _('Shrink large images before adding them to notes.'), + options: () => { + return { + alwaysAsk: _('Always ask'), + alwaysResize: _('Always resize'), + neverResize: _('Never resize'), + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + + 'notes.listRendererId': { + value: 'compact', + type: SettingItemType.String, + public: false, + appTypes: [AppType.Desktop], + storage: SettingStorage.File, + isGlobal: true, + }, + + 'plugins.states': { + value: {} as PluginSettings, + type: SettingItemType.Object, + section: 'plugins', + public: true, + appTypes: [AppType.Desktop, AppType.Mobile], + needRestart: true, + autoSave: true, + }, + + 'plugins.enableWebviewDebugging': { + value: false, + type: SettingItemType.Bool, + section: 'plugins', + public: true, + appTypes: [AppType.Mobile], + show: (settings) => { + // Hide on iOS due to App Store guidelines. See + // https://github.com/laurent22/joplin/pull/10086 for details. + return shim.mobilePlatform() !== 'ios' && settings['plugins.pluginSupportEnabled']; + }, + needRestart: true, + advanced: true, + + label: () => _('Plugin WebView debugging'), + description: () => _('Allows debugging mobile plugins. See %s for details.', 'https://https://joplinapp.org/help/api/references/mobile_plugin_debugging/'), + }, + + 'plugins.pluginSupportEnabled': { + value: false, + public: true, + autoSave: true, + section: 'plugins', + advanced: true, + type: SettingItemType.Bool, + appTypes: [AppType.Mobile], + label: () => _('Enable plugin support'), + // On mobile, we have a screen that manages this setting when it's disabled. + show: (settings) => settings['plugins.pluginSupportEnabled'], + }, + + 'plugins.devPluginPaths': { + value: '', + type: SettingItemType.String, + section: 'plugins', + public: true, + advanced: true, + appTypes: [AppType.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': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] }, + 'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] }, + // Deprecated + + 'markdown.plugin.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` }, + 'markdown.plugin.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` }, + 'markdown.plugin.linkify': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` }, + + 'markdown.plugin.katex': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` }, + 'markdown.plugin.fountain': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` }, + 'markdown.plugin.mermaid': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` }, + + 'markdown.plugin.audioPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` }, + 'markdown.plugin.videoPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` }, + 'markdown.plugin.pdfViewer': { storage: SettingStorage.File, isGlobal: true, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` }, + 'markdown.plugin.mark': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` }, + 'markdown.plugin.footnote': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` }, + 'markdown.plugin.toc': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` }, + 'markdown.plugin.sub': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` }, + 'markdown.plugin.sup': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` }, + 'markdown.plugin.deflist': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` }, + 'markdown.plugin.abbr': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` }, + 'markdown.plugin.emoji': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` }, + 'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` }, + 'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.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 + // 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: SettingItemType.Bool, + section: 'application', + public: true, + appTypes: [AppType.Desktop], + label: () => _('Show tray icon'), + 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, + isGlobal: true, + }, + + showMenuBar: { + value: true, // Show the menu bar by default + type: SettingItemType.Bool, + public: false, + appTypes: [AppType.Desktop], + }, + + startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') }, + + collapsedFolderIds: { value: [] as string[], type: SettingItemType.Array, public: false }, + + 'keychain.supported': { value: -1, type: SettingItemType.Int, public: false }, + 'db.ftsEnabled': { value: -1, type: SettingItemType.Int, public: false }, + 'db.fuzzySearchEnabled': { value: -1, type: SettingItemType.Int, public: false }, + 'encryption.enabled': { value: false, type: SettingItemType.Bool, public: false }, + 'encryption.activeMasterKeyId': { value: '', type: SettingItemType.String, public: false }, + 'encryption.passwordCache': { + value: {} as Record, + type: SettingItemType.Object, + public: false, + secure: true, + }, + 'encryption.masterPassword': { value: '', type: SettingItemType.String, public: false, secure: true }, + 'encryption.shouldReencrypt': { + value: -1, // will be set on app startup + type: SettingItemType.Int, + public: false, + }, + + 'sync.userId': { + value: '', + type: SettingItemType.String, + public: false, + }, + + // Deprecated in favour of windowContentZoomFactor + 'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 }, + + 'style.editor.fontSize': { + value: 15, + type: SettingItemType.Int, + public: true, + storage: SettingStorage.File, + isGlobal: true, + appTypes: [AppType.Desktop, AppType.Mobile], + section: 'appearance', + label: () => _('Editor font size'), + minimum: 4, + maximum: 50, + step: 1, + }, + 'style.editor.fontFamily': + (mobilePlatform) ? + ({ + value: Setting.FONT_DEFAULT, + type: SettingItemType.String, + isEnum: true, + public: true, + label: () => _('Editor font'), + appTypes: [AppType.Mobile], + section: 'appearance', + options: () => { + // IMPORTANT: The font mapping must match the one in global-styles.js::editorFont() + if (mobilePlatform === 'ios') { + return { + [Setting.FONT_DEFAULT]: _('Default'), + [Setting.FONT_MENLO]: 'Menlo', + [Setting.FONT_COURIER_NEW]: 'Courier New', + [Setting.FONT_AVENIR]: 'Avenir', + }; + } + return { + [Setting.FONT_DEFAULT]: _('Default'), + [Setting.FONT_MONOSPACE]: 'Monospace', + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }) : { + value: '', + type: SettingItemType.String, + public: true, + appTypes: [AppType.Desktop], + section: 'appearance', + label: () => _('Editor font family'), + description: () => + _('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'), + storage: SettingStorage.File, + isGlobal: true, + subType: SettingItemSubType.FontFamily, + }, + 'style.editor.monospaceFontFamily': { + value: '', + type: SettingItemType.String, + public: true, + appTypes: [AppType.Desktop], + section: 'appearance', + label: () => _('Editor monospace font family'), + description: () => + _('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'), + storage: SettingStorage.File, + isGlobal: true, + subType: SettingItemSubType.MonospaceFontFamily, + }, + + 'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') }, + + 'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] }, + + 'ui.lastSelectedPluginPanel': { + value: '', + type: SettingItemType.String, + public: false, + description: () => 'The last selected plugin panel ID in pop-up mode (mobile).', + storage: SettingStorage.Database, + appTypes: [AppType.Mobile], + }, + + // 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 + // serve as a customizable Setting. But because the Setting page is auto- + // generated from this list of settings, there wasn't a really elegant way + // to do that directly in the React markup. + 'style.customCss.renderedMarkdown': { + value: null as string|null, + onClick: () => { + shim.openOrCreateFile( + customCssFilePath(Setting, Setting.customCssFilenames.RENDERED_MARKDOWN), + '/* For styling the rendered Markdown */', + ); + }, + type: SettingItemType.Button, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Custom stylesheet for rendered Markdown'), + section: 'appearance', + advanced: true, + storage: SettingStorage.File, + isGlobal: true, + }, + 'style.customCss.joplinApp': { + value: null as string, + onClick: () => { + shim.openOrCreateFile( + customCssFilePath(Setting, Setting.customCssFilenames.JOPLIN_APP), + `/* For styling the entire Joplin app (except the rendered Markdown, which is defined in \`${Setting.customCssFilenames.RENDERED_MARKDOWN}\`) */`, + ); + }, + type: SettingItemType.Button, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Custom stylesheet for Joplin-wide app styles'), + section: 'appearance', + advanced: true, + 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.', + storage: SettingStorage.File, + isGlobal: true, + }, + + 'sync.clearLocalSyncStateButton': { + value: null as null, + type: SettingItemType.Button, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Re-upload local data to sync target'), + section: 'sync', + advanced: true, + description: () => 'If the data on the sync target is incorrect or empty, you can use this button to force a re-upload of your data to the sync target. Application will have to be restarted', + }, + + 'sync.clearLocalDataButton': { + value: null as null, + type: SettingItemType.Button, + public: true, + appTypes: [AppType.Desktop], + label: () => _('Delete local data and re-download from sync target'), + section: 'sync', + advanced: true, + description: () => 'If the data on the sync target is correct but your local data is not, you can use this button to clear your local data and force re-downloading everything from the sync target. As your local data will be deleted first, it is recommended to export your data as JEX first. Application will have to be restarted', + }, + + + autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') }, + 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') }, + + 'autoUploadCrashDumps': { + value: false, + section: 'application', + type: SettingItemType.Bool, + public: true, + appTypes: [AppType.Desktop], + label: () => 'Automatically upload crash reports', + description: () => 'If you experience a crash, please enable this option to automatically send crash reports. You will need to restart the application for this change to take effect.', + isGlobal: true, + storage: SettingStorage.File, + }, + + 'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false }, + 'sync.interval': { + value: 300, + type: SettingItemType.Int, + section: 'sync', + 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), + }; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + 'sync.mobileWifiOnly': { + value: false, + type: SettingItemType.Bool, + section: 'sync', + public: true, + label: () => _('Synchronise only over WiFi connection'), + storage: SettingStorage.File, + appTypes: [AppType.Mobile], + isGlobal: true, + }, + noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] }, + tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] }, + folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] }, + editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.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, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => { + return { + 'A4': _('A4'), + 'Letter': _('Letter'), + 'A3': _('A3'), + 'A5': _('A5'), + 'Tabloid': _('Tabloid'), + 'Legal': _('Legal'), + }; + } }, + 'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, isGlobal: true, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => { + return { + 'portrait': _('Portrait'), + 'landscape': _('Landscape'), + }; + } }, + + useCustomPdfViewer: { + value: false, + type: SettingItemType.Bool, + public: false, + advanced: true, + appTypes: [AppType.Desktop], + label: () => 'Use custom PDF viewer (Beta)', + description: () => 'The custom PDF viewer remembers the last page that was viewed, however it has some technical issues.', + storage: SettingStorage.File, + isGlobal: true, + }, + + 'editor.keyboardMode': { + value: '', + type: SettingItemType.String, + public: true, + appTypes: [AppType.Desktop], + isEnum: true, + advanced: true, + label: () => _('Keyboard Mode'), + options: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const output: any = {}; + output[''] = _('Default'); + output['emacs'] = _('Emacs'); + output['vim'] = _('Vim'); + return output; + }, + storage: SettingStorage.File, + isGlobal: true, + }, + + 'editor.spellcheckBeta': { + value: false, + type: SettingItemType.Bool, + public: true, + appTypes: [AppType.Desktop], + label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)', + 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)', + storage: SettingStorage.File, + isGlobal: true, + }, + + 'imageeditor.jsdrawToolbar': { + value: '', + type: SettingItemType.String, + public: false, + appTypes: [AppType.Mobile], + label: () => '', + storage: SettingStorage.File, + }, + + 'imageeditor.imageTemplate': { + value: '{ }', + type: SettingItemType.String, + public: false, + appTypes: [AppType.Mobile], + label: () => 'Template for the image editor', + storage: SettingStorage.File, + }, + + // 2023-09-07: This setting is now used to track the desktop beta editor. It + // was used to track the mobile beta editor previously. + 'editor.beta': { + value: false, + type: SettingItemType.Bool, + section: 'general', + public: true, + appTypes: [AppType.Desktop], + label: () => 'Opt-in to the editor beta', + description: () => 'This beta adds improved accessibility and plugin API compatibility with the mobile editor. If you find bugs, please report them in the Discourse forum.', + storage: SettingStorage.File, + isGlobal: true, + }, + + 'linking.extraAllowedExtensions': { + value: [] as string[], + type: SettingItemType.Array, + public: false, + appTypes: [AppType.Desktop], + label: () => 'Additional file types that can be opened without confirmation.', + storage: SettingStorage.File, + }, + + 'net.customCertificates': { + value: '', + type: SettingItemType.String, + section: 'sync', + advanced: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return [ + SyncTargetRegistry.nameToId('amazon_s3'), + SyncTargetRegistry.nameToId('nextcloud'), + SyncTargetRegistry.nameToId('webdav'), + SyncTargetRegistry.nameToId('joplinServer'), + ].indexOf(settings['sync.target']) >= 0; + }, + public: true, + appTypes: [AppType.Desktop, AppType.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, + type: SettingItemType.Bool, + advanced: true, + section: 'sync', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => { + return (shim.isNode() || shim.mobilePlatform() === 'android') && + [ + SyncTargetRegistry.nameToId('amazon_s3'), + SyncTargetRegistry.nameToId('nextcloud'), + SyncTargetRegistry.nameToId('webdav'), + SyncTargetRegistry.nameToId('joplinServer'), + // Needs to be enabled for Joplin Cloud too because + // some companies filter all traffic and swap TLS + // certificates, which result in error + // UNABLE_TO_GET_ISSUER_CERT_LOCALLY + SyncTargetRegistry.nameToId('joplinCloud'), + ].indexOf(settings['sync.target']) >= 0; + }, + public: true, + label: () => _('Ignore TLS certificate errors'), + storage: SettingStorage.File, + }, + 'net.proxyEnabled': { + value: false, + type: SettingItemType.Bool, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('Proxy enabled'), + storage: SettingStorage.File, + }, + 'net.proxyUrl': { + value: '', + type: SettingItemType.String, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('Proxy URL'), + description: () => _('For example "%s"', 'http://my.proxy.com:80'), + storage: SettingStorage.File, + }, + 'net.proxyTimeout': { + value: 1, + type: SettingItemType.Int, + maximum: 60, + advanced: true, + section: 'sync', + isGlobal: true, + public: true, + label: () => _('Proxy timeout (seconds)'), + storage: SettingStorage.File, + }, + 'sync.wipeOutFailSafe': { + value: true, + type: SettingItemType.Bool, + advanced: true, + public: true, + 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 as string|null, + type: SettingItemType.String, + public: false, + storage: SettingStorage.File, + isGlobal: true, + }, + 'api.port': { + value: null as number|null, + type: SettingItemType.Int, + storage: SettingStorage.File, + isGlobal: true, + public: true, + appTypes: [AppType.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 }, + 'revisionService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false }, + + 'searchEngine.initialIndexingDone': { value: false, type: SettingItemType.Bool, public: false }, + 'searchEngine.lastProcessedResource': { value: '', type: SettingItemType.String, public: false }, + + 'revisionService.enabled': { section: 'revisionService', storage: SettingStorage.File, value: true, type: SettingItemType.Bool, public: true, label: () => _('Enable note history') }, + 'revisionService.ttlDays': { + section: 'revisionService', + value: 90, + type: SettingItemType.Int, + public: true, + minimum: 1, + maximum: 365 * 2, + step: 1, + unitLabel: (value: number = null) => { + 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 }, + + 'welcome.wasBuilt': { value: false, type: SettingItemType.Bool, public: false }, + 'welcome.enabled': { value: true, type: SettingItemType.Bool, public: false }, + + 'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] }, + 'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] }, + + 'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false }, + 'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false }, // Depreciated in favour of spellChecker.languages. + 'spellChecker.languages': { value: [] as string[], type: SettingItemType.Array, isGlobal: true, storage: SettingStorage.File, public: false }, + + windowContentZoomFactor: { + value: 100, + type: SettingItemType.Int, + public: false, + appTypes: [AppType.Desktop], + minimum: 30, + maximum: 300, + step: 10, + storage: SettingStorage.File, + isGlobal: true, + }, + + 'layout.folderList.factor': { + value: 1, + type: SettingItemType.Int, + section: 'appearance', + public: true, + appTypes: [AppType.Cli], + label: () => _('Notebook list growth factor'), + 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.'), + storage: SettingStorage.File, + isGlobal: true, + }, + 'layout.noteList.factor': { + value: 1, + type: SettingItemType.Int, + section: 'appearance', + public: true, + appTypes: [AppType.Cli], + label: () => _('Note list growth factor'), + 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.'), + storage: SettingStorage.File, + isGlobal: true, + }, + 'layout.note.factor': { + value: 2, + type: SettingItemType.Int, + section: 'appearance', + public: true, + appTypes: [AppType.Cli], + label: () => _('Note area growth factor'), + 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.'), + storage: SettingStorage.File, + isGlobal: true, + }, + + 'syncInfoCache': { + value: '', + type: SettingItemType.String, + public: false, + }, + + isSafeMode: { + value: false, + type: SettingItemType.Bool, + public: false, + appTypes: [AppType.Desktop], + storage: SettingStorage.Database, + }, + + lastSettingDefaultMigration: { + value: -1, + type: SettingItemType.Int, + public: false, + }, + + wasClosedSuccessfully: { + value: true, + type: SettingItemType.Bool, + public: false, + }, + + installedDefaultPlugins: { + value: [] as string[], + type: SettingItemType.Array, + public: false, + storage: SettingStorage.Database, + }, + + // The biometrics feature is disabled by default and marked as beta + // because it seems to cause a freeze or slow down startup on + // certain devices. May be the reason for: + // + // - https://discourse.joplinapp.org/t/on-android-when-joplin-gets-started-offline/29951/1 + // - https://github.com/laurent22/joplin/issues/7956 + 'security.biometricsEnabled': { + value: false, + type: SettingItemType.Bool, + label: () => `${_('Use biometrics to secure access to the app')} (Beta)`, + description: () => 'Important: This is a beta feature and it is not compatible with certain devices. If the app no longer starts after enabling this or is very slow to start, please uninstall and reinstall the app.', + public: true, + appTypes: [AppType.Mobile], + }, + + 'security.biometricsSupportedSensors': { + value: '', + type: SettingItemType.String, + public: false, + appTypes: [AppType.Mobile], + }, + + 'security.biometricsInitialPromptDone': { + value: false, + type: SettingItemType.Bool, + public: false, + appTypes: [AppType.Mobile], + }, + + // 'featureFlag.syncAccurateTimestamps': { + // value: false, + // type: SettingItemType.Bool, + // public: false, + // storage: SettingStorage.File, + // }, + + // 'featureFlag.syncMultiPut': { + // value: false, + // type: SettingItemType.Bool, + // public: false, + // storage: SettingStorage.File, + // }, + + 'sync.allowUnsupportedProviders': { + value: -1, + type: SettingItemType.Int, + public: false, + }, + + 'sync.shareCache': { + value: null as string|null, + type: SettingItemType.String, + public: false, + }, + + 'voiceTypingBaseUrl': { + value: '', + type: SettingItemType.String, + public: true, + appTypes: [AppType.Mobile], + description: () => _('Leave it blank to download the language files from the default website'), + label: () => _('Voice typing language files (URL)'), + section: 'note', + }, + + 'trash.autoDeletionEnabled': { + value: true, + type: SettingItemType.Bool, + public: true, + label: () => _('Automatically delete notes in the trash after a number of days'), + storage: SettingStorage.File, + isGlobal: false, + }, + + 'trash.ttlDays': { + value: 90, + type: SettingItemType.Int, + public: true, + minimum: 1, + maximum: 300, + step: 1, + unitLabel: (value: number = null) => { + return value === null ? _('days') : _('%d days', value); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show: (settings: any) => settings['trash.autoDeletionEnabled'], + label: () => _('Keep notes in the trash for'), + storage: SettingStorage.File, + isGlobal: false, + }, + } satisfies Record; +}; + +export type BuiltInMetadataKeys = keyof ReturnType; +export type BuiltInMetadataValues = { + [key in BuiltInMetadataKeys]: ReturnType[key]['value']; +}; + +export default builtInMetadata; diff --git a/packages/lib/models/settings/types.ts b/packages/lib/models/settings/types.ts new file mode 100644 index 0000000000..e76d3fc703 --- /dev/null +++ b/packages/lib/models/settings/types.ts @@ -0,0 +1,108 @@ +import type { BuiltInMetadataValues } from './builtInMetadata'; + +export enum SettingItemType { + Int = 1, + String = 2, + Bool = 3, + Array = 4, + Object = 5, + Button = 6, +} + +export enum SettingItemSubType { + FilePathAndArgs = 'file_path_and_args', + FilePath = 'file_path', // Not supported on mobile! + DirectoryPath = 'directory_path', // Not supported on mobile! + FontFamily = 'font_family', + MonospaceFontFamily = 'monospace_font_family', +} + +export enum SettingStorage { + Database = 1, + File = 2, +} + + +// This is the definition of a setting item +export interface SettingItem { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + value: any; + type: SettingItemType; + public: boolean; + + subType?: string; + key?: string; + isEnum?: boolean; + section?: string; + label?(): string; + description?: (_appType: AppType)=> string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + options?(): any; + optionsOrder?(): string[]; + appTypes?: AppType[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + show?(settings: any): boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + filter?(value: any): any; + secure?: boolean; + advanced?: boolean; + minimum?: number; + maximum?: number; + step?: number; + onClick?(): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied + unitLabel?: (value: any)=> string; + needRestart?: boolean; + autoSave?: boolean; + storage?: SettingStorage; + hideLabel?: boolean; + + // In a multi-profile context, all settings are by default local - they take + // their value from the current profile. This flag can be set to specify + // that the setting is global and that its value should come from the root + // profile. This flag only applies to sub-profiles. + // + // At the moment, all global settings must be saved to file (have the + // storage attribute set to "file") because it's simpler to load the root + // profile settings.json than load the whole SQLite database. This + // restriction is not an issue normally since all settings that are + // considered global are also the user-facing ones. + isGlobal?: boolean; +} + +export interface SettingItems { + [key: string]: SettingItem; +} + +export enum SettingSectionSource { + Default = 1, + Plugin = 2, +} + +export interface SettingSection { + label: string; + iconName?: string; + description?: string; + name?: string; + source?: SettingSectionSource; +} + +export enum SyncStartupOperation { + None = 0, + ClearLocalSyncState = 1, + ClearLocalData = 2, +} + +export enum Env { + Undefined = 'SET_ME', + Dev = 'dev', + Prod = 'prod', +} + +export enum AppType { + Desktop = 'desktop', + Mobile = 'mobile', + Cli = 'cli', +} + +export type SettingsRecord = BuiltInMetadataValues & { [key: string]: unknown }; diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index d7ae98f2ac..ba230e4ec8 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -12,6 +12,7 @@ import { getListRendererIds } from './services/noteList/renderers'; import { ProcessResultsRow } from './services/search/SearchEngine'; import { getDisplayParentId } from './services/trash'; import Logger from '@joplin/utils/Logger'; +import { SettingsRecord } from './models/settings/types'; const fastDeepEqual = require('fast-deep-equal'); const { ALL_NOTES_FILTER_ID } = require('./reserved-ids'); const { createSelectorCreator, defaultMemoize } = require('reselect'); @@ -100,8 +101,7 @@ export interface State { syncReport: any; searchQuery: string; searchResults: ProcessResultsRow[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - settings: Record; + settings: Partial; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sharedData: any; appState: string; diff --git a/packages/lib/registry.test.ts b/packages/lib/registry.test.ts index 599bdde2f9..6565280801 100644 --- a/packages/lib/registry.test.ts +++ b/packages/lib/registry.test.ts @@ -1,4 +1,4 @@ -import Setting from './models/Setting'; +import Setting, { Env } from './models/Setting'; import { reg } from './registry'; const sync = { @@ -9,7 +9,7 @@ describe('Registry', () => { let originalSyncTarget: typeof reg.syncTarget; beforeAll(() => { - Setting.setConstant('env', 'prod'); + Setting.setConstant('env', Env.Prod); originalSyncTarget = reg.syncTarget; reg.syncTarget = () => ({ isAuthenticated: () => true, @@ -18,7 +18,7 @@ describe('Registry', () => { }); afterAll(() => { - Setting.setConstant('env', 'dev'); + Setting.setConstant('env', Env.Dev); reg.syncTarget = originalSyncTarget; }); diff --git a/packages/lib/services/plugins/PluginService.ts b/packages/lib/services/plugins/PluginService.ts index 8c70487f2b..083c61dff2 100644 --- a/packages/lib/services/plugins/PluginService.ts +++ b/packages/lib/services/plugins/PluginService.ts @@ -215,8 +215,8 @@ export default class PluginService extends BaseService { return output as PluginSettings; } - public serializePluginSettings(settings: PluginSettings): string { - return JSON.stringify(settings); + public serializePluginSettings(settings: PluginSettings) { + return settings; } public pluginIdByContentScriptId(contentScriptId: string): string { diff --git a/packages/lib/testing/test-utils.ts b/packages/lib/testing/test-utils.ts index be95b19834..c4867d5215 100644 --- a/packages/lib/testing/test-utils.ts +++ b/packages/lib/testing/test-utils.ts @@ -2,7 +2,7 @@ import BaseApplication from '../BaseApplication'; import BaseModel from '../BaseModel'; import Logger, { TargetType, LoggerWrapper, LogLevel } from '@joplin/utils/Logger'; -import Setting from '../models/Setting'; +import Setting, { AppType, Env } from '../models/Setting'; import BaseService from '../services/BaseService'; import FsDriverNode from '../fs-driver-node'; import time from '../time'; @@ -190,14 +190,14 @@ BaseItem.loadClass('MasterKey', MasterKey); BaseItem.loadClass('Revision', Revision); Setting.setConstant('appId', 'net.cozic.joplintest-cli'); -Setting.setConstant('appType', 'cli'); +Setting.setConstant('appType', AppType.Cli); Setting.setConstant('tempDir', baseTempDir); Setting.setConstant('cacheDir', baseTempDir); Setting.setConstant('resourceDir', baseTempDir); Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`); Setting.setConstant('profileDir', profileDir); Setting.setConstant('rootProfileDir', rootProfileDir); -Setting.setConstant('env', 'dev'); +Setting.setConstant('env', Env.Dev); BaseService.logger_ = logger; @@ -640,7 +640,7 @@ async function initFileApi() { mustRunInBand(); const { parameters, setEnvOverride } = require('../parameters.js'); - Setting.setConstant('env', 'dev'); + Setting.setConstant('env', Env.Dev); setEnvOverride('test'); const config = parameters().oneDriveTest; const api = new OneDriveApi(config.id, config.secret, false);