From 6779b5f539e332d6b4a6af21d24736de5d4c9f6e Mon Sep 17 00:00:00 2001 From: Lukas Wolfsteiner Date: Mon, 6 Jul 2026 12:04:29 +0200 Subject: [PATCH] feat(settings): support settings search with legacy fallback Implement the declarative setting definitions API so plugin settings show up in the settings search on newer Obsidian versions. Add a self-contained control value reader/writer and an imperative fallback renderer so the tab keeps working on app versions that predate the declarative API. Lower the minimum app version to match the documented compatibility floor already declared in the version map. Co-authored-by: Cursor --- manifest.json | 2 +- src/settings.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/manifest.json b/manifest.json index 698f1b3..9d78bb6 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "raindrop-io", "name": "Raindrop.io", "version": "1.0.4", - "minAppVersion": "1.13.0", + "minAppVersion": "1.7.2", "description": "Browse Raindrop.io links in notes with note-aware and configurable search filters.", "author": "Lukas '@dotWee' Wolfsteiner", "authorUrl": "https://lukas.wolfsteiner.media", diff --git a/src/settings.ts b/src/settings.ts index 20fe155..e46e31b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,5 @@ -import { App, PluginSettingTab } from "obsidian"; -import type { SettingDefinitionItem } from "obsidian"; +import { App, PluginSettingTab, Setting } from "obsidian"; +import type { SettingControl, SettingDefinition, SettingDefinitionItem, SettingGroup } from "obsidian"; import { DEFAULT_DISPLAY_FIELDS, type RaindropDisplayFields } from "./renderer"; import type RaindropViewPlugin from "./main"; @@ -151,9 +151,106 @@ export class RaindropSettingTab extends PluginSettingTab { ]; } - setControlValue(key: string, value: unknown): void | Promise { - if (key === "tagClickBehavior" && !isRaindropTagClickBehavior(value)) return; - return super.setControlValue(key, typeof value === "string" ? value.trim() : value); + // Implemented without super calls so the tab also works on Obsidian + // versions before 1.13.0, where the SettingTab base class does not + // provide getControlValue/setControlValue. + getControlValue(key: string): unknown { + return (this.plugin.settings as unknown as Record)[key]; } + async setControlValue(key: string, value: unknown): Promise { + if (key === "tagClickBehavior" && !isRaindropTagClickBehavior(value)) return; + const normalized = typeof value === "string" ? value.trim() : value; + (this.plugin.settings as unknown as Record)[key] = normalized; + await this.plugin.saveSettings(); + } + + /** + * Fallback for Obsidian versions before 1.13.0, which do not call + * getSettingDefinitions(). Renders the same definitions imperatively. + * On 1.13.0+ this method is never called because getSettingDefinitions() + * returns a non-empty array. + */ + display(): void { + const { containerEl } = this; + containerEl.empty(); + this.renderDefinitionItems(this.getSettingDefinitions(), containerEl); + } + + private renderDefinitionItems(items: SettingDefinitionItem[], containerEl: HTMLElement): void { + for (const item of items) { + if ("type" in item && (item.type === "group" || item.type === "list")) { + if (item.heading) { + new Setting(containerEl).setName(item.heading).setHeading(); + } + this.renderDefinitionItems(item.items ?? [], containerEl); + continue; + } + if ("type" in item && item.type === "page") continue; + this.renderDefinition(item as SettingDefinition, containerEl); + } + } + + private renderDefinition(def: SettingDefinition, containerEl: HTMLElement): void { + const setting = new Setting(containerEl).setName(def.name); + if (def.desc) setting.setDesc(def.desc); + if (def.render) { + // Our render callbacks only use the Setting instance; SettingGroup + // does not exist at runtime before 1.13.0. + def.render(setting, undefined as unknown as SettingGroup); + return; + } + if (def.control) this.renderControl(setting, def.control); + } + + private renderControl(setting: Setting, control: SettingControl): void { + const rawValue = this.getControlValue(control.key) ?? control.defaultValue; + const currentValue = + typeof rawValue === "string" || typeof rawValue === "number" || typeof rawValue === "boolean" + ? rawValue + : undefined; + switch (control.type) { + case "text": + setting.addText((text) => { + if (control.placeholder) text.setPlaceholder(control.placeholder); + text.setValue(currentValue === undefined ? "" : String(currentValue)).onChange(async (value) => { + if (control.validate && (await control.validate(value))) return; + await this.setControlValue(control.key, value); + }); + }); + break; + case "number": + setting.addText((text) => { + text.inputEl.type = "number"; + if (control.min !== undefined) text.inputEl.min = String(control.min); + if (control.max !== undefined) text.inputEl.max = String(control.max); + if (control.step !== undefined) text.inputEl.step = String(control.step); + if (control.placeholder) text.setPlaceholder(control.placeholder); + text.setValue(currentValue === undefined ? "" : String(currentValue)).onChange(async (value) => { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return; + if (control.validate && (await control.validate(parsed))) return; + await this.setControlValue(control.key, parsed); + }); + }); + break; + case "dropdown": + setting.addDropdown((dropdown) => { + dropdown.addOptions(control.options); + dropdown.setValue(currentValue === undefined ? "" : String(currentValue)).onChange(async (value) => { + await this.setControlValue(control.key, value); + }); + }); + break; + case "toggle": + setting.addToggle((toggle) => { + toggle.setValue(Boolean(currentValue)).onChange(async (value) => { + await this.setControlValue(control.key, value); + }); + }); + break; + default: + break; + } + } }