mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: implement granular selective synchronization and non-Markdown status sync
- Expand sync categories to include templates, custom statuses, colors, UI, workflow, storage, and features - Add support for syncing non-Markdown status data to a vault-based JSON file - Update NonMarkdownStatusStore to handle vault-based storage and real-time updates - Enhance Synchronization UI with granular group selection and data sync options - Add safety checks to prevent TypeErrors during settings initialization and migration
This commit is contained in:
parent
e5c456b0aa
commit
f97bca5fa9
8 changed files with 331 additions and 148 deletions
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const SynchronizationSettings: React.FC<Props> = ({
|
|||
};
|
||||
|
||||
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<Props> = ({
|
|||
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<Props> = ({
|
|||
<div>
|
||||
<h3>Multi-device Synchronization</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<SettingItem
|
||||
name="Enable external 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="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" }}>
|
||||
<h4>Selective Synchronization</h4>
|
||||
<div className="sync-section" style={{ marginBottom: "30px" }}>
|
||||
<h4>Settings Synchronization</h4>
|
||||
<p className="setting-item-description">
|
||||
Choose which groups of settings should be included in the
|
||||
synchronization.
|
||||
Save plugin configuration to a JSON file.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "10px",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
|
||||
<SettingItem
|
||||
name="Enable settings synchronization"
|
||||
description="Automatically save and load selected plugin settings from a file in your vault."
|
||||
>
|
||||
{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>
|
||||
<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
|
||||
className="setting-item-description"
|
||||
style={{ marginTop: "4px", fontSize: "0.85em" }}
|
||||
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)}
|
||||
>
|
||||
{group.description}
|
||||
<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>
|
||||
|
||||
<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="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 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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -57,5 +57,15 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
writeMappedTagsToDefault: false,
|
||||
enableExternalStatusSync: false,
|
||||
externalStatusSyncPath: "_note-status-sync.json",
|
||||
syncGroups: ["statuses", "appearance", "behavior", "features"],
|
||||
syncGroups: [
|
||||
"templates",
|
||||
"customStatuses",
|
||||
"statusColors",
|
||||
"uiAppearance",
|
||||
"workflow",
|
||||
"storage",
|
||||
"features",
|
||||
],
|
||||
enableNonMarkdownSync: false,
|
||||
nonMarkdownSyncPath: "_note-status-data.json",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,20 +4,13 @@ import { Plugin, TFile, normalizePath } from "obsidian";
|
|||
import eventBus from "./eventBus";
|
||||
|
||||
export const SETTINGS_GROUPS: Record<SyncGroup, (keyof PluginSettings)[]> = {
|
||||
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<SyncGroup, (keyof PluginSettings)[]> = {
|
|||
"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<keyof PluginSettings>();
|
||||
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<PluginSettings> = {};
|
||||
|
|
@ -177,8 +179,11 @@ class SettingsService {
|
|||
|
||||
// Determine which keys we should actually apply based on CURRENT settings
|
||||
const allowedKeys = new Set<keyof PluginSettings>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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,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<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 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<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 +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<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) => {
|
||||
|
|
|
|||
2
main.tsx
2
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([
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue