From 2090d426310aafc51c1c6eb349c087116ae48236 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 3 Jul 2026 10:33:32 +0000 Subject: [PATCH] Add Obsidian setting definition renderer --- .../2026_07_setting_definition_repository.md | 15 ++- src/lib | 2 +- .../SettingDialogue/LiveSyncSetting.ts | 24 ++++- .../ObsidianSettingRenderer.ts | 76 +++++++++++++++ .../features/SettingDialogue/PaneAdvanced.ts | 28 +++--- .../SettingDialogue/PaneCustomisationSync.ts | 13 +-- .../features/SettingDialogue/PaneGeneral.ts | 17 ++-- .../features/SettingDialogue/PaneHatch.ts | 7 +- .../features/SettingDialogue/PanePatches.ts | 46 ++++----- .../SettingDialogue/PanePowerUsers.ts | 31 +++--- .../SettingDialogue/PaneRemoteConfig.ts | 3 +- .../features/SettingDialogue/PaneSelector.ts | 9 +- .../features/SettingDialogue/PaneSetup.ts | 8 +- .../SettingDialogue/PaneSyncSettings.ts | 97 +++++++++---------- .../features/SettingDialogue/SettingPane.ts | 9 +- 15 files changed, 243 insertions(+), 142 deletions(-) create mode 100644 src/modules/features/SettingDialogue/ObsidianSettingRenderer.ts diff --git a/docs/adr/2026_07_setting_definition_repository.md b/docs/adr/2026_07_setting_definition_repository.md index d2a06fc..571b161 100644 --- a/docs/adr/2026_07_setting_definition_repository.md +++ b/docs/adr/2026_07_setting_definition_repository.md @@ -110,6 +110,7 @@ type SettingDefinition = { validate?: (value: unknown, context: SettingEvaluationContext) => SettingValidationResult; coerce?: (value: unknown, context: SettingEvaluationContext) => unknown; affects?: SettingEffect[]; + commit?: SettingCommitPolicy; render?: "auto" | "custom"; }; ``` @@ -127,8 +128,15 @@ consumers. `label` and `description` remain compatibility aliases while existing code still expects resolved strings. `internal` should mark settings that are not currently editable from the UI. -Obsolete settings are internal by default. Persisted settings without UI metadata -are also internal until explicitly classified otherwise. +Obsolete settings are internal by default. Missing UI metadata alone should not +make a setting internal; `kind`, `render`, and explicit `internal` metadata +should decide whether the automatic renderer can safely handle it. + +`commit` should describe when a value is persisted, not how a button is rendered. +Immediate settings can save on change. Explicit settings are held in the editing +buffer until an apply action commits the configured group. This keeps apply +buttons out of the repository while still making grouped save behaviour +testable. ## Storage Domains @@ -273,6 +281,9 @@ adapter or custom workflow. - Replace `isNeedRebuildLocal()` and `isNeedRebuildRemote()` with repository-driven effect calculation. +- Model explicit commit groups for settings that must be applied together, for + example configuration encryption passphrase settings, setting sync file, and + database suffix changes. - Add capability metadata for CouchDB diagnostics, repair, Hidden File Sync, and Obsidian-only plug-in operations. - Use this to improve warnings for database-scoped CouchDB users and diff --git a/src/lib b/src/lib index 72a2f68..c7c5fe2 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 72a2f684d10a77899a35643dfb2929ed81777626 +Subproject commit c7c5fe21be5e4498bb89eb4f6c06eee5f34b4a66 diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index d3a113e..ef8defa 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -8,7 +8,13 @@ import { type ValueComponent, } from "@/deps.ts"; import { unique } from "octagonal-wheels/collection"; -import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@lib/common/types.ts"; +import { + LEVEL_ADVANCED, + LEVEL_POWER_USER, + statusDisplay, + type ConfigurationItem, + type SettingDefinition, +} from "@lib/common/types.ts"; import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import { type AllSettingItemKey, @@ -18,9 +24,21 @@ import { type AllNumericItemKey, type AllBooleanItemKey, } from "./settingConstants.ts"; -import { $msg } from "@lib/common/i18n.ts"; +import { $msg, $t } from "@lib/common/i18n.ts"; import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; +function configurationFromDefinition(definition: SettingDefinition): ConfigurationItem { + return { + name: $t(definition.labelKey), + desc: definition.descriptionKey ? $t(definition.descriptionKey) : undefined, + placeHolder: definition.placeholder, + status: definition.status, + obsolete: definition.obsolete, + level: definition.level, + isHidden: definition.secret, + }; +} + export class LiveSyncSetting extends Setting { autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; applyButtonComponent?: ButtonComponent; @@ -56,7 +74,7 @@ export class LiveSyncSetting extends Setting { return this; } autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption) { - const conf = getConfig(key); + const conf = opt?.settingDefinition ? configurationFromDefinition(opt.settingDefinition) : getConfig(key); if (!conf) { // throw new Error($msg("liveSyncSetting.errorNoSuchSettingItem", { key })); return; diff --git a/src/modules/features/SettingDialogue/ObsidianSettingRenderer.ts b/src/modules/features/SettingDialogue/ObsidianSettingRenderer.ts new file mode 100644 index 0000000..f9c5a8c --- /dev/null +++ b/src/modules/features/SettingDialogue/ObsidianSettingRenderer.ts @@ -0,0 +1,76 @@ +import { + getExplicitSettingCommitGroup, + getSettingDefinition, + type AllBooleanItemKey, + type AllNumericItemKey, + type AllSettingItemKey, + type AllStringItemKey, +} from "./settingConstants.ts"; +import type { LiveSyncSetting } from "./LiveSyncSetting.ts"; +import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import type { AutoWireOption } from "./SettingPane.ts"; + +export type ObsidianSettingRenderOption = AutoWireOption & { + options?: Record; + clampMin?: number; + clampMax?: number; + acceptZero?: boolean; + renderInternal?: boolean; +}; + +export function addObsidianApplyButton(setting: LiveSyncSetting, group: string, text?: string): LiveSyncSetting { + const commitGroup = getExplicitSettingCommitGroup(group); + if (!commitGroup) { + throw new Error(`No explicit setting commit group found for '${group}'`); + } + return setting.addApplyButton(commitGroup.applyKeys, text); +} + +export function renderObsidianApplyButton(containerEl: HTMLElement, group: string, text?: string): LiveSyncSetting { + return addObsidianApplyButton(new Setting(containerEl), group, text); +} + +export function renderObsidianSetting( + containerEl: HTMLElement, + key: AllSettingItemKey, + opt: ObsidianSettingRenderOption = {} +): LiveSyncSetting { + const definition = getSettingDefinition(key); + if (!definition) { + throw new Error(`No setting definition found for '${key}'`); + } + if (definition.internal && !opt.renderInternal) { + throw new Error(`Setting '${key}' is internal and cannot be rendered automatically`); + } + if (definition.render === "custom" || definition.kind === "custom") { + throw new Error(`Setting '${key}' requires a custom renderer`); + } + + const isExplicitCommit = definition.commit?.mode === "explicit"; + const holdValue = opt.holdValue ?? isExplicitCommit; + const setting = new Setting(containerEl); + const wireOption = { + ...opt, + holdValue, + settingDefinition: definition, + }; + + if (opt.options) { + return setting.autoWireDropDown(key as AllStringItemKey, { ...wireOption, options: opt.options }); + } + + switch (definition.kind) { + case "boolean": + return setting.autoWireToggle(key as AllBooleanItemKey, wireOption); + case "number": + return setting.autoWireNumeric(key as AllNumericItemKey, wireOption); + case "password": + return setting.autoWireText(key as AllStringItemKey, { ...wireOption, isPassword: true }); + case "textarea": + return setting.autoWireTextArea(key as AllStringItemKey, wireOption); + case "select": + throw new Error(`Setting '${key}' requires select options`); + case "text": + return setting.autoWireText(key as AllStringItemKey, wireOption); + } +} diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.ts b/src/modules/features/SettingDialogue/PaneAdvanced.ts index 0a4a73c..1e3df41 100644 --- a/src/modules/features/SettingDialogue/PaneAdvanced.ts +++ b/src/modules/features/SettingDialogue/PaneAdvanced.ts @@ -1,41 +1,37 @@ import { ChunkAlgorithmNames } from "@lib/common/types.ts"; -import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { void addPanel(paneEl, "Memory cache").then((paneEl) => { - new Setting(paneEl).autoWireNumeric("hashCacheMaxCount", { clampMin: 10 }); - // new Setting(paneEl).autoWireNumeric("hashCacheMaxAmount", { clampMin: 1 }); + renderObsidianSetting(paneEl, "hashCacheMaxCount", { clampMin: 10 }); + // renderObsidianSetting(paneEl, "hashCacheMaxAmount", { clampMin: 1 }); }); void addPanel(paneEl, "Local Database Tweak").then((paneEl) => { paneEl.addClass("wizardHidden"); const items = ChunkAlgorithmNames; - new Setting(paneEl).autoWireDropDown("chunkSplitterVersion", { + renderObsidianSetting(paneEl, "chunkSplitterVersion", { options: items, }); - new Setting(paneEl).autoWireNumeric("customChunkSize", { clampMin: 0, acceptZero: true }); + renderObsidianSetting(paneEl, "customChunkSize", { clampMin: 0, acceptZero: true }); }); void addPanel(paneEl, "Transfer Tweak").then((paneEl) => { - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("readChunksOnline", { onUpdate: this.onlyOnCouchDB }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("useOnlyLocalChunk", { onUpdate: this.onlyOnCouchDB }); + renderObsidianSetting(paneEl, "readChunksOnline", { onUpdate: this.onlyOnCouchDB }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "useOnlyLocalChunk", { onUpdate: this.onlyOnCouchDB }).setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("concurrencyOfReadChunksOnline", { + renderObsidianSetting(paneEl, "concurrencyOfReadChunksOnline", { clampMin: 10, onUpdate: this.onlyOnCouchDB, - }); + }).setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("minimumIntervalOfReadChunksOnline", { + renderObsidianSetting(paneEl, "minimumIntervalOfReadChunksOnline", { clampMin: 10, onUpdate: this.onlyOnCouchDB, - }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("autoAcceptCompatibleTweak"); + }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "autoAcceptCompatibleTweak").setClass("wizardHidden"); // new Setting(paneEl) // .setClass("wizardHidden") // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) diff --git a/src/modules/features/SettingDialogue/PaneCustomisationSync.ts b/src/modules/features/SettingDialogue/PaneCustomisationSync.ts index 927050d..d7532e3 100644 --- a/src/modules/features/SettingDialogue/PaneCustomisationSync.ts +++ b/src/modules/features/SettingDialogue/PaneCustomisationSync.ts @@ -1,6 +1,7 @@ import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "@/common/events.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { enableOnly, visibleOnly } from "./SettingPane.ts"; export function paneCustomisationSync( @@ -35,27 +36,27 @@ export function paneCustomisationSync( visibleOnly(() => this.isConfiguredAs("usePluginSync", true)) ); - new Setting(paneEl).autoWireText("deviceAndVaultName", { + renderObsidianSetting(paneEl, "deviceAndVaultName", { placeHolder: "desktop", onUpdate: enableOnlyOnPluginSyncIsNotEnabled, }); - new Setting(paneEl).autoWireToggle("usePluginSyncV2"); + renderObsidianSetting(paneEl, "usePluginSyncV2"); - new Setting(paneEl).autoWireToggle("usePluginSync", { + renderObsidianSetting(paneEl, "usePluginSync", { onUpdate: enableOnly(() => !this.isConfiguredAs("deviceAndVaultName", "")), }); - new Setting(paneEl).autoWireToggle("autoSweepPlugins", { + renderObsidianSetting(paneEl, "autoSweepPlugins", { onUpdate: visibleOnlyOnPluginSyncEnabled, }); - new Setting(paneEl).autoWireToggle("autoSweepPluginsPeriodic", { + renderObsidianSetting(paneEl, "autoSweepPluginsPeriodic", { onUpdate: visibleOnly( () => this.isConfiguredAs("usePluginSync", true) && this.isConfiguredAs("autoSweepPlugins", true) ), }); - new Setting(paneEl).autoWireToggle("notifyPluginOrSettingUpdated", { + renderObsidianSetting(paneEl, "notifyPluginOrSettingUpdated", { onUpdate: visibleOnlyOnPluginSyncEnabled, }); diff --git a/src/modules/features/SettingDialogue/PaneGeneral.ts b/src/modules/features/SettingDialogue/PaneGeneral.ts index ccbdce9..65047de 100644 --- a/src/modules/features/SettingDialogue/PaneGeneral.ts +++ b/src/modules/features/SettingDialogue/PaneGeneral.ts @@ -1,6 +1,7 @@ import { $msg, $t } from "@lib/common/i18n.ts"; import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; @@ -16,20 +17,20 @@ export function paneGeneral( // ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")], ...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]), ]) as Record; - new Setting(paneEl).autoWireDropDown("displayLanguage", { + renderObsidianSetting(paneEl, "displayLanguage", { options: languages, }); this.addOnSaved("displayLanguage", () => this.display()); - new Setting(paneEl).autoWireToggle("showStatusOnEditor"); + renderObsidianSetting(paneEl, "showStatusOnEditor"); this.addOnSaved("showStatusOnEditor", () => { eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR); }); - new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", { + renderObsidianSetting(paneEl, "showOnlyIconsOnEditor", { onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)), }); - new Setting(paneEl).autoWireToggle("showStatusOnStatusbar"); - new Setting(paneEl).autoWireToggle("hideFileWarningNotice"); - new Setting(paneEl).autoWireDropDown("networkWarningStyle", { + renderObsidianSetting(paneEl, "showStatusOnStatusbar"); + renderObsidianSetting(paneEl, "hideFileWarningNotice"); + renderObsidianSetting(paneEl, "networkWarningStyle", { options: { [NetworkWarningStyles.BANNER]: "Show full banner", [NetworkWarningStyles.ICON]: "Show icon only", @@ -43,9 +44,9 @@ export function paneGeneral( void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).autoWireToggle("lessInformationInLog"); + renderObsidianSetting(paneEl, "lessInformationInLog"); - new Setting(paneEl).autoWireToggle("showVerboseLog", { + renderObsidianSetting(paneEl, "showVerboseLog", { onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)), }); }); diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index a5b267c..215db53 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -25,6 +25,7 @@ import { ICHeader, ICXHeader, PSCHeader } from "@/common/types.ts"; import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts"; import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { isNotFoundError } from "@lib/common/utils.doc.ts"; export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { @@ -87,14 +88,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, eventHub.emitEvent(EVENT_REQUEST_CHECK_REMOTE_SIZE); }) ); - new Setting(paneEl).autoWireToggle("writeLogToTheFile"); + renderObsidianSetting(paneEl, "writeLogToTheFile"); }); void addPanel(paneEl, "Scram Switches").then((paneEl) => { - new Setting(paneEl).autoWireToggle("suspendFileWatching"); + renderObsidianSetting(paneEl, "suspendFileWatching"); this.addOnSaved("suspendFileWatching", () => this.services.appLifecycle.askRestart()); - new Setting(paneEl).autoWireToggle("suspendParseReplicationResult"); + renderObsidianSetting(paneEl, "suspendParseReplicationResult"); this.addOnSaved("suspendParseReplicationResult", () => this.services.appLifecycle.askRestart()); }); diff --git a/src/modules/features/SettingDialogue/PanePatches.ts b/src/modules/features/SettingDialogue/PanePatches.ts index c83e7d8..c01d8c1 100644 --- a/src/modules/features/SettingDialogue/PanePatches.ts +++ b/src/modules/features/SettingDialogue/PanePatches.ts @@ -7,6 +7,7 @@ import { } from "@lib/common/types.ts"; import { Logger } from "@lib/common/logger.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { addObsidianApplyButton, renderObsidianApplyButton, renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; @@ -16,17 +17,17 @@ import { migrateDatabases } from "./settingUtils.ts"; export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { void addPanel(paneEl, "Compatibility (Metadata)").then((paneEl) => { - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("deleteMetadataOfDeletedFiles"); + renderObsidianSetting(paneEl, "deleteMetadataOfDeletedFiles").setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("automaticallyDeleteMetadataOfDeletedFiles", { + renderObsidianSetting(paneEl, "automaticallyDeleteMetadataOfDeletedFiles", { onUpdate: visibleOnly(() => this.isConfiguredAs("deleteMetadataOfDeletedFiles", true)), - }); + }).setClass("wizardHidden"); }); void addPanel(paneEl, "Compatibility (Conflict Behaviour)").then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("disableMarkdownAutoMerge"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("writeDocumentsIfConflicted"); + renderObsidianSetting(paneEl, "disableMarkdownAutoMerge").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "writeDocumentsIfConflicted").setClass("wizardHidden"); }); void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => { @@ -113,18 +114,18 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen }); } } - new Setting(paneEl).autoWireToggle("handleFilenameCaseSensitive", { holdValue: true }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "handleFilenameCaseSensitive", { holdValue: true }).setClass("wizardHidden"); }); void addPanel(paneEl, "Compatibility (Internal API Usage)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("watchInternalFileChanges", { invert: true }); + renderObsidianSetting(paneEl, "watchInternalFileChanges", { invert: true }); }); void addPanel(paneEl, "Compatibility (Remote Database)").then((paneEl) => { - new Setting(paneEl).autoWireDropDown("E2EEAlgorithm", { + renderObsidianSetting(paneEl, "E2EEAlgorithm", { options: E2EEAlgorithmNames, }); }); - new Setting(paneEl).autoWireToggle("useDynamicIterationCount", { + renderObsidianSetting(paneEl, "useDynamicIterationCount", { holdValue: true, onUpdate: visibleOnly( () => @@ -134,16 +135,15 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen }); void addPanel(paneEl, "Edge case addressing (Database)").then((paneEl) => { - new Setting(paneEl) - .autoWireText("additionalSuffixOfDatabaseName", { holdValue: true }) - .addApplyButton(["additionalSuffixOfDatabaseName"]); + renderObsidianSetting(paneEl, "additionalSuffixOfDatabaseName"); + renderObsidianApplyButton(paneEl, "database-suffix"); this.addOnSaved("additionalSuffixOfDatabaseName", async (key) => { Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE); await this.services.databaseEvents.initialiseDatabase(); }); - new Setting(paneEl).autoWireDropDown("hashAlg", { + renderObsidianSetting(paneEl, "hashAlg", { options: { "": "Old Algorithm", xxhash32: "xxhash32 (Fast but less collision resistance)", @@ -157,15 +157,15 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen }); }); void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("doNotSuspendOnFetching"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); - new Setting(paneEl).autoWireToggle("processSizeMismatchedFiles"); + renderObsidianSetting(paneEl, "doNotSuspendOnFetching"); + renderObsidianSetting(paneEl, "doNotDeleteFolder").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "processSizeMismatchedFiles"); }); void addPanel(paneEl, "Edge case addressing (Processing)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("disableWorkerForGeneratingChunks"); + renderObsidianSetting(paneEl, "disableWorkerForGeneratingChunks"); - new Setting(paneEl).autoWireToggle("processSmallFilesInUIThread", { + renderObsidianSetting(paneEl, "processSmallFilesInUIThread", { onUpdate: visibleOnly(() => this.isConfiguredAs("disableWorkerForGeneratingChunks", false)), }); }); @@ -173,11 +173,11 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen // new Setting(paneEl).autoWireToggle("useRequestAPI"); // }); void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => { - new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch"); + renderObsidianSetting(paneEl, "disableCheckingConfigMismatch"); }); void addPanel(paneEl, "Remediation").then((paneEl) => { let dateEl: HTMLSpanElement; - new Setting(paneEl) + const remediationSetting = new Setting(paneEl) .addText((text) => { const updateDateText = () => { if (this.editingSettings.maxMTimeForReflectEvents == 0) { @@ -212,8 +212,8 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen updateDateText(); return text; }) - .setAuto("maxMTimeForReflectEvents") - .addApplyButton(["maxMTimeForReflectEvents"]); + .setAuto("maxMTimeForReflectEvents"); + addObsidianApplyButton(remediationSetting, "remediation-reflect-events"); this.addOnSaved("maxMTimeForReflectEvents", async (key) => { const buttons = ["Restart Now", "Later"] as const; @@ -240,6 +240,6 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen // .setClass("wizardHidden"); // new Setting(paneEl).autoWireNumeric("maxAgeInEden", { onUpdate: onlyUsingEden }).setClass("wizardHidden"); - new Setting(paneEl).autoWireToggle("enableCompression").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "enableCompression").setClass("wizardHidden"); }); } diff --git a/src/modules/features/SettingDialogue/PanePowerUsers.ts b/src/modules/features/SettingDialogue/PanePowerUsers.ts index 7bec258..8fe3eec 100644 --- a/src/modules/features/SettingDialogue/PanePowerUsers.ts +++ b/src/modules/features/SettingDialogue/PanePowerUsers.ts @@ -1,5 +1,5 @@ import { type ConfigPassphraseStore } from "@lib/common/types.ts"; -import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { renderObsidianApplyButton, renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; @@ -21,14 +21,14 @@ export function panePowerUsers( this.onlyOnCouchDB ).addClass("wizardHidden"); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireNumeric("batch_size", { clampMin: 2, onUpdate: this.onlyOnCouchDB }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batches_limit", { + renderObsidianSetting(paneEl, "batch_size", { clampMin: 2, onUpdate: this.onlyOnCouchDB }).setClass( + "wizardHidden" + ); + renderObsidianSetting(paneEl, "batches_limit", { clampMin: 2, onUpdate: this.onlyOnCouchDB, - }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useTimeouts", { onUpdate: this.onlyOnCouchDB }); + }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "useTimeouts", { onUpdate: this.onlyOnCouchDB }).setClass("wizardHidden"); }); void addPanel(paneEl, "Configuration Encryption").then((paneEl) => { const passphrase_options: Record = { @@ -37,23 +37,18 @@ export function panePowerUsers( ASK_AT_LAUNCH: "Ask an passphrase at every launch", }; - new Setting(paneEl) - .setName("Encrypting sensitive configuration items") - .autoWireDropDown("configPassphraseStore", { - options: passphrase_options, - holdValue: true, - }) - .setClass("wizardHidden"); + renderObsidianSetting(paneEl, "configPassphraseStore", { + options: passphrase_options, + }).setClass("wizardHidden"); - new Setting(paneEl) - .autoWireText("configPassphrase", { isPassword: true, holdValue: true }) + renderObsidianSetting(paneEl, "configPassphrase") .setClass("wizardHidden") .addOnUpdate(() => ({ disabled: !this.isConfiguredAs("configPassphraseStore", "LOCALSTORAGE"), })); - new Setting(paneEl).addApplyButton(["configPassphrase", "configPassphraseStore"]).setClass("wizardHidden"); + renderObsidianApplyButton(paneEl, "configuration-encryption").setClass("wizardHidden"); }); void addPanel(paneEl, "Developer").then((paneEl) => { - new Setting(paneEl).autoWireToggle("enableDebugTools").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "enableDebugTools").setClass("wizardHidden"); }); } diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index bdc7734..e9e385f 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -10,6 +10,7 @@ import { import { Menu, type ButtonComponent } from "@/deps.ts"; import { $msg } from "@lib/common/i18n.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; // import { visibleOnly } from "./SettingPane.ts"; @@ -674,7 +675,7 @@ export function paneRemoteConfig( void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleNotification"), () => {}).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).autoWireNumeric("notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "notifyThresholdOfRemoteStorageSize", {}).setClass("wizardHidden"); }); // new Setting(paneEl).setClass("wizardOnly").addButton((button) => diff --git a/src/modules/features/SettingDialogue/PaneSelector.ts b/src/modules/features/SettingDialogue/PaneSelector.ts index 4f00c68..af65447 100644 --- a/src/modules/features/SettingDialogue/PaneSelector.ts +++ b/src/modules/features/SettingDialogue/PaneSelector.ts @@ -4,6 +4,7 @@ import MultipleRegExpControl from "./MultipleRegExpControl.svelte"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { mount } from "svelte"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { @@ -46,12 +47,12 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme }, }, }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncMaxSizeInMB", { clampMin: 0 }); + renderObsidianSetting(paneEl, "syncMaxSizeInMB", { clampMin: 0 }).setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("useIgnoreFiles"); - new Setting(paneEl).setClass("wizardHidden").autoWireTextArea("ignoreFiles", { + renderObsidianSetting(paneEl, "useIgnoreFiles").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "ignoreFiles", { onUpdate: visibleOnly(() => this.isConfiguredAs("useIgnoreFiles", true)), - }); + }).setClass("wizardHidden"); }); void addPanel(paneEl, "Hidden Files", undefined, undefined, LEVEL_ADVANCED).then((paneEl) => { const targetPatternSetting = new Setting(paneEl) diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts index 8cd78b5..9218584 100644 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -15,6 +15,7 @@ import { DEFAULT_SETTINGS } from "@lib/common/types.ts"; import { request } from "@/deps.ts"; import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts"; import { LiveSyncError } from "@lib/common/LSError.ts"; +import { renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; export function paneSetup( this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, @@ -107,10 +108,9 @@ export function paneSetup( }); void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => { - new Setting(paneEl).autoWireToggle("useAdvancedMode"); - - new Setting(paneEl).autoWireToggle("usePowerUserMode"); - new Setting(paneEl).autoWireToggle("useEdgeCaseMode"); + renderObsidianSetting(paneEl, "useAdvancedMode"); + renderObsidianSetting(paneEl, "usePowerUserMode"); + renderObsidianSetting(paneEl, "useEdgeCaseMode"); this.addOnSaved("useAdvancedMode", () => this.display()); this.addOnSaved("usePowerUserMode", () => this.display()); diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts index 9d2a20e..31dc567 100644 --- a/src/modules/features/SettingDialogue/PaneSyncSettings.ts +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -2,6 +2,7 @@ import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, REMOTE_COUCHDB, LEVEL_ import { Logger } from "@lib/common/logger.ts"; import { $msg } from "@lib/common/i18n.ts"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; +import { renderObsidianApplyButton, renderObsidianSetting } from "./ObsidianSettingRenderer.ts"; import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "@/common/events.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; @@ -31,18 +32,16 @@ export function paneSyncSettings( DISABLE: $msg("obsidianLiveSyncSettingTab.optionDisableAllAutomatic"), }; - new Setting(paneEl) - .autoWireDropDown("preset", { - options: options, - holdValue: true, - }) - .addButton((button) => { - button.setButtonText($msg("obsidianLiveSyncSettingTab.btnApply")); - button.onClick(async () => { - // await this.saveSettings(["preset"]); - await this.saveAllDirtySettings(); - }); + renderObsidianSetting(paneEl, "preset", { + options: options, + holdValue: true, + }).addButton((button) => { + button.setButtonText($msg("obsidianLiveSyncSettingTab.btnApply")); + button.onClick(async () => { + // await this.saveSettings(["preset"]); + await this.saveAllDirtySettings(); }); + }); this.addOnSaved("preset", async (currentPreset) => { if (currentPreset == "") { @@ -136,7 +135,7 @@ export function paneSyncSettings( const onlyOnNonLiveSync = visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")); const onlyOnPeriodic = visibleOnly(() => this.isConfiguredAs("syncMode", "PERIODIC")); - const optionsSyncMode = + const optionsSyncMode: Record = this.editingSettings.remoteType == REMOTE_COUCHDB ? { ONEVENTS: $msg("obsidianLiveSyncSettingTab.optionOnEvents"), @@ -148,12 +147,9 @@ export function paneSyncSettings( PERIODIC: $msg("obsidianLiveSyncSettingTab.optionPeriodicAndEvents"), }; - new Setting(paneEl) - .autoWireDropDown("syncMode", { - //@ts-ignore - options: optionsSyncMode, - }) - .setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncMode", { + options: optionsSyncMode, + }).setClass("wizardHidden"); this.addOnSaved("syncMode", async (value) => { this.editingSettings.liveSync = false; this.editingSettings.periodicReplication = false; @@ -167,32 +163,28 @@ export function paneSyncSettings( await this.services.control.applySettings(); }); - new Setting(paneEl) - .autoWireNumeric("periodicReplicationInterval", { - clampMax: 5000, - onUpdate: onlyOnPeriodic, - }) - .setClass("wizardHidden"); + renderObsidianSetting(paneEl, "periodicReplicationInterval", { + clampMax: 5000, + onUpdate: onlyOnPeriodic, + }).setClass("wizardHidden"); - new Setting(paneEl).autoWireNumeric("syncMinimumInterval", { + renderObsidianSetting(paneEl, "syncMinimumInterval", { onUpdate: onlyOnNonLiveSync, }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnSave", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl) - .setClass("wizardHidden") - .autoWireToggle("syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); + renderObsidianSetting(paneEl, "syncOnSave", { onUpdate: onlyOnNonLiveSync }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncOnEditorSave", { onUpdate: onlyOnNonLiveSync }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncOnStart", { onUpdate: onlyOnNonLiveSync }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncAfterMerge", { onUpdate: onlyOnNonLiveSync }).setClass("wizardHidden"); // Desktop app only, and only for the sync modes that keep a background replication channel // (LiveSync and Periodic). Ignored on mobile, where suspending preserves battery. The // visibility predicate mirrors the runtime guard in ModuleObsidianEvents. if (!this.services.API.isMobile()) { - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("keepReplicationActiveInBackground", { + renderObsidianSetting(paneEl, "keepReplicationActiveInBackground", { onUpdate: visibleOnly( () => this.isConfiguredAs("syncMode", "LIVESYNC") || this.isConfiguredAs("syncMode", "PERIODIC") ), - }); + }).setClass("wizardHidden"); } }); @@ -203,15 +195,15 @@ export function paneSyncSettings( visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC")) ).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("batchSave"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMinimumDelay", { + renderObsidianSetting(paneEl, "batchSave").setClass("wizardHidden"); + renderObsidianSetting(paneEl, "batchSaveMinimumDelay", { acceptZero: true, onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), - }); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("batchSaveMaximumDelay", { + }).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "batchSaveMaximumDelay", { acceptZero: true, onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)), - }); + }).setClass("wizardHidden"); }); void addPanel( @@ -222,9 +214,9 @@ export function paneSyncSettings( LEVEL_ADVANCED ).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("trashInsteadDelete"); + renderObsidianSetting(paneEl, "trashInsteadDelete").setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("doNotDeleteFolder"); + renderObsidianSetting(paneEl, "doNotDeleteFolder").setClass("wizardHidden"); }); void addPanel( paneEl, @@ -235,11 +227,11 @@ export function paneSyncSettings( ).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("resolveConflictsByNewerFile"); + renderObsidianSetting(paneEl, "resolveConflictsByNewerFile").setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("checkConflictOnlyOnOpen"); + renderObsidianSetting(paneEl, "checkConflictOnlyOnOpen").setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("showMergeDialogOnlyOnActive"); + renderObsidianSetting(paneEl, "showMergeDialogOnlyOnActive").setClass("wizardHidden"); }); void addPanel( @@ -250,11 +242,12 @@ export function paneSyncSettings( LEVEL_ADVANCED ).then((paneEl) => { paneEl.addClass("wizardHidden"); - new Setting(paneEl).autoWireText("settingSyncFile", { holdValue: true }).addApplyButton(["settingSyncFile"]); + renderObsidianSetting(paneEl, "settingSyncFile"); + renderObsidianApplyButton(paneEl, "setting-sync-file"); - new Setting(paneEl).autoWireToggle("writeCredentialsForSettingSync"); + renderObsidianSetting(paneEl, "writeCredentialsForSettingSync"); - new Setting(paneEl).autoWireToggle("notifyAllSettingSyncFile"); + renderObsidianSetting(paneEl, "notifyAllSettingSyncFile"); }); void addPanel( @@ -313,14 +306,14 @@ export function paneSyncSettings( }); } - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("suppressNotifyHiddenFilesChange", {}); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncInternalFilesBeforeReplication", { + renderObsidianSetting(paneEl, "suppressNotifyHiddenFilesChange", {}).setClass("wizardHidden"); + renderObsidianSetting(paneEl, "syncInternalFilesBeforeReplication", { onUpdate: visibleOnly(() => this.isConfiguredAs("watchInternalFileChanges", true)), - }); + }).setClass("wizardHidden"); - new Setting(paneEl).setClass("wizardHidden").autoWireNumeric("syncInternalFilesInterval", { + renderObsidianSetting(paneEl, "syncInternalFilesInterval", { clampMin: 10, acceptZero: true, - }); + }).setClass("wizardHidden"); }); } diff --git a/src/modules/features/SettingDialogue/SettingPane.ts b/src/modules/features/SettingDialogue/SettingPane.ts index 1a52f3f..706b20a 100644 --- a/src/modules/features/SettingDialogue/SettingPane.ts +++ b/src/modules/features/SettingDialogue/SettingPane.ts @@ -1,5 +1,11 @@ import { $msg } from "@lib/common/i18n"; -import { LEVEL_ADVANCED, LEVEL_EDGE_CASE, LEVEL_POWER_USER, type ConfigLevel } from "@lib/common/types"; +import { + LEVEL_ADVANCED, + LEVEL_EDGE_CASE, + LEVEL_POWER_USER, + type ConfigLevel, + type SettingDefinition, +} from "@lib/common/types"; import type { AllSettingItemKey, AllSettings } from "./settingConstants"; export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => { @@ -77,6 +83,7 @@ export type AutoWireOption = { invert?: boolean; onUpdate?: OnUpdateFunc; obsolete?: boolean; + settingDefinition?: SettingDefinition; }; export function findAttrFromParent(el: HTMLElement, attr: string): string {