diff --git a/components/SettingsUI/CustomStatusSettings.tsx b/components/SettingsUI/CustomStatusSettings.tsx index 3c38696..1ca8a1c 100644 --- a/components/SettingsUI/CustomStatusSettings.tsx +++ b/components/SettingsUI/CustomStatusSettings.tsx @@ -16,7 +16,7 @@ export const CustomStatusSettings: React.FC = ({ onChange, }) => { const addNewCustomStatus = () => { - const currentStatuses = [...settings.customStatuses]; + const currentStatuses = [...(settings.customStatuses || [])]; currentStatuses.push({ name: "", icon: "", @@ -27,7 +27,7 @@ export const CustomStatusSettings: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ }; 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 = ({ vertical >
- {settings.customStatuses.length === 0 ? ( + {(settings.customStatuses || []).length === 0 ? (

No custom statuses yet. Click "Add Status" below @@ -97,7 +97,7 @@ export const CustomStatusSettings: React.FC = ({

) : ( - settings.customStatuses.map((status, index) => ( + (settings.customStatuses || []).map((status, index) => ( = ({ onMoveDown={moveCustomStatusDown} canMoveUp={index > 0} canMoveDown={ - index < settings.customStatuses.length - 1 + index < + (settings.customStatuses || []).length - 1 } /> )) diff --git a/components/SettingsUI/SynchronizationSettings.tsx b/components/SettingsUI/SynchronizationSettings.tsx index fb300b3..52f8528 100644 --- a/components/SettingsUI/SynchronizationSettings.tsx +++ b/components/SettingsUI/SynchronizationSettings.tsx @@ -23,7 +23,7 @@ export const SynchronizationSettings: React.FC = ({ }; const toggleGroup = (group: SyncGroup) => { - const currentGroups = [...settings.syncGroups]; + const currentGroups = [...(settings.syncGroups || [])]; const index = currentGroups.indexOf(group); if (index === -1) { currentGroups.push(group); @@ -36,24 +36,39 @@ export const SynchronizationSettings: React.FC = ({ const syncGroups: { id: SyncGroup; label: string; description: string }[] = [ { - id: "statuses", - label: "Statuses & Templates", - description: "Custom statuses, templates, and workflow rules.", + id: "templates", + label: "Templates", + description: "Status templates and their enabled status.", }, { - id: "appearance", - label: "Appearance", - description: "Colors, icons, and UI element styles.", + id: "customStatuses", + label: "Custom Statuses", + description: "Standalone custom statuses.", }, { - id: "behavior", - label: "Behavior & Storage", - description: "Frontmatter keys, mappings, and general logic.", + 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: "Commands & Features", - description: "Quick commands and experimental feature toggles.", + label: "Features & Limits", + description: "Quick commands, experiments, and vault limits.", }, ]; @@ -61,103 +76,164 @@ export const SynchronizationSettings: React.FC = ({

Multi-device Synchronization

- Keep your plugin settings in sync across devices using a file in - your vault. + Keep your plugin settings and data in sync across devices using + files in your vault.

- - - onChange("enableExternalStatusSync", e.target.checked) - } - /> - - - - - onChange("externalStatusSyncPath", e.target.value) - } - placeholder="_note-status-sync.json" - /> - - -
-

Selective Synchronization

+
+

Settings Synchronization

- Choose which groups of settings should be included in the - synchronization. + Save plugin configuration to a JSON file.

-
- {syncGroups.map((group) => ( -
toggleGroup(group.id)} - > -
- {}} // Handled by parent div onClick - style={{ marginRight: "8px" }} - /> - {group.label} -
+ + onChange( + "enableExternalStatusSync", + e.target.checked, + ) + } + /> + + + + + onChange("externalStatusSyncPath", e.target.value) + } + placeholder="_note-status-sync.json" + /> + + +
+
Selective Synchronization
+

+ Choose which groups of settings should be included in + the synchronization. +

+
+ {syncGroups.map((group) => (
toggleGroup(group.id)} > - {group.description} +
+ {}} // Handled by parent div onClick + style={{ marginRight: "8px" }} + /> + {group.label} +
+
+ {group.description} +
-
- ))} + ))} +
+
+ +
+ +
-
- - +
+

Non-Markdown Data Synchronization

+

+ Statuses for non-Markdown files (PDFs, images, etc.) are + stored in a special data file. Enable this to keep them in + sync across devices. +

+ + + + onChange("enableNonMarkdownSync", e.target.checked) + } + /> + + + + + onChange("nonMarkdownSyncPath", e.target.value) + } + placeholder="_note-status-data.json" + /> +
); diff --git a/components/SettingsUI/TemplateSettings.tsx b/components/SettingsUI/TemplateSettings.tsx index 4d9788d..99bd9a8 100644 --- a/components/SettingsUI/TemplateSettings.tsx +++ b/components/SettingsUI/TemplateSettings.tsx @@ -27,7 +27,7 @@ export const TemplateSettings: React.FC = ({ 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 = ({
- {settings.templates.map((template) => ( + {(settings.templates || []).map((template) => ( = { - statuses: [ - "templates", - "customStatuses", - "enabledTemplates", - "useCustomStatusesOnly", - "useMultipleStatuses", - "singleStatusStorageMode", - "strictStatuses", - ], - appearance: [ + templates: ["templates", "enabledTemplates"], + customStatuses: ["customStatuses"], + statusColors: ["statusColors"], + uiAppearance: [ "fileExplorerIconPosition", "fileExplorerIconFrame", "fileExplorerIconColorMode", - "statusColors", "showStatusBar", "autoHideStatusBar", "statusBarShowTemplateName", @@ -40,19 +33,25 @@ export const SETTINGS_GROUPS: Record = { "editorToolbarButtonPosition", "editorToolbarButtonDisplay", ], - behavior: [ + workflow: [ + "useCustomStatusesOnly", + "useMultipleStatuses", + "singleStatusStorageMode", + "strictStatuses", + "enableStatusOverviewPopup", + ], + storage: [ "tagPrefix", "statusFrontmatterMappings", "writeMappedTagsToDefault", "applyStatusRecursivelyToSubfolders", - "vaultSizeLimit", - "enableStatusOverviewPopup", ], features: [ "quickStatusCommands", "enableExperimentalFeatures", "enableStatusDashboard", "enableGroupedStatusView", + "vaultSizeLimit", ], }; @@ -140,8 +139,11 @@ class SettingsService { // Filter settings based on selected groups const keysToSync = new Set(); - this.settings.syncGroups.forEach((group) => { - SETTINGS_GROUPS[group].forEach((key) => keysToSync.add(key)); + (this.settings.syncGroups || []).forEach((group) => { + const groupKeys = SETTINGS_GROUPS[group]; + if (groupKeys) { + groupKeys.forEach((key) => keysToSync.add(key)); + } }); const filteredSettings: Partial = {}; @@ -177,8 +179,11 @@ class SettingsService { // Determine which keys we should actually apply based on CURRENT settings const allowedKeys = new Set(); - this.settings.syncGroups.forEach((group) => { - SETTINGS_GROUPS[group].forEach((key) => allowedKeys.add(key)); + (this.settings.syncGroups || []).forEach((group) => { + const groupKeys = SETTINGS_GROUPS[group]; + if (groupKeys) { + groupKeys.forEach((key) => allowedKeys.add(key)); + } }); let hasChanged = false; diff --git a/core/statusStores/nonMarkdownStatusStore.ts b/core/statusStores/nonMarkdownStatusStore.ts index 7c01531..81661fb 100644 --- a/core/statusStores/nonMarkdownStatusStore.ts +++ b/core/statusStores/nonMarkdownStatusStore.ts @@ -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>; @@ -11,19 +12,58 @@ 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 { 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 normalizePath(settings.nonMarkdownSyncPath); + } + + const configDir = this.plugin.app.vault.configDir; + const pluginDir = `${configDir}/plugins/${this.plugin.manifest.id}`; + return normalizePath(`${pluginDir}/non-markdown-statuses.json`); } canHandle(file: TFile): boolean { @@ -70,14 +110,18 @@ export class NonMarkdownStatusStore implements StatusStore { } private async loadFromDisk(): Promise { + 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 +131,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 { - const dir = this.dataPath.split("/").slice(0, -1).join("/"); + private async ensureDirectoryExists(path: string): Promise { + 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) => { diff --git a/main.tsx b/main.tsx index 2ea5ed9..aab14ed 100644 --- a/main.tsx +++ b/main.tsx @@ -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([ diff --git a/types/pluginSettings.ts b/types/pluginSettings.ts index fc25208..619552e 100644 --- a/types/pluginSettings.ts +++ b/types/pluginSettings.ts @@ -22,7 +22,14 @@ export interface StatusTemplate { statuses: NoteStatus[]; } -export type SyncGroup = "statuses" | "appearance" | "behavior" | "features"; +export type SyncGroup = + | "templates" + | "customStatuses" + | "statusColors" + | "uiAppearance" + | "workflow" + | "storage" + | "features"; export type PluginSettings = { fileExplorerIconPosition: @@ -75,5 +82,7 @@ export type PluginSettings = { 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; };