Merge pull request #90 from devonthesofa/feature/syncronize-settings

feat: implement multi-device synchronization for plugin settings
This commit is contained in:
Aleix Soler 2026-03-18 12:48:05 +01:00 committed by GitHub
commit 07c8b8d4df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 684 additions and 40 deletions

View file

@ -16,7 +16,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
onChange,
}) => {
const addNewCustomStatus = () => {
const currentStatuses = [...settings.customStatuses];
const currentStatuses = [...(settings.customStatuses || [])];
currentStatuses.push({
name: "",
icon: "",
@ -27,7 +27,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
const removeCustomStatus: CustomStatusItemProps["onCustomStatusRemove"] = (
index,
) => {
const currentStatuses = [...settings.customStatuses];
const currentStatuses = [...(settings.customStatuses || [])];
currentStatuses.splice(index, 1);
onChange("customStatuses", currentStatuses);
};
@ -37,7 +37,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
column,
value,
) => {
const currentStatuses = [...settings.customStatuses];
const currentStatuses = [...(settings.customStatuses || [])];
const target = currentStatuses[index];
if (target) {
@ -48,7 +48,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
const moveCustomStatusUp = (index: number) => {
if (index <= 0) return;
const currentStatuses = [...settings.customStatuses];
const currentStatuses = [...(settings.customStatuses || [])];
[currentStatuses[index - 1], currentStatuses[index]] = [
currentStatuses[index],
currentStatuses[index - 1],
@ -57,8 +57,8 @@ export const CustomStatusSettings: React.FC<Props> = ({
};
const moveCustomStatusDown = (index: number) => {
if (index >= settings.customStatuses.length - 1) return;
const currentStatuses = [...settings.customStatuses];
const currentStatuses = [...(settings.customStatuses || [])];
if (index >= currentStatuses.length - 1) return;
[currentStatuses[index], currentStatuses[index + 1]] = [
currentStatuses[index + 1],
currentStatuses[index],
@ -89,7 +89,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
vertical
>
<div className="custom-status-list">
{settings.customStatuses.length === 0 ? (
{(settings.customStatuses || []).length === 0 ? (
<div className="custom-status-list__empty">
<p>
No custom statuses yet. Click "Add Status" below
@ -97,7 +97,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
</p>
</div>
) : (
settings.customStatuses.map((status, index) => (
(settings.customStatuses || []).map((status, index) => (
<CustomStatusItem
key={index}
status={status}
@ -108,7 +108,8 @@ export const CustomStatusSettings: React.FC<Props> = ({
onMoveDown={moveCustomStatusDown}
canMoveUp={index > 0}
canMoveDown={
index < settings.customStatuses.length - 1
index <
(settings.customStatuses || []).length - 1
}
/>
))

View file

@ -0,0 +1,271 @@
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
import React from "react";
import { SettingItem } from "./SettingItem";
import settingsService from "@/core/settingsService";
import statusStoreManager from "@/core/statusStoreManager";
export type Props = {
settings: PluginSettings;
onChange: (key: keyof PluginSettings, value: unknown) => void;
};
export const SynchronizationSettings: React.FC<Props> = ({
settings,
onChange,
}) => {
const handleExport = async () => {
await settingsService.syncToExternalFile(true);
alert("Plugin settings exported successfully.");
};
const handleImport = async () => {
await settingsService.loadFromExternalFile(true);
alert("Plugin settings imported successfully.");
};
const handleDataExport = async () => {
try {
await statusStoreManager.getNonMarkdownStore().exportToVault();
alert("Non-Markdown status data exported to vault.");
} catch (e) {
alert(`Export failed: ${e.message}`);
}
};
const handleDataImport = async () => {
try {
await statusStoreManager.getNonMarkdownStore().importFromVault();
alert("Non-Markdown status data imported from vault.");
} catch (e) {
alert(`Import failed: ${e.message}`);
}
};
const toggleGroup = (group: SyncGroup) => {
const currentGroups = [...(settings.syncGroups || [])];
const index = currentGroups.indexOf(group);
if (index === -1) {
currentGroups.push(group);
} else {
currentGroups.splice(index, 1);
}
onChange("syncGroups", currentGroups);
};
const syncGroups: { id: SyncGroup; label: string; description: string }[] =
[
{
id: "templates",
label: "Templates",
description: "Status templates and their enabled status.",
},
{
id: "customStatuses",
label: "Custom Statuses",
description: "Standalone custom statuses.",
},
{
id: "statusColors",
label: "Status Colors",
description: "Custom colors for all statuses.",
},
{
id: "uiAppearance",
label: "UI & Appearance",
description: "Icons, frames, status bar, and toolbar styles.",
},
{
id: "workflow",
label: "Workflow Rules",
description: "Multi-status, strict mode, and overview popups.",
},
{
id: "storage",
label: "Storage & Tagging",
description: "Frontmatter keys, mappings, and tag prefix.",
},
{
id: "features",
label: "Features & Limits",
description: "Quick commands, experiments, and vault limits.",
},
];
return (
<div>
<h3>Multi-device Synchronization</h3>
<p>
Keep your plugin settings and data in sync across devices using
files in your vault.
</p>
<div className="sync-section" style={{ marginBottom: "30px" }}>
<h4>Settings Synchronization</h4>
<p className="setting-item-description">
Save plugin configuration to a JSON file.
</p>
<SettingItem
name="Enable settings synchronization"
description="Automatically save and load selected plugin settings from a file in your vault."
>
<input
type="checkbox"
checked={settings.enableExternalStatusSync || false}
onChange={(e) =>
onChange(
"enableExternalStatusSync",
e.target.checked,
)
}
/>
</SettingItem>
<SettingItem
name="Settings sync file path"
description="Path to the JSON file where settings will be stored (relative to vault root)."
>
<input
type="text"
value={
settings.externalStatusSyncPath ||
"_note-status-sync.json"
}
onChange={(e) =>
onChange("externalStatusSyncPath", e.target.value)
}
placeholder="_note-status-sync.json"
/>
</SettingItem>
<div
className="sync-groups-container"
style={{ margin: "20px 0" }}
>
<h5>Selective Synchronization</h5>
<p className="setting-item-description">
Choose which groups of settings should be included in
the synchronization.
</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "10px",
marginTop: "10px",
}}
>
{syncGroups.map((group) => (
<div
key={group.id}
className={`selectable-group-item ${
(settings.syncGroups || []).includes(
group.id,
)
? "is-selected"
: ""
}`}
style={{
padding: "10px",
border: "1px solid var(--background-modifier-border)",
borderRadius: "4px",
cursor: "pointer",
backgroundColor: (
settings.syncGroups || []
).includes(group.id)
? "var(--background-modifier-hover)"
: "transparent",
}}
onClick={() => toggleGroup(group.id)}
>
<div style={{ fontWeight: "bold" }}>
<input
type="checkbox"
checked={(
settings.syncGroups || []
).includes(group.id)}
onChange={() => {}} // Handled by parent div onClick
style={{ marginRight: "8px" }}
/>
{group.label}
</div>
<div
className="setting-item-description"
style={{
marginTop: "4px",
fontSize: "0.85em",
}}
>
{group.description}
</div>
</div>
))}
</div>
</div>
<div
className="note-status-settings__actions"
style={{ marginTop: "1em", display: "flex", gap: "10px" }}
>
<button onClick={handleExport}>
📤 Export selected now
</button>
<button onClick={handleImport}>
📥 Import selected now
</button>
</div>
</div>
<div className="sync-section">
<h4>Non-Markdown Data Synchronization</h4>
<p className="setting-item-description">
Statuses for non-Markdown files (PDFs, images, etc.) are
stored in a special data file. Enable this to keep them in
sync across devices.
</p>
<SettingItem
name="Sync non-Markdown statuses"
description="Store non-Markdown status data in a vault file instead of the internal plugin folder."
>
<input
type="checkbox"
checked={settings.enableNonMarkdownSync || false}
onChange={(e) =>
onChange("enableNonMarkdownSync", e.target.checked)
}
/>
</SettingItem>
<SettingItem
name="Data sync file path"
description="Path to the JSON file where non-Markdown statuses will be stored."
>
<input
type="text"
value={
settings.nonMarkdownSyncPath ||
"_note-status-data.json"
}
onChange={(e) =>
onChange("nonMarkdownSyncPath", e.target.value)
}
placeholder="_note-status-data.json"
/>
</SettingItem>
<div
className="note-status-settings__actions"
style={{ marginTop: "1em", display: "flex", gap: "10px" }}
>
<button onClick={handleDataExport}>
📤 Export internal data to vault
</button>
<button onClick={handleDataImport}>
📥 Import data from vault
</button>
</div>
</div>
</div>
);
};

View file

@ -27,7 +27,7 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
const handleTemplateToggle = useCallback(
(templateId: string, enabled: boolean) => {
let newEnabledTemplates = [...settings.enabledTemplates];
let newEnabledTemplates = [...(settings.enabledTemplates || [])];
if (enabled) {
if (!newEnabledTemplates.includes(templateId)) {
newEnabledTemplates.push(templateId);
@ -176,13 +176,13 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
</button>
</div>
<div className="template-list">
{settings.templates.map((template) => (
{(settings.templates || []).map((template) => (
<TemplateItem
key={template.id}
template={template}
isEnabled={settings.enabledTemplates.includes(
template.id,
)}
isEnabled={(
settings.enabledTemplates || []
).includes(template.id)}
onToggle={handleTemplateToggle}
onEdit={handleEditTemplate}
onDelete={handleDeleteTemplate}

View file

@ -4,6 +4,7 @@ import { TemplateSettings } from "./TemplateSettings";
import { BehaviourSettings } from "./BehaviourSettings";
import { CustomStatusSettings } from "./CustomStatusSettings";
import { QuickCommandsSettings } from "./QuickCommandsSettings";
import { SynchronizationSettings } from "./SynchronizationSettings";
import {
EditorToolbarSettings,
ExperimentalSettings,
@ -25,6 +26,7 @@ type SectionId =
| "file-explorer"
| "unknown-appearance"
| "behavior-storage"
| "synchronization"
| "experimental-features";
type SectionDefinition = {
@ -137,6 +139,18 @@ const SettingsUI: React.FC<Props> = ({ settings, onChange }) => {
/>
),
},
{
id: "synchronization",
label: "Synchronization",
description:
"Keep custom statuses and templates in sync across multiple devices.",
content: (
<SynchronizationSettings
settings={localSettings}
onChange={handleChange}
/>
),
},
{
id: "experimental-features",
label: "Experimental Features",

View file

@ -55,4 +55,17 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
applyStatusRecursivelyToSubfolders: false,
statusFrontmatterMappings: [],
writeMappedTagsToDefault: false,
enableExternalStatusSync: false,
externalStatusSyncPath: "_note-status-sync.json",
syncGroups: [
"templates",
"customStatuses",
"statusColors",
"uiAppearance",
"workflow",
"storage",
"features",
],
enableNonMarkdownSync: false,
nonMarkdownSyncPath: "_note-status-data.json",
};

View file

@ -1,17 +1,75 @@
import { DEFAULT_PLUGIN_SETTINGS } from "@/constants/defaultSettings";
import { PluginSettings } from "@/types/pluginSettings";
import { Plugin } from "obsidian";
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
import { Plugin, TFile, normalizePath } from "obsidian";
import eventBus from "./eventBus";
export const SETTINGS_GROUPS: Record<SyncGroup, (keyof PluginSettings)[]> = {
templates: ["templates", "enabledTemplates"],
customStatuses: ["customStatuses"],
statusColors: ["statusColors"],
uiAppearance: [
"fileExplorerIconPosition",
"fileExplorerIconFrame",
"fileExplorerIconColorMode",
"showStatusBar",
"autoHideStatusBar",
"statusBarShowTemplateName",
"statusBarBadgeStyle",
"statusBarBadgeContentMode",
"showStatusIconsInExplorer",
"hideUnknownStatusInExplorer",
"fileExplorerColorFileName",
"fileExplorerColorBlock",
"fileExplorerLeftBorder",
"fileExplorerStatusDot",
"fileExplorerUnderlineFileName",
"unknownStatusIcon",
"unknownStatusLucideIcon",
"unknownStatusColor",
"statusBarNoStatusText",
"statusBarShowNoStatusIcon",
"statusBarShowNoStatusText",
"showEditorToolbarButton",
"editorToolbarButtonPosition",
"editorToolbarButtonDisplay",
],
workflow: [
"useCustomStatusesOnly",
"useMultipleStatuses",
"singleStatusStorageMode",
"strictStatuses",
"enableStatusOverviewPopup",
],
storage: [
"tagPrefix",
"statusFrontmatterMappings",
"writeMappedTagsToDefault",
"applyStatusRecursivelyToSubfolders",
],
features: [
"quickStatusCommands",
"enableExperimentalFeatures",
"enableStatusDashboard",
"enableGroupedStatusView",
"vaultSizeLimit",
],
};
class SettingsService {
private plugin: Plugin;
public settings: PluginSettings;
private isWatcherRegistered = false;
constructor() {}
async initialize(plugin: Plugin): Promise<void> {
this.plugin = plugin;
await this.loadSettings();
if (this.settings.enableExternalStatusSync) {
await this.loadFromExternalFile();
this.setupExternalFileWatcher();
}
}
setValue(key: keyof PluginSettings, value: unknown) {
@ -21,6 +79,19 @@ class SettingsService {
// const oldValue = this.settings[key]; // TODO: Send the old value
this.settings[key] = value;
this.saveSettings().catch(console.error);
// Handle external sync
if (this.settings.enableExternalStatusSync) {
this.syncToExternalFile().catch(console.error);
}
if (key === "enableExternalStatusSync") {
if (value) {
this.syncToExternalFile().catch(console.error);
this.setupExternalFileWatcher();
}
}
// INFO: Send the propgation event
eventBus.publish("plugin-settings-changed", {
key,
@ -30,17 +101,142 @@ class SettingsService {
}
private async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_PLUGIN_SETTINGS,
await this.plugin.loadData(),
);
const loadedData = await this.plugin.loadData();
this.settings = this.mergeSettings(DEFAULT_PLUGIN_SETTINGS, loadedData);
return this.settings;
}
/**
* 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;
}
private async saveSettings() {
await this.plugin.saveData(this.settings);
}
async syncToExternalFile(force = false) {
if (!this.settings.enableExternalStatusSync && !force) return;
const path = normalizePath(this.settings.externalStatusSyncPath);
// Filter settings based on selected groups
const keysToSync = new Set<keyof PluginSettings>();
(this.settings.syncGroups || []).forEach((group) => {
const groupKeys = SETTINGS_GROUPS[group];
if (groupKeys) {
groupKeys.forEach((key) => keysToSync.add(key));
}
});
const filteredSettings: Partial<PluginSettings> = {};
keysToSync.forEach((key) => {
(filteredSettings as Record<string, unknown>)[key] =
this.settings[key];
});
const data = {
...filteredSettings,
syncGroups: this.settings.syncGroups, // Always include meta-settings
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);
}
}
async loadFromExternalFile(force = false) {
if (!this.settings.enableExternalStatusSync && !force) return;
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);
// Determine which keys we should actually apply based on CURRENT settings
const allowedKeys = new Set<keyof PluginSettings>();
(this.settings.syncGroups || []).forEach((group) => {
const groupKeys = SETTINGS_GROUPS[group];
if (groupKeys) {
groupKeys.forEach((key) => allowedKeys.add(key));
}
});
let hasChanged = false;
const keys = Object.keys(data).filter(
(k) => k !== "updatedAt" && k !== "syncGroups",
) as Array<keyof PluginSettings>;
for (const key of keys) {
if (!(key in this.settings) || !allowedKeys.has(key)) continue;
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;
}
}
export default new SettingsService();

View file

@ -5,14 +5,22 @@ import { StatusStore } from "./statusStores/types";
class StatusStoreManager {
private stores: StatusStore[] = [];
private nonMarkdownStore: NonMarkdownStatusStore | null = null;
async initialize(plugin: Plugin) {
const frontmatterStore = new FrontmatterStatusStore(plugin.app);
this.stores = [frontmatterStore];
const nonMarkdownStore = new NonMarkdownStatusStore(plugin);
await nonMarkdownStore.initialize();
this.registerStore(nonMarkdownStore);
this.nonMarkdownStore = new NonMarkdownStatusStore(plugin);
await this.nonMarkdownStore.initialize();
this.registerStore(this.nonMarkdownStore);
}
getNonMarkdownStore(): NonMarkdownStatusStore {
if (!this.nonMarkdownStore) {
throw new Error("NonMarkdownStatusStore is not initialized");
}
return this.nonMarkdownStore;
}
registerStore(store: StatusStore) {

View file

@ -1,5 +1,6 @@
import { normalizePath, Plugin, TAbstractFile, TFile } from "obsidian";
import eventBus from "@/core/eventBus";
import settingsService from "@/core/settingsService";
import { StatusMutation, StatusStore } from "./types";
type FileStatusMap = Record<string, Record<string, string[]>>;
@ -11,19 +12,103 @@ type PersistedData = {
export class NonMarkdownStatusStore implements StatusStore {
private data: FileStatusMap = {};
private readonly dataPath: string;
private isWatcherRegistered = false;
constructor(private readonly plugin: Plugin) {
const configDir = this.plugin.app.vault.configDir;
const pluginDir = `${configDir}/plugins/${this.plugin.manifest.id}`;
this.dataPath = normalizePath(
`${pluginDir}/non-markdown-statuses.json`,
);
}
constructor(private readonly plugin: Plugin) {}
async initialize(): Promise<void> {
this.data = await this.loadFromDisk();
this.registerVaultEvents();
this.setupSyncWatcher();
// Reload if settings change
eventBus.subscribe(
"plugin-settings-changed",
({ key }) => {
if (
key === "enableNonMarkdownSync" ||
key === "nonMarkdownSyncPath"
) {
this.loadFromDisk()
.then((newData) => {
this.data = newData;
this.setupSyncWatcher();
// Notify UI that statuses might have changed
this.plugin.app.workspace.iterateAllLeaves(
(leaf) => {
const view = leaf.view as {
requestRefresh?: () => void;
};
if (view.requestRefresh)
view.requestRefresh();
},
);
})
.catch(console.error);
}
},
"non-markdown-status-store-sync-subscriptor",
);
}
private getDataPath(): string {
const settings = settingsService.settings;
if (
settings &&
settings.enableNonMarkdownSync &&
settings.nonMarkdownSyncPath
) {
return this.getVaultPath();
}
return this.getInternalPath();
}
private getInternalPath(): string {
const configDir = this.plugin.app.vault.configDir;
const pluginDir = `${configDir}/plugins/${this.plugin.manifest.id}`;
return normalizePath(`${pluginDir}/non-markdown-statuses.json`);
}
private getVaultPath(): string {
const settings = settingsService.settings;
return normalizePath(settings.nonMarkdownSyncPath);
}
async exportToVault(): Promise<void> {
const internalPath = this.getInternalPath();
const vaultPath = this.getVaultPath();
if (!(await this.plugin.app.vault.adapter.exists(internalPath))) {
throw new Error("No internal data found to export.");
}
const raw = await this.plugin.app.vault.adapter.read(internalPath);
await this.ensureDirectoryExists(vaultPath);
await this.plugin.app.vault.adapter.write(vaultPath, raw);
// Reload if vault sync is active
if (settingsService.settings.enableNonMarkdownSync) {
this.data = await this.loadFromDisk();
}
}
async importFromVault(): Promise<void> {
const internalPath = this.getInternalPath();
const vaultPath = this.getVaultPath();
if (!(await this.plugin.app.vault.adapter.exists(vaultPath))) {
throw new Error("No vault sync file found to import.");
}
const raw = await this.plugin.app.vault.adapter.read(vaultPath);
await this.ensureDirectoryExists(internalPath);
await this.plugin.app.vault.adapter.write(internalPath, raw);
// Reload if vault sync is inactive
if (!settingsService.settings.enableNonMarkdownSync) {
this.data = await this.loadFromDisk();
}
}
canHandle(file: TFile): boolean {
@ -70,14 +155,18 @@ export class NonMarkdownStatusStore implements StatusStore {
}
private async loadFromDisk(): Promise<FileStatusMap> {
const path = this.getDataPath();
try {
const raw = await this.plugin.app.vault.adapter.read(this.dataPath);
const exists = await this.plugin.app.vault.adapter.exists(path);
if (!exists) return {};
const raw = await this.plugin.app.vault.adapter.read(path);
const parsed = JSON.parse(raw) as PersistedData;
if (parsed && typeof parsed === "object" && parsed.files) {
return parsed.files;
}
} catch {
// File may not exist yet or be malformed; start with empty dataset.
} catch (e) {
console.error("Failed to load non-markdown statuses:", e);
}
return {};
}
@ -87,20 +176,58 @@ export class NonMarkdownStatusStore implements StatusStore {
version: 1,
files: this.data,
};
await this.ensureDirectoryExists();
const path = this.getDataPath();
await this.ensureDirectoryExists(path);
await this.plugin.app.vault.adapter.write(
this.dataPath,
path,
JSON.stringify(payload, null, 2),
);
}
private async ensureDirectoryExists(): Promise<void> {
const dir = this.dataPath.split("/").slice(0, -1).join("/");
private async ensureDirectoryExists(path: string): Promise<void> {
const dir = path.split("/").slice(0, -1).join("/");
if (!dir) return;
if (!(await this.plugin.app.vault.adapter.exists(dir))) {
await this.plugin.app.vault.adapter.mkdir(dir);
}
}
private setupSyncWatcher() {
if (this.isWatcherRegistered) return;
this.plugin.registerEvent(
this.plugin.app.vault.on("modify", (file) => {
const settings = settingsService.settings;
if (!settings.enableNonMarkdownSync) return;
if (
file instanceof TFile &&
file.path === normalizePath(settings.nonMarkdownSyncPath)
) {
this.loadFromDisk()
.then((newData) => {
this.data = newData;
// Emit events for all files that might have changed
// This is a bit heavy, but non-markdown files are fewer
Object.keys(this.data).forEach((filePath) => {
const f =
this.plugin.app.vault.getAbstractFileByPath(
filePath,
);
if (f instanceof TFile) {
eventBus.publish("status-changed", {
file: f,
});
}
});
})
.catch(console.error);
}
}),
);
this.isWatcherRegistered = true;
}
private registerVaultEvents() {
this.plugin.registerEvent(
this.plugin.app.vault.on("rename", (file, oldPath) => {

View file

@ -32,8 +32,8 @@ export default class NoteStatusPlugin extends Plugin {
async onload() {
BaseNoteStatusService.initialize(this.app);
await statusStoreManager.initialize(this);
await this.loadPluginSettings();
await statusStoreManager.initialize(this);
// INFO: initialize all integrations
Promise.all([

View file

@ -22,6 +22,15 @@ export interface StatusTemplate {
statuses: NoteStatus[];
}
export type SyncGroup =
| "templates"
| "customStatuses"
| "statusColors"
| "uiAppearance"
| "workflow"
| "storage"
| "features";
export type PluginSettings = {
fileExplorerIconPosition:
| "absolute-right"
@ -70,5 +79,10 @@ export type PluginSettings = {
applyStatusRecursivelyToSubfolders: boolean; // Whether to show recursive folder context option
statusFrontmatterMappings: StatusFrontmatterMapping[]; // Custom mappings between templates/statuses and frontmatter keys
writeMappedTagsToDefault: boolean; // Whether mapped tags should also write to the default tag
enableExternalStatusSync: boolean; // Whether to sync statuses to an external file
externalStatusSyncPath: string; // Path to the external sync file
syncGroups: SyncGroup[]; // Selected groups of settings to synchronize
enableNonMarkdownSync: boolean; // Whether to sync non-markdown statuses to a vault file
nonMarkdownSyncPath: string; // Path to the non-markdown sync file
[key: string]: unknown;
};