groldsf_obsidian_check_plugin/src/ui/CheckboxSyncPluginSettingTab.ts

316 lines
No EOL
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Mutex } from "async-mutex";
import { App, ButtonComponent, Notice, PluginSettingTab, Setting, TextComponent } from "obsidian";
import CheckboxSyncPlugin from "../main";
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "../types";
import { SettingGroup } from "./SettingGroup";
import { EnableChildSyncSettingComponent } from "./components/EnableChildSyncSettingComponent";
import { EnableFileSyncSettingComponent } from "./components/EnableFileSyncSettingComponent";
import { EnableParentSyncSettingComponent } from "./components/EnableParentSyncSettingComponent";
import { UnknownPolicySettingComponent } from "./components/UnknownPolicySettingComponent";
import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals";
import { ValidationError } from "./validation/types";
import { CheckedSymbolsSettingComponent } from "./components/CheckedSymbolsSettingComponent";
import { UncheckedSymbolsSettingComponent } from "./components/UncheckedSymbolsSettingComponent";
import { IgnoreSymbolsSettingComponent } from "./components/IgnoreSymbolsSettingComponent";
import { SettingsValidator } from "./validation/SettingsValidator";
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
plugin: CheckboxSyncPlugin;
private settingGroups: SettingGroup[] = []
private applyButton: ButtonComponent;
private resetToDefaultButton: ButtonComponent;
private errorDisplayEl: HTMLElement;
private isDirty: boolean = false;
private actionMutex = new Mutex();
constructor(app: App, plugin: CheckboxSyncPlugin) {
super(app, plugin);
this.plugin = plugin;
this.initializeSettingGroups();
}
// Метод для создания и конфигурации групп и компонентов
private initializeSettingGroups(): void {
// 1. Создаем экземпляры наших компонентов
const checkedSymbolsComp = new CheckedSymbolsSettingComponent();
checkedSymbolsComp.setChangeListener(() => this.settingChanged());
const uncheckedSymbolsComp = new UncheckedSymbolsSettingComponent();
uncheckedSymbolsComp.setChangeListener(() => this.settingChanged());
const unknownPolicyComp = new UnknownPolicySettingComponent();
unknownPolicyComp.setChangeListener(() => this.settingChanged());
const ignoreSymbolsComp = new IgnoreSymbolsSettingComponent();
ignoreSymbolsComp.setChangeListener(() => this.settingChanged());
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();
parentToggleComp.setChangeListener(() => this.settingChanged());
const childrenToggleComp = new EnableChildSyncSettingComponent();
childrenToggleComp.setChangeListener(() => this.settingChanged());
const automaticFileSyncToggleComp = new EnableFileSyncSettingComponent();
automaticFileSyncToggleComp.setChangeListener(() => this.settingChanged());
const behaviorGroup = new SettingGroup(
"Synchronization Behavior", // Заголовок группы
[parentToggleComp, childrenToggleComp, automaticFileSyncToggleComp] // Массив компонентов для этой группы
// Можно добавить описание группы третьим аргументом, если нужно
);
// 3. Сохраняем созданную группу (или группы) в поле класса
this.settingGroups = [
symbolGroup,
behaviorGroup
// Сюда позже добавятся другие группы (например, для символов)
];
}
private settingChanged() {
this.isDirty = true;
this.applyButton.setDisabled(false).setCta();
}
private settingSaved() {
this.isDirty = false;
this.applyButton.setDisabled(true).removeCta();
}
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.errorDisplayEl = containerEl.createDiv({ cls: 'checkbox-sync-settings-error' });
// Используем стандартные CSS переменные Obsidian для цвета ошибки
this.errorDisplayEl.style.color = 'var(--text-error)';
this.errorDisplayEl.style.marginTop = '10px';
this.errorDisplayEl.style.marginBottom = '10px';
this.errorDisplayEl.style.minHeight = '1.5em'; // Резервируем место
this.errorDisplayEl.style.whiteSpace = 'pre-wrap'; // Для переноса длинных ошибок
this.errorDisplayEl.style.userSelect = 'text'; // или 'all'
// Используем Setting для группировки кнопок
const buttonGroup = new Setting(containerEl)
.setClass('checkbox-sync-button-group'); // Добавим класс для возможной стилизации
buttonGroup.addButton(button => {
button
.setButtonText("Reset changes")
.setTooltip("Revert changes to the last applied settings")
.onClick(() => {
this.resetInputsToSavedSettings();
});
});
buttonGroup.addButton(button => {
this.resetToDefaultButton = button;
button
.setButtonText("Reset to defaults")
.setTooltip("Reset all settings to default values and apply immediately")
.onClick(() => {
new ConfirmModal(this.app,
"Reset all settings to default and apply immediately?\nThis cannot be undone.", // Добавил \n для переноса
async () => { await this.applyDefaultSettings(); }
).open();
});
});
// --- Кнопка Применить ---
buttonGroup
.addButton(button => {
this.applyButton = button;
button
.setButtonText("Apply Changes")
.setDisabled(!this.isDirty)
.onClick(async () => await this.applyChanges())
});
}
private async applyChanges() {
await this.actionMutex.runExclusive(async () => {
const originalButtonText = "Apply Changes";
this.applyButton.setDisabled(true).setButtonText("Applying...").removeCta();
try {
await this.validateAndSaveSettings();
this.settingSaved();
this.errorDisplayEl.setText('');
new Notice("Checkbox Sync settings applied!", 3000);
} catch (error: any) {
console.error("Error applying checkbox sync settings:", error);
// Выводим ошибку в специальный div
this.errorDisplayEl.setText(`${error.message || "An unknown error occurred."}`);
} finally {
this.applyButton.setButtonText(originalButtonText); // Восстанавливаем текст
}
});
}
/**
* 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.errorDisplayEl?.setText('');
this.settingSaved(); // Используем существующий метод для сброса isDirty и состояния кнопки Apply
}
/**
* Applies the default settings immediately after confirmation.
*/
private async applyDefaultSettings() {
await this.actionMutex.runExclusive(async () => {
this.resetToDefaultButton.setDisabled(true).setButtonText("Resetting...");
this.errorDisplayEl.setText(''); // Очищаем предыдущие ошибки
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);
this.errorDisplayEl.setText(`❌ Error resetting to defaults: ${error.message}`);
} finally {
// Восстанавливаем кнопку Reset
this.resetToDefaultButton.setDisabled(false).setButtonText("Reset to defaults");
}
});
}
/**
* Overridden hide method to handle unsaved changes.
*/
async hide() {
if (this.isDirty) {
// Колбэк для кнопки "Save"
const saveCallback = async () => {
try {
await this.validateAndSaveSettings();
this.isDirty = false; // Сбрасываем флаг
new Notice("Settings saved.", 2000);
} catch (error: any) {
console.error("Error saving settings on hide:", 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<void> {
let errors: ValidationError[] = []; // Массив для сбора всех ошибок валидации
let newSettingsData: Partial<CheckboxSyncPluginSettings> = {}; // Объект для новых данных
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) {
// Формируем сообщение об ошибке
const errorMessage = errors.map(e => `${e.field ? `[${e.field}]: ` : ''}${e.message}`).join('\n');
throw new Error(errorMessage);
}
// Вызываем сохранение -> await (может кинуть ошибку)
await this.plugin.updateSettings(settings => {
Object.assign(settings, newSettingsData);
});
}
}