mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 12:00:31 +00:00
316 lines
12 KiB
TypeScript
316 lines
12 KiB
TypeScript
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<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) {
|
||
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;
|
||
}
|
||
}
|
||
}
|