2025-07-15 15:30:17 +00:00
|
|
|
import { DEFAULT_PLUGIN_SETTINGS } from "@/constants/defaultSettings";
|
2026-03-18 10:45:54 +00:00
|
|
|
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
|
2026-03-18 18:02:33 +00:00
|
|
|
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
2026-03-18 10:32:38 +00:00
|
|
|
import { Plugin, TFile, normalizePath } from "obsidian";
|
2025-07-15 15:30:17 +00:00
|
|
|
import eventBus from "./eventBus";
|
|
|
|
|
|
2026-03-18 10:45:54 +00:00
|
|
|
export const SETTINGS_GROUPS: Record<SyncGroup, (keyof PluginSettings)[]> = {
|
2026-03-18 11:35:49 +00:00
|
|
|
templates: ["templates", "enabledTemplates"],
|
|
|
|
|
customStatuses: ["customStatuses"],
|
|
|
|
|
statusColors: ["statusColors"],
|
|
|
|
|
uiAppearance: [
|
2026-03-18 10:45:54 +00:00
|
|
|
"fileExplorerIconPosition",
|
|
|
|
|
"fileExplorerIconFrame",
|
|
|
|
|
"fileExplorerIconColorMode",
|
|
|
|
|
"showStatusBar",
|
|
|
|
|
"autoHideStatusBar",
|
|
|
|
|
"statusBarShowTemplateName",
|
|
|
|
|
"statusBarBadgeStyle",
|
|
|
|
|
"statusBarBadgeContentMode",
|
|
|
|
|
"showStatusIconsInExplorer",
|
|
|
|
|
"hideUnknownStatusInExplorer",
|
|
|
|
|
"fileExplorerColorFileName",
|
|
|
|
|
"fileExplorerColorBlock",
|
|
|
|
|
"fileExplorerLeftBorder",
|
|
|
|
|
"fileExplorerStatusDot",
|
|
|
|
|
"fileExplorerUnderlineFileName",
|
|
|
|
|
"unknownStatusIcon",
|
|
|
|
|
"unknownStatusLucideIcon",
|
|
|
|
|
"unknownStatusColor",
|
|
|
|
|
"statusBarNoStatusText",
|
|
|
|
|
"statusBarShowNoStatusIcon",
|
|
|
|
|
"statusBarShowNoStatusText",
|
|
|
|
|
"showEditorToolbarButton",
|
|
|
|
|
"editorToolbarButtonPosition",
|
|
|
|
|
"editorToolbarButtonDisplay",
|
|
|
|
|
],
|
2026-03-18 11:35:49 +00:00
|
|
|
workflow: [
|
|
|
|
|
"useCustomStatusesOnly",
|
|
|
|
|
"useMultipleStatuses",
|
|
|
|
|
"singleStatusStorageMode",
|
|
|
|
|
"strictStatuses",
|
|
|
|
|
"enableStatusOverviewPopup",
|
2026-03-18 18:38:17 +00:00
|
|
|
"defaultStatusForNewNotes",
|
2026-03-18 11:35:49 +00:00
|
|
|
],
|
|
|
|
|
storage: [
|
2026-03-18 10:45:54 +00:00
|
|
|
"tagPrefix",
|
|
|
|
|
"statusFrontmatterMappings",
|
|
|
|
|
"writeMappedTagsToDefault",
|
|
|
|
|
"applyStatusRecursivelyToSubfolders",
|
|
|
|
|
],
|
|
|
|
|
features: [
|
|
|
|
|
"quickStatusCommands",
|
|
|
|
|
"enableExperimentalFeatures",
|
|
|
|
|
"enableStatusDashboard",
|
|
|
|
|
"enableGroupedStatusView",
|
2026-03-18 11:35:49 +00:00
|
|
|
"vaultSizeLimit",
|
2026-03-18 10:45:54 +00:00
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-15 15:30:17 +00:00
|
|
|
class SettingsService {
|
|
|
|
|
private plugin: Plugin;
|
|
|
|
|
public settings: PluginSettings;
|
2026-03-18 10:32:38 +00:00
|
|
|
private isWatcherRegistered = false;
|
2025-07-15 15:30:17 +00:00
|
|
|
|
|
|
|
|
constructor() {}
|
|
|
|
|
|
|
|
|
|
async initialize(plugin: Plugin): Promise<void> {
|
|
|
|
|
this.plugin = plugin;
|
2026-03-18 18:02:33 +00:00
|
|
|
const loadedData = await this.loadSettings();
|
|
|
|
|
this.migrateLegacySettings(loadedData);
|
2026-03-18 10:32:38 +00:00
|
|
|
|
|
|
|
|
if (this.settings.enableExternalStatusSync) {
|
|
|
|
|
await this.loadFromExternalFile();
|
|
|
|
|
this.setupExternalFileWatcher();
|
|
|
|
|
}
|
2025-07-15 15:30:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setValue(key: keyof PluginSettings, value: unknown) {
|
|
|
|
|
if (!(key in this.settings)) {
|
|
|
|
|
throw new Error(`The "${key}" setting is not a known setting key`);
|
|
|
|
|
}
|
2025-07-19 11:59:23 +00:00
|
|
|
// const oldValue = this.settings[key]; // TODO: Send the old value
|
2025-07-15 15:30:17 +00:00
|
|
|
this.settings[key] = value;
|
|
|
|
|
this.saveSettings().catch(console.error);
|
2026-03-18 10:32:38 +00:00
|
|
|
|
|
|
|
|
// Handle external sync
|
|
|
|
|
if (this.settings.enableExternalStatusSync) {
|
|
|
|
|
this.syncToExternalFile().catch(console.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key === "enableExternalStatusSync") {
|
|
|
|
|
if (value) {
|
|
|
|
|
this.syncToExternalFile().catch(console.error);
|
|
|
|
|
this.setupExternalFileWatcher();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 15:30:17 +00:00
|
|
|
// INFO: Send the propgation event
|
|
|
|
|
eventBus.publish("plugin-settings-changed", {
|
|
|
|
|
key,
|
|
|
|
|
value,
|
|
|
|
|
currentSettings: this.settings,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadSettings() {
|
2026-03-18 10:32:38 +00:00
|
|
|
const loadedData = await this.plugin.loadData();
|
|
|
|
|
this.settings = this.mergeSettings(DEFAULT_PLUGIN_SETTINGS, loadedData);
|
2026-03-18 15:20:01 +00:00
|
|
|
this.deduplicateTemplates();
|
2026-03-18 18:02:33 +00:00
|
|
|
return loadedData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Migrates legacy settings and ensures new users have a starter template.
|
|
|
|
|
*/
|
|
|
|
|
private migrateLegacySettings(loadedData: Partial<PluginSettings> | null) {
|
|
|
|
|
const hasTemplates =
|
|
|
|
|
this.settings.templates && this.settings.templates.length > 0;
|
|
|
|
|
|
2026-03-19 10:52:30 +00:00
|
|
|
// 1. If it's a truly new user (no data at all), wait, we now provide all templates in DEFAULT_PLUGIN_SETTINGS
|
2026-03-18 18:02:33 +00:00
|
|
|
if (!loadedData) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. If it's an existing user but has no templates, they were using the old defaults
|
|
|
|
|
if (!hasTemplates) {
|
|
|
|
|
const legacyIds = [
|
|
|
|
|
"colorful",
|
|
|
|
|
"minimal",
|
|
|
|
|
"academic",
|
|
|
|
|
"project",
|
|
|
|
|
"creative-writing",
|
|
|
|
|
];
|
|
|
|
|
const enabledLegacy = (this.settings.enabledTemplates || []).filter(
|
|
|
|
|
(id) => legacyIds.includes(id),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (enabledLegacy.length > 0) {
|
|
|
|
|
// Restore the ones they had enabled from the marketplace list
|
|
|
|
|
const toRestore = PREDEFINED_TEMPLATES.filter((t) =>
|
|
|
|
|
enabledLegacy.includes(t.id),
|
|
|
|
|
).map((t) => ({
|
|
|
|
|
...t,
|
|
|
|
|
id: t.id, // Keep original ID for legacy compatibility
|
|
|
|
|
statuses: t.statuses.map((s) => ({
|
|
|
|
|
...s,
|
|
|
|
|
templateId: t.id,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this.settings.templates = toRestore;
|
|
|
|
|
// Update enabled list
|
|
|
|
|
this.settings.enabledTemplates = toRestore.map((t) => t.id);
|
|
|
|
|
this.saveSettings().catch(console.error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 15:20:01 +00:00
|
|
|
/**
|
|
|
|
|
* Removes duplicate templates with the same name, keeping the first occurrence.
|
|
|
|
|
*/
|
|
|
|
|
private deduplicateTemplates() {
|
|
|
|
|
if (!this.settings.templates) return;
|
|
|
|
|
|
|
|
|
|
const seenNames = new Set<string>();
|
|
|
|
|
const uniqueTemplates = [];
|
|
|
|
|
let hasDuplicates = false;
|
|
|
|
|
|
|
|
|
|
for (const template of this.settings.templates) {
|
|
|
|
|
const lowerName = template.name.toLowerCase().trim();
|
|
|
|
|
if (!seenNames.has(lowerName)) {
|
|
|
|
|
seenNames.add(lowerName);
|
|
|
|
|
uniqueTemplates.push(template);
|
|
|
|
|
} else {
|
|
|
|
|
hasDuplicates = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasDuplicates) {
|
|
|
|
|
this.settings.templates = uniqueTemplates;
|
|
|
|
|
this.saveSettings().catch(console.error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:32:38 +00:00
|
|
|
/**
|
|
|
|
|
* Deep merges loaded settings with defaults to prevent data loss for nested objects like statusColors.
|
|
|
|
|
*/
|
|
|
|
|
private mergeSettings(
|
|
|
|
|
defaults: PluginSettings,
|
|
|
|
|
loaded: Partial<PluginSettings> | null,
|
|
|
|
|
): PluginSettings {
|
|
|
|
|
if (!loaded) return { ...defaults };
|
|
|
|
|
|
|
|
|
|
const result = { ...defaults, ...loaded };
|
|
|
|
|
|
|
|
|
|
// Deep merge statusColors
|
|
|
|
|
if (loaded.statusColors) {
|
|
|
|
|
result.statusColors = {
|
|
|
|
|
...defaults.statusColors,
|
|
|
|
|
...loaded.statusColors,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 15:30:17 +00:00
|
|
|
private async saveSettings() {
|
|
|
|
|
await this.plugin.saveData(this.settings);
|
|
|
|
|
}
|
2026-03-18 10:32:38 +00:00
|
|
|
|
2026-03-18 10:39:40 +00:00
|
|
|
async syncToExternalFile(force = false) {
|
|
|
|
|
if (!this.settings.enableExternalStatusSync && !force) return;
|
2026-03-18 10:32:38 +00:00
|
|
|
|
|
|
|
|
const path = normalizePath(this.settings.externalStatusSyncPath);
|
2026-03-18 10:45:54 +00:00
|
|
|
|
|
|
|
|
// Filter settings based on selected groups
|
|
|
|
|
const keysToSync = new Set<keyof PluginSettings>();
|
2026-03-18 11:35:49 +00:00
|
|
|
(this.settings.syncGroups || []).forEach((group) => {
|
|
|
|
|
const groupKeys = SETTINGS_GROUPS[group];
|
|
|
|
|
if (groupKeys) {
|
|
|
|
|
groupKeys.forEach((key) => keysToSync.add(key));
|
|
|
|
|
}
|
2026-03-18 10:45:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const filteredSettings: Partial<PluginSettings> = {};
|
|
|
|
|
keysToSync.forEach((key) => {
|
|
|
|
|
(filteredSettings as Record<string, unknown>)[key] =
|
|
|
|
|
this.settings[key];
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-18 10:32:38 +00:00
|
|
|
const data = {
|
2026-03-18 10:45:54 +00:00
|
|
|
...filteredSettings,
|
|
|
|
|
syncGroups: this.settings.syncGroups, // Always include meta-settings
|
2026-03-18 10:32:38 +00:00
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.plugin.app.vault.adapter.write(
|
|
|
|
|
path,
|
|
|
|
|
JSON.stringify(data, null, 2),
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to sync settings to external file:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:39:40 +00:00
|
|
|
async loadFromExternalFile(force = false) {
|
|
|
|
|
if (!this.settings.enableExternalStatusSync && !force) return;
|
2026-03-18 10:32:38 +00:00
|
|
|
const path = normalizePath(this.settings.externalStatusSyncPath);
|
|
|
|
|
if (!(await this.plugin.app.vault.adapter.exists(path))) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = await this.plugin.app.vault.adapter.read(path);
|
|
|
|
|
const data = JSON.parse(content);
|
|
|
|
|
|
2026-03-18 10:45:54 +00:00
|
|
|
// Determine which keys we should actually apply based on CURRENT settings
|
|
|
|
|
const allowedKeys = new Set<keyof PluginSettings>();
|
2026-03-18 11:35:49 +00:00
|
|
|
(this.settings.syncGroups || []).forEach((group) => {
|
|
|
|
|
const groupKeys = SETTINGS_GROUPS[group];
|
|
|
|
|
if (groupKeys) {
|
|
|
|
|
groupKeys.forEach((key) => allowedKeys.add(key));
|
|
|
|
|
}
|
2026-03-18 10:45:54 +00:00
|
|
|
});
|
|
|
|
|
|
2026-03-18 10:32:38 +00:00
|
|
|
let hasChanged = false;
|
|
|
|
|
const keys = Object.keys(data).filter(
|
2026-03-18 10:45:54 +00:00
|
|
|
(k) => k !== "updatedAt" && k !== "syncGroups",
|
2026-03-18 10:32:38 +00:00
|
|
|
) as Array<keyof PluginSettings>;
|
|
|
|
|
|
|
|
|
|
for (const key of keys) {
|
2026-03-18 10:45:54 +00:00
|
|
|
if (!(key in this.settings) || !allowedKeys.has(key)) continue;
|
2026-03-18 10:32:38 +00:00
|
|
|
|
|
|
|
|
const newValue = data[key];
|
|
|
|
|
const currentValue = this.settings[key];
|
|
|
|
|
|
|
|
|
|
// Basic deep equality check to avoid unnecessary updates
|
|
|
|
|
if (JSON.stringify(newValue) !== JSON.stringify(currentValue)) {
|
|
|
|
|
(this.settings as Record<string, unknown>)[key] = newValue;
|
|
|
|
|
hasChanged = true;
|
|
|
|
|
|
|
|
|
|
// Notify UI and other services
|
|
|
|
|
eventBus.publish("plugin-settings-changed", {
|
|
|
|
|
key: key as keyof PluginSettings,
|
|
|
|
|
value: newValue,
|
|
|
|
|
currentSettings: this.settings,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasChanged) {
|
|
|
|
|
await this.saveSettings();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load settings from external file:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupExternalFileWatcher() {
|
|
|
|
|
if (this.isWatcherRegistered) return;
|
|
|
|
|
|
|
|
|
|
this.plugin.registerEvent(
|
|
|
|
|
this.plugin.app.vault.on("modify", (file) => {
|
|
|
|
|
if (!this.settings.enableExternalStatusSync) return;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
file instanceof TFile &&
|
|
|
|
|
file.path ===
|
|
|
|
|
normalizePath(this.settings.externalStatusSyncPath)
|
|
|
|
|
) {
|
|
|
|
|
this.loadFromExternalFile().catch(console.error);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
this.isWatcherRegistered = true;
|
|
|
|
|
}
|
2025-07-15 15:30:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default new SettingsService();
|