mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
feat: Add reset buttons and dirty state handling to settings
This commit is contained in:
parent
7068d32440
commit
ade67c54ba
4 changed files with 348 additions and 74 deletions
19
src/main.ts
19
src/main.ts
|
|
@ -1,20 +1,11 @@
|
|||
import { Plugin, TFile } from "obsidian";
|
||||
import { CheckboxSyncPluginSettingTab } from "./settings/CheckboxSyncPluginSettingTab";
|
||||
import { CheckboxUtils } from "./checkboxUtils";
|
||||
import FileStateHolder from "./FileStateHolder";
|
||||
import SyncController from "./SyncController";
|
||||
import { CheckboxState, CheckboxSyncPluginSettings } from "./types";
|
||||
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
|
||||
import { FileChangeEventHandler } from "./events/FileChangeEventHandler";
|
||||
|
||||
const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
xOnlyMode: true,
|
||||
enableAutomaticParentState: true,
|
||||
enableAutomaticChildState: true,
|
||||
checkedSymbols: ['x'],
|
||||
uncheckedSymbols: [' '],
|
||||
unknownSymbolPolicy: CheckboxState.Checked,
|
||||
};
|
||||
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
|
||||
import FileStateHolder from "./FileStateHolder";
|
||||
import { CheckboxSyncPluginSettingTab } from "./settings/CheckboxSyncPluginSettingTab";
|
||||
import SyncController from "./SyncController";
|
||||
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "./types";
|
||||
|
||||
export default class CheckboxSyncPlugin extends Plugin {
|
||||
private _settings: CheckboxSyncPluginSettings;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { App, ButtonComponent, DropdownComponent, Notice, PluginSettingTab, Setting, TextComponent, ToggleComponent } from "obsidian";
|
||||
import CheckboxSyncPlugin from "../main";
|
||||
import { CheckboxState } from "../types";
|
||||
import { CheckboxState, DEFAULT_SETTINGS } from "../types";
|
||||
import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||
plugin: CheckboxSyncPlugin;
|
||||
|
|
@ -10,7 +12,10 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
private parentToggle: ToggleComponent;
|
||||
private childToggle: ToggleComponent;
|
||||
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);
|
||||
|
|
@ -78,7 +83,18 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
return { isValid: true };
|
||||
}
|
||||
|
||||
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' });
|
||||
|
|
@ -94,7 +110,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
text
|
||||
.setPlaceholder("e.g., [\"x\", \" \"]")
|
||||
.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols))
|
||||
.onChange(() => this.applyButton.setDisabled(false))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
// --- Unchecked Symbols ---
|
||||
|
|
@ -106,7 +122,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
text
|
||||
.setPlaceholder("e.g., [\" \", \"?\", \",\"]")
|
||||
.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols))
|
||||
.onChange(() => this.applyButton.setDisabled(false))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
// --- Unknown Symbol Policy ---
|
||||
|
|
@ -120,7 +136,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
.addOption(CheckboxState.Unchecked, 'Treat as Unchecked')
|
||||
.addOption(CheckboxState.Ignore, 'Ignore')
|
||||
.setValue(this.plugin.settings.unknownSymbolPolicy)
|
||||
.onChange(() => this.applyButton.setDisabled(false))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -133,7 +149,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
this.parentToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticParentState)
|
||||
.onChange(() => this.applyButton.setDisabled(false))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -145,7 +161,7 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
this.childToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticChildState)
|
||||
.onChange(() => this.applyButton.setDisabled(false))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -159,77 +175,209 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
|||
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();
|
||||
});
|
||||
});
|
||||
// --- Кнопка Применить ---
|
||||
new Setting(containerEl)
|
||||
buttonGroup
|
||||
.addButton(button => {
|
||||
this.applyButton = button;
|
||||
button
|
||||
.setButtonText("Apply Changes")
|
||||
.setCta() // Делает кнопку более заметной (синей)
|
||||
.setDisabled(true)
|
||||
.setDisabled(!this.isDirty)
|
||||
.onClick(async () => await this.applyChanges())
|
||||
});
|
||||
}
|
||||
|
||||
private async applyChanges() {
|
||||
const originalButtonText = "Apply Changes";
|
||||
this.applyButton.setDisabled(true).setButtonText("Applying...");
|
||||
await this.actionMutex.runExclusive(async () => {
|
||||
const originalButtonText = "Apply Changes";
|
||||
this.applyButton.setDisabled(true).setButtonText("Applying...").removeCta();
|
||||
|
||||
try {
|
||||
const checkedValue = this.checkedSymbolsInput.getValue();
|
||||
const uncheckedValue = this.uncheckedSymbolsInput.getValue();
|
||||
const policyValue = this.unknownPolicyDropdown.getValue() as CheckboxState;
|
||||
const parentValue = this.parentToggle.getValue();
|
||||
const childValue = this.childToggle.getValue();
|
||||
|
||||
const parsedChecked = this.parseJsonStringArray(checkedValue);
|
||||
const parsedUnchecked = this.parseJsonStringArray(uncheckedValue);
|
||||
try {
|
||||
await this.validateAndSaveSettings();
|
||||
|
||||
if (parsedChecked.error) {
|
||||
throw new Error(`Checked Symbols Error: ${parsedChecked.error}`);
|
||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols));
|
||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols));
|
||||
|
||||
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."}`);
|
||||
return error;
|
||||
} finally {
|
||||
this.applyButton?.setButtonText(originalButtonText); // Восстанавливаем текст
|
||||
}
|
||||
if (parsedUnchecked.error) {
|
||||
throw new Error(`Unchecked Symbols Error: ${parsedUnchecked.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the settings UI elements to reflect the currently saved plugin settings.
|
||||
*/
|
||||
private resetInputsToSavedSettings() {
|
||||
const settings = this.plugin.settings; // Получаем текущие сохраненные настройки
|
||||
|
||||
// Обновляем значения всех полей ввода
|
||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(settings.checkedSymbols));
|
||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(settings.uncheckedSymbols));
|
||||
this.unknownPolicyDropdown.setValue(settings.unknownSymbolPolicy);
|
||||
this.parentToggle.setValue(settings.enableAutomaticParentState);
|
||||
this.childToggle.setValue(settings.enableAutomaticChildState);
|
||||
|
||||
// Очищаем ошибку и сбрасываем состояние "грязный"
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const checkedSymbolsArray = parsedChecked.result;
|
||||
const uncheckedSymbolsArray = parsedUnchecked.result;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
};
|
||||
|
||||
const intersectionValidation = this.validateSettingsArraysIntersection(checkedSymbolsArray, uncheckedSymbolsArray);
|
||||
if (!intersectionValidation.isValid) {
|
||||
throw new Error(intersectionValidation.error!);
|
||||
}
|
||||
// Колбэк для кнопки "Discard"
|
||||
const discardCallback = () => {
|
||||
this.isDirty = false; // Считаем изменения отмененными
|
||||
};
|
||||
|
||||
if (checkedSymbolsArray.length === 0) {
|
||||
throw new Error("Checked symbols list cannot be empty.")
|
||||
}
|
||||
if (uncheckedSymbolsArray.length === 0) {
|
||||
throw new Error("Unchecked symbols list cannot be empty.");
|
||||
}
|
||||
|
||||
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.checkedSymbols = checkedSymbolsArray;
|
||||
settings.uncheckedSymbols = uncheckedSymbolsArray;
|
||||
settings.unknownSymbolPolicy = policyValue;
|
||||
settings.enableAutomaticParentState = parentValue;
|
||||
settings.enableAutomaticChildState = childValue;
|
||||
console.log(JSON.stringify(settings));
|
||||
});
|
||||
|
||||
// 8. Обновляем поля ввода каноническим JSON (на случай исправлений парсером)
|
||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(checkedSymbolsArray));
|
||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(uncheckedSymbolsArray));
|
||||
|
||||
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); // Восстанавливаем текст
|
||||
// Открываем модалку 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> {
|
||||
// 1. Считываем значения из UI
|
||||
const checkedValue = this.checkedSymbolsInput.getValue();
|
||||
const uncheckedValue = this.uncheckedSymbolsInput.getValue();
|
||||
const policyValue = this.unknownPolicyDropdown.getValue() as CheckboxState;
|
||||
const parentValue = this.parentToggle.getValue();
|
||||
const childValue = this.childToggle.getValue();
|
||||
|
||||
// 2. Парсим JSON
|
||||
const parsedChecked = this.parseJsonStringArray(checkedValue);
|
||||
const parsedUnchecked = this.parseJsonStringArray(uncheckedValue);
|
||||
|
||||
// 3. Проверяем ошибки парсинга -> throw
|
||||
if (parsedChecked.error) {
|
||||
throw new Error(`Checked Symbols Error: ${parsedChecked.error}`);
|
||||
}
|
||||
if (parsedUnchecked.error) {
|
||||
throw new Error(`Unchecked Symbols Error: ${parsedUnchecked.error}`);
|
||||
}
|
||||
|
||||
const checkedSymbolsArray = parsedChecked.result;
|
||||
const uncheckedSymbolsArray = parsedUnchecked.result;
|
||||
|
||||
// 4. Валидируем пересечение -> throw
|
||||
const intersectionValidation = this.validateSettingsArraysIntersection(
|
||||
checkedSymbolsArray,
|
||||
uncheckedSymbolsArray
|
||||
);
|
||||
if (!intersectionValidation.isValid) {
|
||||
throw new Error(intersectionValidation.error!);
|
||||
}
|
||||
|
||||
// 5. Валидируем на пустые списки -> throw
|
||||
if (checkedSymbolsArray.length === 0) {
|
||||
throw new Error("Checked symbols list cannot be empty.");
|
||||
}
|
||||
if (uncheckedSymbolsArray.length === 0) {
|
||||
throw new Error("Unchecked symbols list cannot be empty.");
|
||||
}
|
||||
|
||||
// 6. Вызываем сохранение -> await (может кинуть ошибку)
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.checkedSymbols = checkedSymbolsArray;
|
||||
settings.uncheckedSymbols = uncheckedSymbolsArray;
|
||||
settings.unknownSymbolPolicy = policyValue;
|
||||
settings.enableAutomaticParentState = parentValue;
|
||||
settings.enableAutomaticChildState = childValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
126
src/settings/modals.ts
Normal file
126
src/settings/modals.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// Добавляем Modal в импорты из 'obsidian'
|
||||
import { App, Modal, Setting } from "obsidian";
|
||||
|
||||
// --- НОВЫЙ КОД: Класс ConfirmModal ---
|
||||
export class ConfirmModal extends Modal {
|
||||
message: string;
|
||||
onConfirm: () => void; // Синхронный колбэк
|
||||
|
||||
constructor(app: App, message: string, onConfirm: () => void) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty(); // Очищаем на всякий случай
|
||||
contentEl.createEl('p').setText(this.message);
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Cancel')
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
}))
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('OK') // Или "Confirm", "Yes"
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onConfirm(); // Вызываем колбэк
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
// --- КОНЕЦ ConfirmModal ---
|
||||
|
||||
// --- НОВЫЙ КОД: Класс InfoModal ---
|
||||
export class InfoModal extends Modal {
|
||||
message: string;
|
||||
|
||||
constructor(app: App, message: string) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
// Используем <pre> для сохранения форматирования ошибки
|
||||
const pre = contentEl.createEl('pre');
|
||||
pre.setText(this.message);
|
||||
pre.style.whiteSpace = 'pre-wrap'; // Для переноса строк
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('OK')
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
// --- КОНЕЦ InfoModal ---
|
||||
|
||||
// --- НОВЫЙ КОД: Класс SaveConfirmModal ---
|
||||
export class SaveConfirmModal extends Modal {
|
||||
onSave: () => Promise<void>; // Асинхронный колбэк для сохранения
|
||||
onDiscard: () => void;
|
||||
|
||||
constructor(app: App, onSave: () => Promise<void>, onDiscard: () => void) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
this.onDiscard = onDiscard;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.setText('You have unsaved changes. Save them before closing?');
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Discard')
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onDiscard();
|
||||
}))
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Save')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true).setButtonText('Saving...');
|
||||
try {
|
||||
await this.onSave();
|
||||
this.close();
|
||||
} catch (e) {
|
||||
console.error("Unexpected error during save callback:", e);
|
||||
new InfoModal(this.app, "An unexpected error occurred during save.").open();
|
||||
this.close(); // Закрываем в любом случае после ошибки
|
||||
} finally {
|
||||
// Восстанавливаем кнопку на случай если модалка не закроется (не должно быть)
|
||||
btn.setDisabled(false).setButtonText('Save');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
11
src/types.ts
11
src/types.ts
|
|
@ -11,4 +11,13 @@ export enum CheckboxState {
|
|||
Checked = 'checked',
|
||||
Unchecked = 'unchecked',
|
||||
Ignore = 'ignore'
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
xOnlyMode: true,
|
||||
enableAutomaticParentState: true,
|
||||
enableAutomaticChildState: true,
|
||||
checkedSymbols: ['x'],
|
||||
uncheckedSymbols: [' '],
|
||||
unknownSymbolPolicy: CheckboxState.Checked,
|
||||
};
|
||||
Loading…
Reference in a new issue