import { Setting, PluginSettingTab, App, Plugin } from 'obsidian'; import { WaypointSettings, DEFAULT_SETTINGS, PeriodNoteSettings } from 'src/settings'; export class WaypointSettingTab extends PluginSettingTab { private plugin: Plugin; private settings: WaypointSettings; private onSettingsChange: () => void; private activeTab: 'calendar' | 'periodic' | 'recent' | 'display' = 'calendar'; constructor(app: App, plugin: Plugin, settings: WaypointSettings, onSettingsChange: () => void) { super(app, plugin); this.plugin = plugin; this.settings = settings; this.onSettingsChange = onSettingsChange; } display(): void { const { containerEl } = this; containerEl.empty(); // ── Tab bar ── const tabBar = containerEl.createDiv({ cls: 'waypoint-settings-tabs' }); const tabs = [ { key: 'calendar' as const, label: 'Calendar' }, { key: 'periodic' as const, label: 'Periodic Notes' }, { key: 'recent' as const, label: 'Recent Files' }, { key: 'display' as const, label: 'Display' }, ]; for (const tab of tabs) { const btn = tabBar.createEl('button', { cls: `waypoint-settings-tab${this.activeTab === tab.key ? ' is-active' : ''}`, text: tab.label, }); btn.addEventListener('click', () => { this.activeTab = tab.key; this.display(); }); } const tabContent = containerEl.createDiv({ cls: 'waypoint-settings-content' }); switch (this.activeTab) { case 'calendar': this.renderCalendarTab(tabContent); break; case 'periodic': this.renderPeriodicTab(tabContent); break; case 'recent': this.renderRecentTab(tabContent); break; case 'display': this.renderDisplayTab(tabContent); break; } } // ═══════════════════════════════ // Calendar tab // ═══════════════════════════════ private renderCalendarTab(container: HTMLElement): void { new Setting(container) .setName('First day of week') .setDesc('Which day the calendar week starts on.') .addDropdown((dropdown) => { dropdown .addOption('0', 'Sunday') .addOption('1', 'Monday') .setValue(String(this.settings.calendar.firstDayOfWeek)) .onChange((value) => { this.settings.calendar.firstDayOfWeek = parseInt(value, 10); this.saveAndRefresh(); }); }); new Setting(container) .setName('Show note indicators') .setDesc('Show a dot on days that have existing notes.') .addToggle((toggle) => { toggle .setValue(this.settings.calendar.showNoteIndicators) .onChange((value) => { this.settings.calendar.showNoteIndicators = value; this.saveAndRefresh(); }); }); } // ═══════════════════════════════ // Periodic Notes tab // ═══════════════════════════════ private renderPeriodicTab(container: HTMLElement): void { this.addPeriodNoteSettings(container, 'Daily', this.settings.daily); this.addPeriodNoteSettings(container, 'Weekly', this.settings.weekly); this.addPeriodNoteSettings(container, 'Monthly', this.settings.monthly); this.addPeriodNoteSettings(container, 'Quarterly', this.settings.quarterly); this.addPeriodNoteSettings(container, 'Yearly', this.settings.yearly); } private addPeriodNoteSettings(container: HTMLElement, label: string, period: PeriodNoteSettings): void { new Setting(container).setHeading().setName(label); new Setting(container) .setName('Folder') .setDesc(`Folder path for ${label.toLowerCase()} notes.`) .addText((text) => { text.setPlaceholder('periodic/daily'); text.setValue(period.folder); text.onChange((value) => { period.folder = value; this.saveAndRefresh(); }); }); new Setting(container) .setName('Name format') .setDesc(`Date format for ${label.toLowerCase()} note filenames (moment.js format).`) .addText((text) => { text.setPlaceholder('yyyy-MM-dd'); text.setValue(period.nameFormat); text.onChange((value) => { period.nameFormat = value; this.saveAndRefresh(); }); }); new Setting(container) .setName('Template file') .setDesc(`Path to the template file (without .md extension).`) .addText((text) => { text.setPlaceholder('Templates/Daily note'); text.setValue(period.templateFile); text.onChange((value) => { period.templateFile = value; this.saveAndRefresh(); }); }); new Setting(container) .setName('Type property') .setDesc(`Value for the 'type' frontmatter property.`) .addText((text) => { text.setPlaceholder('daily-note'); text.setValue(period.typeProperty); text.onChange((value) => { period.typeProperty = value; this.saveAndRefresh(); }); }); } // ═══════════════════════════════ // Recent Files tab // ═══════════════════════════════ private renderRecentTab(container: HTMLElement): void { new Setting(container) .setName('Max items') .setDesc('Maximum number of recent files to track.') .addText((text) => { text.inputEl.setAttr('type', 'number'); text.inputEl.setAttr('placeholder', '50'); text.setValue(String(this.settings.recentFiles.maxItems)); text.inputEl.onblur = () => { const val = parseInt(text.getValue(), 10); if (!isNaN(val) && val > 0) { this.settings.recentFiles.maxItems = val; this.saveAndRefresh(); } }; }); new Setting(container) .setName('Update on') .setDesc('When to add a file to the recent list.') .addDropdown((dropdown) => { dropdown .addOption('file-open', 'File opened') .addOption('file-edit', 'File changed') .setValue(this.settings.recentFiles.updateOn) .onChange((value: 'file-open' | 'file-edit') => { this.settings.recentFiles.updateOn = value; this.saveAndRefresh(); }); }); const pathDesc = new DocumentFragment(); pathDesc.appendText('Regex patterns for paths to exclude. One per line.'); new Setting(container) .setName('Omitted paths') .setDesc(pathDesc) .addTextArea((text) => { text.inputEl.setAttr('rows', 4); text.setPlaceholder('^archives/\n\\.png$'); text.setValue(this.settings.recentFiles.omittedPaths.join('\n')); text.inputEl.onblur = () => { this.settings.recentFiles.omittedPaths = text.getValue().split('\n').filter(p => p.trim()); this.saveAndRefresh(); }; }); const tagDesc = new DocumentFragment(); tagDesc.appendText('Regex patterns for frontmatter tags to exclude. One per line.'); new Setting(container) .setName('Omitted tags') .setDesc(tagDesc) .addTextArea((text) => { text.inputEl.setAttr('rows', 4); text.setPlaceholder('ignore\narchive'); text.setValue(this.settings.recentFiles.omittedTags.join('\n')); text.inputEl.onblur = () => { this.settings.recentFiles.omittedTags = text.getValue().split('\n').filter(t => t.trim()); this.saveAndRefresh(); }; }); } // ═══════════════════════════════ // Display tab // ═══════════════════════════════ private renderDisplayTab(container: HTMLElement): void { new Setting(container).setHeading().setName('Bookmarks'); this.addSliderSetting(container, 'Row size', 'Height of bookmark items.', this.settings.display, 'rowSize', 18, 40, 1, 'px', ); this.addSliderSetting(container, 'Row spacing', 'Gap between bookmark items.', this.settings.display, 'rowSpacing', 0, 12, 1, 'px', ); this.addSliderSetting(container, 'Indent size', 'Indent per nesting depth.', this.settings.display, 'indentSize', 8, 32, 2, 'px', ); this.addSliderSetting(container, 'Font size', 'Label font size.', this.settings.display, 'fontSize', 10, 18, 1, 'px', ); this.addSliderSetting(container, 'Icon size', 'Bookmark icon size.', this.settings.display, 'iconSize', 12, 24, 1, 'px', ); new Setting(container).setHeading().setName('Calendar'); this.addSliderSetting(container, 'Cell size', 'Height of calendar day cells.', this.settings.display, 'calendarCellSize', 20, 48, 2, 'px', ); new Setting(container) .addButton((btn) => btn .setButtonText('Reset to defaults') .onClick(() => { this.settings.display = { ...DEFAULT_SETTINGS.display }; this.saveAndRefresh(); this.display(); }), ); } private addSliderSetting( container: HTMLElement, name: string, desc: string, obj: any, key: string, min: number, max: number, step: number, unit: string, ): void { const setting = new Setting(container) .setName(name) .setDesc(`${desc} (${obj[key]}${unit})`); setting.addSlider((slider) => { slider .setLimits(min, max, step) .setValue(obj[key]) .setDynamicTooltip() .onChange((value) => { obj[key] = value; setting.setDesc(`${desc} (${value}${unit})`); this.saveAndRefresh(); }); }); } private async saveAndRefresh(): Promise { await this.plugin.saveData(this.settings); this.onSettingsChange(); } }