import { Mutex } from "async-mutex"; import { App, Notice, PluginSettingTab } from "obsidian"; import CheckboxSyncPlugin from "../main"; import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "../types"; import { ErrorDisplay } from "./ErrorDisplay"; import { SettingGroup } from "./SettingGroup"; import { ISettingsControlActions, SettingsControls } from "./SettingsControls"; import { CheckedSymbolsSettingComponent } from "./components/checkboxSymbolConfiguration/CheckedSymbolsSettingComponent"; import { IgnoreSymbolsSettingComponent } from "./components/checkboxSymbolConfiguration/IgnoreSymbolsSettingComponent"; import { UncheckedSymbolsSettingComponent } from "./components/checkboxSymbolConfiguration/UncheckedSymbolsSettingComponent"; import { UnknownPolicySettingComponent } from "./components/checkboxSymbolConfiguration/UnknownPolicySettingComponent"; import { EnableChildSyncSettingComponent } from "./components/synchronizationBehavior/EnableChildSyncSettingComponent"; import { EnableFileSyncSettingComponent } from "./components/synchronizationBehavior/EnableFileSyncSettingComponent"; import { EnableParentSyncSettingComponent } from "./components/synchronizationBehavior/EnableParentSyncSettingComponent"; import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals"; import { SettingsValidator } from "./validation/SettingsValidator"; import { ValidationError } from "./validation/types"; import { EnableConsoleLogSettingComponent } from "./components/devGroup/EnableDebug"; import { PathGlobsSettingComponent } from "./components/pathGroup/PathGlobsSettingComponent"; export class CheckboxSyncPluginSettingTab extends PluginSettingTab { plugin: CheckboxSyncPlugin; private settingGroups: SettingGroup[] = [] private errorDisplay: ErrorDisplay; private settingsControls: SettingsControls; private isDirty: boolean = false; private actionMutex = new Mutex(); constructor(app: App, plugin: CheckboxSyncPlugin) { super(app, plugin); this.plugin = plugin; this.initializeSettingGroups(); } // Метод для создания и конфигурации групп и компонентов private initializeSettingGroups(): void { // Создаем экземпляры наших компонентов const checkedSymbolsComp = new CheckedSymbolsSettingComponent(); const uncheckedSymbolsComp = new UncheckedSymbolsSettingComponent(); const unknownPolicyComp = new UnknownPolicySettingComponent(); const ignoreSymbolsComp = new IgnoreSymbolsSettingComponent(); const symbolGroup = new SettingGroup( "Checkbox Symbol Configuration (Advanced: JSON)", [checkedSymbolsComp, uncheckedSymbolsComp, ignoreSymbolsComp, unknownPolicyComp], "Warning: Requires valid JSON format. Use double quotes for strings." ); const parentToggleComp = new EnableParentSyncSettingComponent(); const childrenToggleComp = new EnableChildSyncSettingComponent(); const automaticFileSyncToggleComp = new EnableFileSyncSettingComponent(); const behaviorGroup = new SettingGroup( "Synchronization Behavior", [parentToggleComp, childrenToggleComp, automaticFileSyncToggleComp] ); const pathGlobsComp = new PathGlobsSettingComponent(); const filteringGroup = new SettingGroup( "File Scope & Filtering", [pathGlobsComp], "Define rules to include or exclude files/folders from plugin processing." ); const consoleLog = new EnableConsoleLogSettingComponent(); const devGroup = new SettingGroup( "Dev", [consoleLog] ); // Сохраняем созданную группу (или группы) в поле класса this.settingGroups = [ symbolGroup, behaviorGroup, filteringGroup, devGroup ]; const changeListener = () => this.settingChanged(); for (const group of this.settingGroups) { for (const component of group.components) { // Устанавливаем общий listener для всех компонентов component.setChangeListener(changeListener); } } } private settingChanged() { this.isDirty = true; this.settingsControls.setApplyState({ disabled: false, cta: true }); } private settingSaved() { this.isDirty = false; this.settingsControls.setApplyState({ disabled: true, cta: false }); } display(): void { this.isDirty = false; const containerEl = this.containerEl; containerEl.empty(); containerEl.createEl('h2', { text: 'Checkbox Sync Settings' }); for (const group of this.settingGroups) { group.render(containerEl, this.plugin.settings); } // --- Область для вывода ошибок --- this.errorDisplay = new ErrorDisplay(containerEl); // Создаем экземпляр SettingsControls const controlActions: ISettingsControlActions = { onApply: () => this.applyChanges(), onReset: () => this.resetInputsToSavedSettings(), // Оставляем ConfirmModal здесь, но передаем вызов applyDefaultSettings onResetDefaults: () => { new ConfirmModal(this.app, "Reset all settings to default and apply immediately?\nThis cannot be undone.", // Только если пользователь нажал ОК, вызываем реальный сброс async () => { await this.applyDefaultSettings(); } // Optional: callback for cancel if needed ).open(); } }; this.settingsControls = new SettingsControls(containerEl, controlActions); // Устанавливаем начальное состояние кнопки Apply // (isDirty здесь всегда false при первом вызове display) this.settingsControls.setApplyState({ disabled: !this.isDirty, cta: this.isDirty }); // Начальное состояние кнопки Reset Defaults (всегда включена) this.settingsControls.setResetDefaultsState({ disabled: false }); } private async applyChanges() { await this.actionMutex.runExclusive(async () => { this.settingsControls.setApplyState({ disabled: true, text: 'Applying...', cta: false }); this.errorDisplay.clear(); try { const result = await this.validateAndSaveSettings(); if (result.success) { this.settingSaved(); new Notice("Checkbox Sync settings applied!", 3000); } else { console.warn("Validation errors:", result.errors); this.errorDisplay.displayErrors(result.errors); } } catch (error: any) { console.error("Unexpected error during applyChanges:", error); const message = error.message || "Unknown error"; this.errorDisplay.displayMessage(`An unexpected error occurred: ${message}`); } finally { this.settingsControls.setApplyState({ disabled: !this.isDirty, cta: this.isDirty }) } }); } /** * Resets the settings UI elements to reflect the currently saved plugin settings. */ private resetInputsToSavedSettings() { const settings = this.plugin.settings; for (const group of this.settingGroups) { for (const component of group.components) { const key = component.getSettingKey(); if (key in settings) { try { component.setValueInUi(settings[key]); } catch (error) { console.error(`Error resetting UI for component ${key}:`, error); } } } } // Очищаем ошибку и сбрасываем состояние "грязный" this.errorDisplay.clear(); this.settingSaved(); // Используем существующий метод для сброса isDirty и состояния кнопки Apply } /** * Applies the default settings immediately after confirmation. */ private async applyDefaultSettings() { await this.actionMutex.runExclusive(async () => { this.settingsControls.setResetDefaultsState({ disabled: true, text: 'Resetting...' }); this.errorDisplay.clear(); try { const defaultsCopy = { ...DEFAULT_SETTINGS }; // Работаем с копией await this.plugin.updateSettings(settings => { // Полностью перезаписываем текущие настройки дефолтными Object.assign(settings, defaultsCopy); }); // После успешного сохранения обновляем UI и состояние this.resetInputsToSavedSettings(); // Обновит UI по новым settings и сбросит isDirty/кнопку Apply new Notice("Settings reset to defaults and applied.", 3000); } catch (error: any) { console.error("Error resetting settings to default:", error); const message = error.message || "Unknown error"; this.errorDisplay.displayMessage(`Error resetting to defaults: ${message}`); } finally { // Восстанавливаем кнопку Reset this.settingsControls.setResetDefaultsState({ disabled: false }); } }); } /** * Overridden hide method to handle unsaved changes. */ async hide() { if (this.isDirty) { // Колбэк для кнопки "Save" const saveCallback = async () => { try { const result = await this.validateAndSaveSettings(); if (result.success) { this.isDirty = false; // Сбрасываем флаг только при успехе new Notice("Settings saved.", 2000); } else { console.error("Error saving settings on hide (validation):", result.errors); const errorMessage = result.errors .map(e => `❌ ${e.field ? `[${e.field}]: ` : ''}${e.message}`) .join('\n'); new InfoModal(this.app, `Failed to save settings:\n\n${errorMessage}\n\nYour changes were not saved.`).open(); } } catch (error: any) { // Ловим НЕОЖИДАННЫЕ ошибки (например, ошибка сохранения) console.error("Error saving settings on hide (unexpected):", error); let errorMessage = "An unknown error occurred."; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } new InfoModal(this.app, `Failed to save settings:\n\n${errorMessage}\n\nYour changes were not saved.`).open(); } }; // Колбэк для кнопки "Discard" const discardCallback = () => { this.isDirty = false; // Считаем изменения отмененными }; // Открываем модалку Save/Discard и ЖДЕМ ее закрытия const saveModal = new SaveConfirmModal(this.app, saveCallback, discardCallback); saveModal.open(); } // Продолжаем стандартное закрытие super.hide(); } /** * Reads UI values, validates them, and calls plugin.updateSettings. * Throws an error if validation or saving fails. * Does NOT interact with UI feedback (buttons, notices, error display). */ private async validateAndSaveSettings(): Promise<{ success: boolean; errors: ValidationError[] }> { let errors: ValidationError[] = []; // Массив для сбора всех ошибок валидации let newSettingsData: Partial = {}; // Объект для новых данных for (const group of this.settingGroups) { for (const component of group.components) { const key = component.getSettingKey(); try { const value = component.getValueFromUi(); const validationError = component.validate(); if (validationError) { errors.push(validationError); } else { newSettingsData[key] = value; } } catch (err: any) { console.error(`Error processing component ${key}:`, err); errors.push({ field: key, message: `Failed to read value: ${err.message}` }); } } } if (errors.length === 0) { const settingsValidator = new SettingsValidator(); const crossErrors = settingsValidator.validate(newSettingsData); errors.push(...crossErrors); } if (errors.length > 0) { return { success: false, errors: errors }; } try { await this.plugin.updateSettings(settings => { Object.assign(settings, newSettingsData); }); return { success: true, errors: [] }; } catch (saveError: any) { // Ловим ТОЛЬКО ошибки сохранения (от updateSettings) console.error("Error saving settings:", saveError); throw saveError; } } }