diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index a9a13e29d..495f10fce 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -1,6 +1,6 @@ "use strict"; -import optionService from "../../services/options.js"; +import optionService, { OptionNames } from "../../services/options.js"; import log from "../../services/log.js"; import searchService from "../../services/search/services/search.js"; import ValidationError from "../../errors/validation_error.js"; @@ -79,7 +79,7 @@ function getOptions() { for (const optionName in optionMap) { if (isAllowed(optionName)) { - resultMap[optionName] = optionMap[optionName]; + resultMap[optionName] = optionMap[optionName as OptionNames]; } } @@ -115,7 +115,7 @@ function update(name: string, value: string) { log.info(`Updating option '${name}' to '${value}'`); } - optionService.setOption(name, value); + optionService.setOption(name as OptionNames, value); if (name === "locale") { // This runs asynchronously, so it's not perfect, but it does the trick for now. diff --git a/src/services/backup.ts b/src/services/backup.ts index f9cad0a02..8973c7136 100644 --- a/src/services/backup.ts +++ b/src/services/backup.ts @@ -1,7 +1,7 @@ "use strict"; import dateUtils from "./date_utils.js"; -import optionService from "./options.js"; +import optionService, { OptionNames } from "./options.js"; import fs from "fs-extra"; import dataDir from "./data_dir.js"; import log from "./log.js"; @@ -38,12 +38,23 @@ function regularBackup() { } function isBackupEnabled(backupType: BackupType) { - const optionName = `${backupType}BackupEnabled`; + let optionName: OptionNames; + switch (backupType) { + case "daily": + optionName = "dailyBackupEnabled"; + break; + case "weekly": + optionName = "weeklyBackupEnabled"; + break; + case "monthly": + optionName = "monthlyBackupEnabled"; + break; + } return optionService.getOptionBool(optionName); } -function periodBackup(optionName: string, backupType: BackupType, periodInSeconds: number) { +function periodBackup(optionName: "lastDailyBackupDate" | "lastWeeklyBackupDate" | "lastMonthlyBackupDate", backupType: BackupType, periodInSeconds: number) { if (!isBackupEnabled(backupType)) { return; } diff --git a/src/services/keyboard_actions_interface.ts b/src/services/keyboard_actions_interface.ts index 80a5ef7d3..9be3f9483 100644 --- a/src/services/keyboard_actions_interface.ts +++ b/src/services/keyboard_actions_interface.ts @@ -92,7 +92,7 @@ const enum KeyboardActionNamesEnum { forceSaveRevision } -type KeyboardActionNames = keyof typeof KeyboardActionNamesEnum; +export type KeyboardActionNames = keyof typeof KeyboardActionNamesEnum; export interface KeyboardShortcut { separator?: string; diff --git a/src/services/options.ts b/src/services/options.ts index 5230f6266..d64f07c03 100644 --- a/src/services/options.ts +++ b/src/services/options.ts @@ -15,14 +15,108 @@ import becca from "../becca/becca.js"; import BOption from "../becca/entities/boption.js"; import { OptionRow } from '../becca/entities/rows.js'; +import { KeyboardActionNames } from "./keyboard_actions_interface.js"; import sql from "./sql.js"; +/** + * For each keyboard action, there is a corresponding option which identifies the key combination defined by the user. + */ +type KeyboardShortcutsOptions = { + [key in T as `keyboardShortcuts${Capitalize}`]: string +}; + +interface OptionDefinitions extends KeyboardShortcutsOptions { + "openNoteContexts": string; + "lastDailyBackupDate": string; + "lastWeeklyBackupDate": string; + "lastMonthlyBackupDate": string; + "dbVersion": string; + "theme": string; + "syncServerHost": string; + "syncServerTimeout": string; + "syncProxy": string; + "mainFontFamily": string; + "treeFontFamily": string; + "detailFontFamily": string; + "monospaceFontFamily": string; + "spellCheckLanguageCode": string; + "codeNotesMimeTypes": string; + "headingStyle": string; + "highlightsList": string; + "customSearchEngineName": string; + "customSearchEngineUrl": string; + "locale": string; + "codeBlockTheme": string; + "textNoteEditorType": string; + "layoutOrientation": string; + "allowedHtmlTags": string; + "documentId": string; + "documentSecret": string; + "passwordVerificationHash": string; + "passwordVerificationSalt": string; + "passwordDerivedKeySalt": string; + "encryptedDataKey": string; + + "lastSyncedPull": number; + "lastSyncedPush": number; + "revisionSnapshotTimeInterval": number; + "revisionSnapshotNumberLimit": number; + "protectedSessionTimeout": number; + "zoomFactor": number; + "mainFontSize": number; + "treeFontSize": number; + "detailFontSize": number; + "monospaceFontSize": number; + "imageMaxWidthHeight": number; + "imageJpegQuality": number; + "leftPaneWidth": number; + "rightPaneWidth": number; + "eraseEntitiesAfterTimeInSeconds": number; + "autoReadonlySizeText": number; + "autoReadonlySizeCode": number; + "maxContentWidth": number; + "minTocHeadings": number; + "eraseUnusedAttachmentsAfterSeconds": number; + "firstDayOfWeek": number; + + "initialized": boolean; + "overrideThemeFonts": boolean; + "spellCheckEnabled": boolean; + "autoFixConsistencyIssues": boolean; + "vimKeymapEnabled": boolean; + "codeLineWrapEnabled": boolean; + "leftPaneVisible": boolean; + "rightPaneVisible": boolean; + "nativeTitleBarVisible": boolean; + "hideArchivedNotes_main": boolean; + "debugModeEnabled": boolean; + "autoCollapseNoteTree": boolean; + "dailyBackupEnabled": boolean; + "weeklyBackupEnabled": boolean; + "monthlyBackupEnabled": boolean; + "compressImages": boolean; + "downloadImagesAutomatically": boolean; + "checkForUpdates": boolean; + "disableTray": boolean; + "promotedAttributesOpenInRibbon": boolean; + "editedNotesOpenInRibbon": boolean; + "codeBlockWordWrap": boolean; + "textNoteEditorMultilineToolbar": boolean; + "backgroundEffects": boolean; +}; + +export type OptionNames = keyof OptionDefinitions; + +type FilterOptionsByType = { + [K in keyof OptionDefinitions]: OptionDefinitions[K] extends U ? K : never; +}[keyof OptionDefinitions]; + /** * A dictionary where the keys are the option keys (e.g. `theme`) and their corresponding values. */ -export type OptionMap = Record; +export type OptionMap = Record; -function getOptionOrNull(name: string): string | null { +function getOptionOrNull(name: OptionNames): string | null { let option; if (becca.loaded) { @@ -35,7 +129,7 @@ function getOptionOrNull(name: string): string | null { return option ? option.value : null; } -function getOption(name: string) { +function getOption(name: OptionNames) { const val = getOptionOrNull(name); if (val === null) { @@ -45,7 +139,7 @@ function getOption(name: string) { return val; } -function getOptionInt(name: string, defaultValue?: number): number { +function getOptionInt(name: FilterOptionsByType, defaultValue?: number): number { const val = getOption(name); const intVal = parseInt(val); @@ -61,7 +155,7 @@ function getOptionInt(name: string, defaultValue?: number): number { return intVal; } -function getOptionBool(name: string): boolean { +function getOptionBool(name: FilterOptionsByType): boolean { const val = getOption(name); if (typeof val !== "string" || !['true', 'false'].includes(val)) { @@ -71,15 +165,11 @@ function getOptionBool(name: string): boolean { return val === 'true'; } -function setOption(name: string, value: string | number | boolean) { - if (value === true || value === false || typeof value === "number") { - value = value.toString(); - } - +function setOption(name: T, value: string | OptionDefinitions[T]) { const option = becca.getOption(name); if (option) { - option.value = value; + option.value = value as string; option.save(); } @@ -95,10 +185,10 @@ function setOption(name: string, value: string | number | boolean) { * @param value the value of the option, as a string. It can then be interpreted as other types such as a number of boolean. * @param isSynced `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme). */ -function createOption(name: string, value: string, isSynced: boolean) { +function createOption(name: T, value: string | OptionDefinitions[T], isSynced: boolean) { new BOption({ name: name, - value: value, + value: value as string, isSynced: isSynced }).save(); } @@ -108,13 +198,13 @@ function getOptions() { } function getOptionMap() { - const map: OptionMap = {}; + const map: Record = {}; for (const option of Object.values(becca.options)) { map[option.name] = option.value; } - return map; + return map as OptionMap; } export default { diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 1835f55b2..cff8493ff 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -1,5 +1,5 @@ import optionService from "./options.js"; -import type { OptionMap } from "./options.js"; +import type { OptionMap, OptionNames } from "./options.js"; import appInfo from "./app_info.js"; import { randomSecureToken, isWindows } from "./utils.js"; import log from "./log.js"; @@ -24,11 +24,11 @@ interface NotSyncedOpts { * Represents a correspondence between an option and its default value, to be initialized when the database is missing that particular option (after a migration from an older version, or when creating a new database). */ interface DefaultOption { - name: string; + name: OptionNames; /** * The value to initialize the option with, if the option is not already present in the database. * - * If a function is passed in instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized. + * If a function is passed Gin instead, the function is called if the option does not exist (with access to the current options) and the return value is used instead. Useful to migrate a new option with a value depending on some other option that might be initialized. */ value: string | ((options: OptionMap) => string); isSynced: boolean; @@ -194,7 +194,7 @@ function getKeyboardDefaultOptions() { name: `keyboardShortcuts${ka.actionName.charAt(0).toUpperCase()}${ka.actionName.slice(1)}`, value: JSON.stringify(ka.defaultShortcuts), isSynced: false - })); + })) as DefaultOption[]; } export default { diff --git a/src/services/sync_options.ts b/src/services/sync_options.ts index 48cf39efd..9b1abfbb2 100644 --- a/src/services/sync_options.ts +++ b/src/services/sync_options.ts @@ -1,6 +1,6 @@ "use strict"; -import optionService from "./options.js"; +import optionService, { OptionNames } from "./options.js"; import config from "./config.js"; /* @@ -10,7 +10,7 @@ import config from "./config.js"; * to live sync server. */ -function get(name: string) { +function get(name: OptionNames) { return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name); }