From 23118c613c33bd417a6de6df98f4cf32063be8cc Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Sat, 24 May 2025 14:14:40 +0200 Subject: [PATCH] chore: restore settings --- integrations/settings/settings-controller.ts | 119 +++++++ integrations/settings/settings-tab.ts | 357 +------------------ integrations/settings/settings-ui.ts | 227 ++++++++++++ main.ts | 28 +- 4 files changed, 372 insertions(+), 359 deletions(-) create mode 100644 integrations/settings/settings-controller.ts create mode 100644 integrations/settings/settings-ui.ts diff --git a/integrations/settings/settings-controller.ts b/integrations/settings/settings-controller.ts new file mode 100644 index 0000000..addcbd8 --- /dev/null +++ b/integrations/settings/settings-controller.ts @@ -0,0 +1,119 @@ +import { App } from 'obsidian'; +import { Status } from '../../models/types'; +import NoteStatus from 'main'; +import { StatusService } from 'services/status-service'; +import { NoteStatusSettingsUI, SettingsUICallbacks } from './settings-ui'; + +/** + * Controller for settings UI, handles business logic and plugin state updates + */ +export class NoteStatusSettingsController implements SettingsUICallbacks { + private app: App; + private plugin: NoteStatus; + private statusService: StatusService; + private ui: NoteStatusSettingsUI; + + constructor(app: App, plugin: NoteStatus, statusService: StatusService) { + this.app = app; + this.plugin = plugin; + this.statusService = statusService; + this.ui = new NoteStatusSettingsUI(this); + } + + /** + * Renders the settings interface + */ + display(containerEl: HTMLElement): void { + this.ui.render(containerEl, this.plugin.settings); + } + + /** + * Handles template enable/disable toggle + */ + async onTemplateToggle(templateId: string, enabled: boolean): Promise { + if (enabled) { + if (!this.plugin.settings.enabledTemplates.includes(templateId)) { + this.plugin.settings.enabledTemplates.push(templateId); + } + } else { + this.plugin.settings.enabledTemplates = this.plugin.settings.enabledTemplates.filter( + (id: string) => id !== templateId + ); + } + + await this.plugin.saveSettings(); + } + + /** + * Handles general setting changes + */ + async onSettingChange(key: string, value: any): Promise { + (this.plugin.settings as any)[key] = value; + await this.plugin.saveSettings(); + } + + /** + * Handles custom status field changes + */ + async onCustomStatusChange(index: number, field: string, value: any): Promise { + const status = this.plugin.settings.customStatuses[index]; + if (!status) return; + + if (field === 'name') { + const oldName = status.name; + status.name = value; + + if (oldName !== status.name) { + this.plugin.settings.statusColors[status.name] = + this.plugin.settings.statusColors[oldName]; + delete this.plugin.settings.statusColors[oldName]; + } + } else if (field === 'color') { + this.plugin.settings.statusColors[status.name] = value; + } else { + (status as any)[field] = value; + } + + await this.plugin.saveSettings(); + } + + /** + * Handles custom status removal + */ + async onCustomStatusRemove(index: number): Promise { + const status = this.plugin.settings.customStatuses[index]; + if (!status) return; + + this.plugin.settings.customStatuses.splice(index, 1); + delete this.plugin.settings.statusColors[status.name]; + + await this.plugin.saveSettings(); + + // Re-render the custom statuses section + const statusList = document.querySelector('.custom-status-list') as HTMLElement; + if (statusList) { + this.ui.renderCustomStatuses(statusList, this.plugin.settings); + } + } + + /** + * Handles adding new custom status + */ + async onCustomStatusAdd(): Promise { + const newStatus: Status = { + name: `status${this.plugin.settings.customStatuses.length + 1}`, + icon: '⭐' + }; + + this.plugin.settings.customStatuses.push(newStatus); + this.plugin.settings.statusColors[newStatus.name] = '#ffffff'; + + await this.plugin.saveSettings(); + + // Re-render the custom statuses section + const statusList = document.querySelector('.custom-status-list') as HTMLElement; + if (statusList) { + this.ui.renderCustomStatuses(statusList, this.plugin.settings); + } + } +} diff --git a/integrations/settings/settings-tab.ts b/integrations/settings/settings-tab.ts index d34334f..f59f470 100644 --- a/integrations/settings/settings-tab.ts +++ b/integrations/settings/settings-tab.ts @@ -1,352 +1,23 @@ -import { App, PluginSettingTab, Setting, Notice } from 'obsidian'; -import { Status } from '../../models/types'; -import { PREDEFINED_TEMPLATES } from '../../constants/status-templates'; +import { App, PluginSettingTab } from 'obsidian'; import NoteStatus from 'main'; import { StatusService } from 'services/status-service'; +import { NoteStatusSettingsController } from './settings-controller'; /** - * Settings tab for the Note Status plugin + * Settings tab for the Note Status plugin - delegates to controller */ export class NoteStatusSettingTab extends PluginSettingTab { - plugin: NoteStatus; - statusService: StatusService + private controller: NoteStatusSettingsController; - constructor(app: App, plugin: any, statusService: StatusService) { - super(app, plugin); - this.plugin = plugin; - this.statusService = statusService; - } + constructor(app: App, plugin: NoteStatus, statusService: StatusService) { + super(app, plugin); + this.controller = new NoteStatusSettingsController(app, plugin, statusService); + } - display(): void { - const { containerEl } = this; - containerEl.empty(); - - // Status Template section - this.displayTemplateSettings(containerEl.createDiv()); - - // UI section - new Setting(containerEl).setName('User interface').setHeading(); - - - // Status bar settings - new Setting(containerEl) - .setName('Show status bar') - .setDesc('Display the status bar') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusBar) - .onChange(async (value) => { - this.plugin.settings.showStatusBar = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Auto-hide status bar') - .setDesc('Hide the status bar when status is unknown') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.autoHideStatusBar) - .onChange(async (value) => { - this.plugin.settings.autoHideStatusBar = value; - await this.plugin.saveSettings(); - })); - - // File explorer settings - new Setting(containerEl) - .setName('Show status icons in file explorer') - .setDesc('Display status icons in the file explorer') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusIconsInExplorer) - .onChange(async (value) => { - this.plugin.settings.showStatusIconsInExplorer = value; - await this.plugin.saveSettings(); - - // Explicitly refresh explorer icons - if (value) { - this.plugin.explorerIntegration.updateAllFileExplorerIcons(); - } else { - this.plugin.explorerIntegration.removeAllFileExplorerIcons(); - } - })); - - // NEW SETTING: Hide unknown status in explorer - new Setting(containerEl) - .setName('Hide unknown status in file explorer') - .setDesc('Hide status icons for files with unknown status in the file explorer') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.hideUnknownStatusInExplorer || false) - .onChange(async (value) => { - this.plugin.settings.hideUnknownStatusInExplorer = value; - await this.plugin.saveSettings(); - - // Refresh explorer icons when this setting changes - this.plugin.explorerIntegration.updateAllFileExplorerIcons(); - })); - - // Compact view setting - new Setting(containerEl) - .setName('Default to compact view') - .setDesc('Start the status pane in compact view by default') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.compactView || false) - .onChange(async (value) => { - this.plugin.settings.compactView = value; - await this.plugin.saveSettings(); - })); - - // Add option to exclude unknown status in pane view for performance - new Setting(containerEl) - .setName('Exclude unassigned notes from status pane') - .setDesc('Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.excludeUnknownStatus || false) - .onChange(async (value) => { - this.plugin.settings.excludeUnknownStatus = value; - await this.plugin.saveSettings(); - - // Refresh the status pane if open - const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPane && statusPane.view) { - this.statusService.refreshUI([]); - } - })); - - new Setting(containerEl).setName('Status tag').setHeading(); - - // Option to use multiple statuses - new Setting(containerEl) - .setName('Enable multiple statuses') - .setDesc('Allow notes to have multiple statuses at the same time') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.useMultipleStatuses) - .onChange(async (value) => { - this.plugin.settings.useMultipleStatuses = value; - await this.plugin.saveSettings(); - - // Refresh UI to show multi-select or single-select options - this.statusService.refreshUI([]); - })); - - // Status tag prefix - new Setting(containerEl) - .setName('Status tag prefix') - .setDesc('The YAML frontmatter tag name used for status (default: obsidian-note-status)') - .addText(text => text - .setValue(this.plugin.settings.tagPrefix) - .onChange(async (value) => { - // Don't allow empty tag prefix - if (!value.trim()) { - return; - } - - this.plugin.settings.tagPrefix = value.trim(); - await this.plugin.saveSettings(); - this.statusService.refreshUI([]); - })); - - - // Status management section - new Setting(containerEl).setName('Custom statuses').setHeading(); - - // Option to use only custom statuses - new Setting(containerEl) - .setName('Use only custom statuses') - .setDesc('Ignore template statuses and use only the custom statuses defined below') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.useCustomStatusesOnly || false) - .onChange(async (value) => { - this.plugin.settings.useCustomStatusesOnly = value; - await this.plugin.saveSettings(); - - // Refresh the UI to show/hide template statuses - this.statusService.refreshUI([]); - this.display(); - })); - - // Custom statuses list - const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); - - const renderStatuses = () => { - statusList.empty(); - - this.plugin.settings.customStatuses.forEach((status: Status, index: number) => { - const setting = new Setting(statusList) - .setName(status.name) - .setClass('status-item'); - - // Name field - now properly implemented - setting.addText(text => text - .setPlaceholder('Name') - .setValue(status.name) - .onChange(async (value) => { - const oldName = status.name; - status.name = value || 'unnamed'; - - // Update color mapping when name changes - if (oldName !== status.name) { - this.plugin.settings.statusColors[status.name] = - this.plugin.settings.statusColors[oldName]; - delete this.plugin.settings.statusColors[oldName]; - } - - await this.plugin.saveSettings(); - })); - - // Icon field - setting.addText(text => text - .setPlaceholder('Icon') - .setValue(status.icon) - .onChange(async (value) => { - status.icon = value || '❓'; - await this.plugin.saveSettings(); - })); - - // Color picker - setting.addColorPicker(colorPicker => colorPicker - .setValue(this.plugin.settings.statusColors[status.name] || '#ffffff') - .onChange(async (value) => { - this.plugin.settings.statusColors[status.name] = value; - await this.plugin.saveSettings(); - })); - - // Description field - setting.addText(text => text - .setPlaceholder('Description') - .setValue(status.description || '') - .onChange(async (value) => { - status.description = value; - await this.plugin.saveSettings(); - })); - - // Remove button - setting.addButton(button => button - .setButtonText('Remove') - .setClass('status-remove-button') - .setWarning() - .onClick(async () => { - this.plugin.settings.customStatuses.splice(index, 1); - delete this.plugin.settings.statusColors[status.name]; - await this.plugin.saveSettings(); - renderStatuses(); - })); - }); - }; - - renderStatuses(); - - // Add new status - new Setting(containerEl) - .setName('Add new status') - .setDesc('Add a custom status with a name, icon, and color') - .addButton(button => button - .setButtonText('Add Status') - .setCta() - .onClick(async () => { - const newStatus = { - name: `status${this.plugin.settings.customStatuses.length + 1}`, - icon: '⭐' - }; - - this.plugin.settings.customStatuses.push(newStatus); - this.plugin.settings.statusColors[newStatus.name] = '#ffffff'; // Initial white color - - await this.plugin.saveSettings(); - renderStatuses(); - })); - } - - /** - * Display template settings section - */ - private displayTemplateSettings(containerEl: HTMLElement): void { - new Setting(containerEl).setName('Status templates').setHeading(); - containerEl.createEl('p', { - text: 'Enable predefined templates to quickly add common status workflows', - cls: 'setting-item-description' - }); - - // Create templates container - const templatesContainer = containerEl.createDiv({ cls: 'templates-container' }); - - // List each template with checkbox and preview - PREDEFINED_TEMPLATES.forEach(template => { - const templateEl = templatesContainer.createDiv({ cls: 'template-item' }); - - // Template header with checkbox and name - const headerEl = templateEl.createDiv({ cls: 'template-header' }); - - // Checkbox for enabling/disabling template - const isEnabled = this.plugin.settings.enabledTemplates.includes(template.id); - const checkbox = headerEl.createEl('input', { - type: 'checkbox', - cls: 'template-checkbox' - }); - checkbox.checked = isEnabled; - - checkbox.addEventListener('change', async () => { - if (checkbox.checked) { - // Enable template - if (!this.plugin.settings.enabledTemplates.includes(template.id)) { - this.plugin.settings.enabledTemplates.push(template.id); - } - } else { - // Disable template - this.plugin.settings.enabledTemplates = this.plugin.settings.enabledTemplates.filter( - (id: string) => id !== template.id - ); - } - - await this.plugin.saveSettings(); - this.statusService.refreshUI([]); - }); - - // Template name - headerEl.createEl('span', { - text: template.name, - cls: 'template-name' - }); - - // Template description - templateEl.createEl('div', { - text: template.description, - cls: 'template-description' - }); - - // Preview statuses - const statusesEl = templateEl.createDiv({ cls: 'template-statuses' }); - - template.statuses.forEach(status => { - const statusEl = statusesEl.createEl('div', { cls: 'template-status-chip' }); - - const colorDot = statusEl.createEl('span', { cls: 'status-color-dot' }); - colorDot.style.setProperty('--dot-color', status.color || '#ffffff') - - // Status icon and name - statusEl.createSpan({ text: `${status.icon} ${status.name}` }); - }); - }); - - // Import/Export buttons for templates - const buttonsContainer = containerEl.createDiv({ cls: 'template-buttons' }); - - // Import button - const importButton = buttonsContainer.createEl('button', { - text: 'Import template', - cls: 'mod-cta' - }); - - importButton.addEventListener('click', () => { - // Show notification about upcoming feature - new Notice('Template import functionality coming soon!'); - }); - - // Export button - const exportButton = buttonsContainer.createEl('button', { - text: 'Export templates', - cls: '' - }); - - exportButton.addEventListener('click', () => { - // Show notification about upcoming feature - new Notice('Template export functionality coming soon!'); - }); - } + /** + * Displays the settings interface + */ + display(): void { + this.controller.display(this.containerEl); + } } diff --git a/integrations/settings/settings-ui.ts b/integrations/settings/settings-ui.ts new file mode 100644 index 0000000..ab166dd --- /dev/null +++ b/integrations/settings/settings-ui.ts @@ -0,0 +1,227 @@ +import { Setting } from 'obsidian'; +import { Status } from '../../models/types'; +import { PREDEFINED_TEMPLATES } from '../../constants/status-templates'; + +/** + * Callbacks interface for settings UI interactions + */ +export interface SettingsUICallbacks { + /** Handle template enable/disable toggle */ + onTemplateToggle: (templateId: string, enabled: boolean) => Promise; + /** Handle general setting changes */ + onSettingChange: (key: string, value: any) => Promise; + /** Handle custom status field changes */ + onCustomStatusChange: (index: number, field: string, value: any) => Promise; + /** Handle custom status removal */ + onCustomStatusRemove: (index: number) => Promise; + /** Handle adding new custom status */ + onCustomStatusAdd: () => Promise; +} + +/** + * Pure UI component for rendering settings interface + */ +export class NoteStatusSettingsUI { + private callbacks: SettingsUICallbacks; + + constructor(callbacks: SettingsUICallbacks) { + this.callbacks = callbacks; + } + + /** + * Renders the complete settings interface + */ + render(containerEl: HTMLElement, settings: any): void { + containerEl.empty(); + + this.renderTemplateSettings(containerEl, settings); + this.renderUISettings(containerEl, settings); + this.renderTagSettings(containerEl, settings); + this.renderCustomStatusSettings(containerEl, settings); + } + + /** + * Renders the status templates section + */ + private renderTemplateSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Status templates').setHeading(); + + containerEl.createEl('p', { + text: 'Enable predefined templates to quickly add common status workflows', + cls: 'setting-item-description' + }); + + const templatesContainer = containerEl.createDiv({ cls: 'templates-container' }); + + PREDEFINED_TEMPLATES.forEach(template => { + const templateEl = templatesContainer.createDiv({ cls: 'template-item' }); + const headerEl = templateEl.createDiv({ cls: 'template-header' }); + + const isEnabled = settings.enabledTemplates.includes(template.id); + const checkbox = headerEl.createEl('input', { + type: 'checkbox', + cls: 'template-checkbox' + }); + checkbox.checked = isEnabled; + + checkbox.addEventListener('change', () => { + this.callbacks.onTemplateToggle(template.id, checkbox.checked); + }); + + headerEl.createEl('span', { + text: template.name, + cls: 'template-name' + }); + + templateEl.createEl('div', { + text: template.description, + cls: 'template-description' + }); + + const statusesEl = templateEl.createDiv({ cls: 'template-statuses' }); + template.statuses.forEach(status => { + const statusEl = statusesEl.createEl('div', { cls: 'template-status-chip' }); + const colorDot = statusEl.createEl('span', { cls: 'status-color-dot' }); + colorDot.style.setProperty('--dot-color', status.color || '#ffffff'); + statusEl.createSpan({ text: `${status.icon} ${status.name}` }); + }); + }); + } + + /** + * Renders the UI display settings section + */ + private renderUISettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('User interface').setHeading(); + + new Setting(containerEl) + .setName('Show status bar') + .setDesc('Display the status bar') + .addToggle(toggle => toggle + .setValue(settings.showStatusBar) + .onChange(value => this.callbacks.onSettingChange('showStatusBar', value))); + + new Setting(containerEl) + .setName('Auto-hide status bar') + .setDesc('Hide the status bar when status is unknown') + .addToggle(toggle => toggle + .setValue(settings.autoHideStatusBar) + .onChange(value => this.callbacks.onSettingChange('autoHideStatusBar', value))); + + new Setting(containerEl) + .setName('Show status icons in file explorer') + .setDesc('Display status icons in the file explorer') + .addToggle(toggle => toggle + .setValue(settings.showStatusIconsInExplorer) + .onChange(value => this.callbacks.onSettingChange('showStatusIconsInExplorer', value))); + + new Setting(containerEl) + .setName('Hide unknown status in file explorer') + .setDesc('Hide status icons for files with unknown status in the file explorer') + .addToggle(toggle => toggle + .setValue(settings.hideUnknownStatusInExplorer || false) + .onChange(value => this.callbacks.onSettingChange('hideUnknownStatusInExplorer', value))); + + new Setting(containerEl) + .setName('Default to compact view') + .setDesc('Start the status pane in compact view by default') + .addToggle(toggle => toggle + .setValue(settings.compactView || false) + .onChange(value => this.callbacks.onSettingChange('compactView', value))); + + new Setting(containerEl) + .setName('Exclude unassigned notes from status pane') + .setDesc('Improves performance by excluding notes with no assigned status from the status pane. Recommended for large vaults.') + .addToggle(toggle => toggle + .setValue(settings.excludeUnknownStatus || false) + .onChange(value => this.callbacks.onSettingChange('excludeUnknownStatus', value))); + } + + /** + * Renders the status tag configuration section + */ + private renderTagSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Status tag').setHeading(); + + new Setting(containerEl) + .setName('Enable multiple statuses') + .setDesc('Allow notes to have multiple statuses at the same time') + .addToggle(toggle => toggle + .setValue(settings.useMultipleStatuses) + .onChange(value => this.callbacks.onSettingChange('useMultipleStatuses', value))); + + new Setting(containerEl) + .setName('Status tag prefix') + .setDesc('The YAML frontmatter tag name used for status (default: obsidian-note-status)') + .addText(text => text + .setValue(settings.tagPrefix) + .onChange(value => { + if (value.trim()) { + this.callbacks.onSettingChange('tagPrefix', value.trim()); + } + })); + } + + /** + * Renders the custom status management section + */ + private renderCustomStatusSettings(containerEl: HTMLElement, settings: any): void { + new Setting(containerEl).setName('Custom statuses').setHeading(); + + new Setting(containerEl) + .setName('Use only custom statuses') + .setDesc('Ignore template statuses and use only the custom statuses defined below') + .addToggle(toggle => toggle + .setValue(settings.useCustomStatusesOnly || false) + .onChange(value => this.callbacks.onSettingChange('useCustomStatusesOnly', value))); + + const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); + this.renderCustomStatuses(statusList, settings); + + new Setting(containerEl) + .setName('Add new status') + .setDesc('Add a custom status with a name, icon, and color') + .addButton(button => button + .setButtonText('Add Status') + .setCta() + .onClick(() => this.callbacks.onCustomStatusAdd())); + } + + /** + * Renders the list of custom statuses with edit controls + */ + renderCustomStatuses(statusList: HTMLElement, settings: any): void { + statusList.empty(); + + settings.customStatuses.forEach((status: Status, index: number) => { + const setting = new Setting(statusList) + .setName(status.name) + .setClass('status-item'); + + setting.addText(text => text + .setPlaceholder('Name') + .setValue(status.name) + .onChange(value => this.callbacks.onCustomStatusChange(index, 'name', value || 'unnamed'))); + + setting.addText(text => text + .setPlaceholder('Icon') + .setValue(status.icon) + .onChange(value => this.callbacks.onCustomStatusChange(index, 'icon', value || '❓'))); + + setting.addColorPicker(colorPicker => colorPicker + .setValue(settings.statusColors[status.name] || '#ffffff') + .onChange(value => this.callbacks.onCustomStatusChange(index, 'color', value))); + + setting.addText(text => text + .setPlaceholder('Description') + .setValue(status.description || '') + .onChange(value => this.callbacks.onCustomStatusChange(index, 'description', value))); + + setting.addButton(button => button + .setButtonText('Remove') + .setClass('status-remove-button') + .setWarning() + .onClick(() => this.callbacks.onCustomStatusRemove(index))); + }); + } +} diff --git a/main.ts b/main.ts index fda59f4..cfca185 100644 --- a/main.ts +++ b/main.ts @@ -80,7 +80,7 @@ export default class NoteStatus extends Plugin { }); // Añadir pestaña de configuración - // this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); + this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); } private initializeIntegrations() { @@ -240,25 +240,21 @@ export default class NoteStatus extends Plugin { async saveSettings() { // await this.saveData(this.settings); - // // Actualizar servicios - // this.statusService.updateSettings(this.settings); - // this.styleService.updateSettings(this.settings); + // Actualizar servicios + this.statusService.updateSettings(this.settings); + this.styleService.updateSettings(this.settings); - // // Actualizar integraciones - // this.updateIntegrationsSettings(); - } - - private updateIntegrationsSettings() { - // // Actualizar configuración en todas las integraciones - // this.explorerIntegration.updateSettings(this.settings); - // this.fileContextMenuIntegration.updateSettings(this.settings); + // Actualizar integraciones + this.explorerIntegration.updateSettings(this.settings); + this.fileContextMenuIntegration.updateSettings(this.settings); // this.editorIntegration.updateSettings(this.settings); - // this.toolbarIntegration.updateSettings(this.settings); - // this.metadataIntegration.updateSettings(this.settings); - // this.workspaceIntegration.updateSettings(this.settings); + //this.metadataIntegration.updateSettings(this.settings); + this.toolbarIntegration.updateSettings(this.settings); + this.workspaceIntegration.updateSettings(this.settings); // // Actualizar componentes UI - // this.statusBar.updateSettings(this.settings); + this.statusBar.updateSettings(this.settings); + this.statusDropdown.updateSettings(this.settings) } onunload() {