feat: implement selective settings synchronization

This commit is contained in:
Aleix Soler 2026-03-18 11:45:54 +01:00
parent 403c4113e4
commit e5c456b0aa
4 changed files with 179 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import { PluginSettings } from "@/types/pluginSettings";
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
import React from "react";
import { SettingItem } from "./SettingItem";
import settingsService from "@/core/settingsService";
@ -22,18 +22,52 @@ export const SynchronizationSettings: React.FC<Props> = ({
alert("Plugin settings imported successfully.");
};
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: "statuses",
label: "Statuses & Templates",
description: "Custom statuses, templates, and workflow rules.",
},
{
id: "appearance",
label: "Appearance",
description: "Colors, icons, and UI element styles.",
},
{
id: "behavior",
label: "Behavior & Storage",
description: "Frontmatter keys, mappings, and general logic.",
},
{
id: "features",
label: "Commands & Features",
description: "Quick commands and experimental feature toggles.",
},
];
return (
<div>
<h3>Multi-device Synchronization</h3>
<p>
Keep all your plugin settings (statuses, templates, UI
preferences, etc.) in sync across devices using a file in your
vault.
Keep your plugin settings in sync across devices using a file in
your vault.
</p>
<SettingItem
name="Enable external synchronization"
description="Automatically save and load all plugin settings from a file in your vault."
description="Automatically save and load selected plugin settings from a file in your vault."
>
<input
type="checkbox"
@ -61,12 +95,69 @@ export const SynchronizationSettings: React.FC<Props> = ({
/>
</SettingItem>
<div className="sync-groups-container" style={{ margin: "20px 0" }}>
<h4>Selective Synchronization</h4>
<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 to file now</button>
<button onClick={handleImport}>📥 Import from file now</button>
<button onClick={handleExport}>📤 Export selected now</button>
<button onClick={handleImport}>📥 Import selected now</button>
</div>
</div>
);

View file

@ -57,4 +57,5 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
writeMappedTagsToDefault: false,
enableExternalStatusSync: false,
externalStatusSyncPath: "_note-status-sync.json",
syncGroups: ["statuses", "appearance", "behavior", "features"],
};

View file

@ -1,8 +1,61 @@
import { DEFAULT_PLUGIN_SETTINGS } from "@/constants/defaultSettings";
import { PluginSettings } from "@/types/pluginSettings";
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
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: [
"fileExplorerIconPosition",
"fileExplorerIconFrame",
"fileExplorerIconColorMode",
"statusColors",
"showStatusBar",
"autoHideStatusBar",
"statusBarShowTemplateName",
"statusBarBadgeStyle",
"statusBarBadgeContentMode",
"showStatusIconsInExplorer",
"hideUnknownStatusInExplorer",
"fileExplorerColorFileName",
"fileExplorerColorBlock",
"fileExplorerLeftBorder",
"fileExplorerStatusDot",
"fileExplorerUnderlineFileName",
"unknownStatusIcon",
"unknownStatusLucideIcon",
"unknownStatusColor",
"statusBarNoStatusText",
"statusBarShowNoStatusIcon",
"statusBarShowNoStatusText",
"showEditorToolbarButton",
"editorToolbarButtonPosition",
"editorToolbarButtonDisplay",
],
behavior: [
"tagPrefix",
"statusFrontmatterMappings",
"writeMappedTagsToDefault",
"applyStatusRecursivelyToSubfolders",
"vaultSizeLimit",
"enableStatusOverviewPopup",
],
features: [
"quickStatusCommands",
"enableExperimentalFeatures",
"enableStatusDashboard",
"enableGroupedStatusView",
],
};
class SettingsService {
private plugin: Plugin;
public settings: PluginSettings;
@ -84,8 +137,22 @@ class SettingsService {
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) => {
SETTINGS_GROUPS[group].forEach((key) => keysToSync.add(key));
});
const filteredSettings: Partial<PluginSettings> = {};
keysToSync.forEach((key) => {
(filteredSettings as Record<string, unknown>)[key] =
this.settings[key];
});
const data = {
...this.settings,
...filteredSettings,
syncGroups: this.settings.syncGroups, // Always include meta-settings
updatedAt: new Date().toISOString(),
};
@ -108,13 +175,19 @@ class SettingsService {
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) => {
SETTINGS_GROUPS[group].forEach((key) => allowedKeys.add(key));
});
let hasChanged = false;
const keys = Object.keys(data).filter(
(k) => k !== "updatedAt",
(k) => k !== "updatedAt" && k !== "syncGroups",
) as Array<keyof PluginSettings>;
for (const key of keys) {
if (!(key in this.settings)) continue;
if (!(key in this.settings) || !allowedKeys.has(key)) continue;
const newValue = data[key];
const currentValue = this.settings[key];

View file

@ -22,6 +22,8 @@ export interface StatusTemplate {
statuses: NoteStatus[];
}
export type SyncGroup = "statuses" | "appearance" | "behavior" | "features";
export type PluginSettings = {
fileExplorerIconPosition:
| "absolute-right"
@ -72,5 +74,6 @@ export type PluginSettings = {
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
[key: string]: unknown;
};