feat(plugins): add plugin dependency resolution system

- Centralize plugin imports in utils/deps.ts with dependency graph
- Cascade dependencies when toggling plugins (e.g., auto-save requires memory)
- Rename plugin setting keys from snake_case to kebab-case to match plugin IDs
- Update settings toggle to use resolve() for dependency-aware behavior
- Fix Object.assign to avoid mutating DEFAULT_SETTINGS
This commit is contained in:
Uglyboy 2026-04-30 09:02:35 +08:00
parent a3ddbeeb66
commit 3b1a19cd0c
5 changed files with 183 additions and 53 deletions

View file

@ -1,22 +1,11 @@
import { Plugins } from "@inkweave/core";
import {
audioPlugin,
autoButtonPlugin,
autoRestorePlugin,
cdButtonPlugin,
classTagPlugin,
fadeEffectPlugin,
imagePlugin,
linkOpenPlugin,
memoryPlugin,
scrollAfterChoicePlugin,
} from "@inkweave/plugins";
import { Platform, Plugin, type TAbstractFile, type WorkspaceLeaf } from "obsidian";
import { setupCommands } from "./commands";
import { I18n, type TransItemType } from "./locales/i18n";
import { SettingsTab } from "./settings";
import { DEFAULT_SETTINGS, type Settings } from "./types";
import { plugins } from "./utils/deps";
import { StoryView, VIEW_TYPE } from "./view";
export default class InkWeavePlugin extends Plugin {
@ -42,21 +31,12 @@ export default class InkWeavePlugin extends Plugin {
this.registerExtensions(["ink"], "markdown");
setupCommands(this);
Plugins.register(imagePlugin);
Plugins.register(audioPlugin);
Plugins.register(autoRestorePlugin);
Plugins.register(fadeEffectPlugin);
Plugins.register(scrollAfterChoicePlugin);
Plugins.register(linkOpenPlugin);
Plugins.register(memoryPlugin);
Plugins.register(autoButtonPlugin);
Plugins.register(cdButtonPlugin);
Plugins.register(classTagPlugin);
for (const p of plugins) Plugins.register(p);
Plugins.setPluginsEnabled(this.getPluginSettings());
}
async loadSettings() {
this.settings = Object.assign(DEFAULT_SETTINGS, (await this.loadData()) ?? {});
this.settings = Object.assign({}, DEFAULT_SETTINGS, (await this.loadData()) ?? {});
Plugins.setPluginsEnabled(this.getPluginSettings());
}

View file

@ -1,8 +1,9 @@
import { type App, PluginSettingTab, Setting } from "obsidian";
import type { TransItemType } from "./locales/i18n";
import type InkWeavePlugin from "./main";
import type { PluginSettings, Settings } from "./types";
import type { PluginSettings } from "./types";
import { DEFAULT_SETTINGS } from "./types";
import { resolve } from "./utils/deps";
export class SettingsTab extends PluginSettingTab {
plugin: InkWeavePlugin;
@ -25,16 +26,21 @@ export class SettingsTab extends PluginSettingTab {
.setName(this.t(titleKey))
.setDesc(this.t(descKey))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings[key])
.onChange(
async (value) =>
await this.plugin.updateSettings({ [key]: value } as Partial<Settings>),
),
toggle.setValue(this.plugin.settings[key]).onChange(async (value) => {
// 获取当前插件设置(排除 linedelay 和 debug
const { linedelay, debug, ...currentPluginSettings } = this.plugin.settings;
// 计算包含依赖关系的新设置
const newPluginSettings = resolve(currentPluginSettings, key, value);
// 更新所有相关设置
await this.plugin.updateSettings(newPluginSettings);
this.display();
}),
);
}
display(): void {
override display(): void {
this.containerEl.empty();
new Setting(this.containerEl).setName(this.t("settings_options")).setHeading();
@ -56,24 +62,23 @@ export class SettingsTab extends PluginSettingTab {
new Setting(this.containerEl).setName(this.t("settings_plugins")).setHeading();
// Fixed plugin settings
this.addPluginToggle("audio", "settings_audio_title", "settings_audio_desc");
this.addPluginToggle("image", "settings_image_title", "settings_image_desc");
this.addPluginToggle("linkopen", "settings_linkopen_title", "settings_linkopen_desc");
this.addPluginToggle("link-open", "settings_linkopen_title", "settings_linkopen_desc");
this.addPluginToggle("memory", "settings_memory_title", "settings_memory_desc");
this.addPluginToggle(
"auto_restore",
"auto-restore",
"settings_auto_restore_title",
"settings_auto_restore_desc",
);
this.addPluginToggle(
"scrollafterchoice",
"scroll-after-choice",
"settings_scrollafterchoice_title",
"settings_scrollafterchoice_desc",
);
this.addPluginToggle("fadeforline", "settings_fadeforline_title", "settings_fadeforline_desc");
this.addPluginToggle("cd_button", "settings_cdbutton_title", "settings_cdbutton_desc");
this.addPluginToggle("auto_button", "settings_autobutton_title", "settings_autobutton_desc");
this.addPluginToggle("auto_save", "settings_autosave_title", "settings_autosave_desc");
this.addPluginToggle("fade-effect", "settings_fadeforline_title", "settings_fadeforline_desc");
this.addPluginToggle("cd-button", "settings_cdbutton_title", "settings_cdbutton_desc");
this.addPluginToggle("auto-button", "settings_autobutton_title", "settings_autobutton_desc");
this.addPluginToggle("auto-save", "settings_autosave_title", "settings_autosave_desc");
}
}

View file

@ -1,14 +1,14 @@
export interface PluginSettings {
audio: boolean;
image: boolean;
linkopen: boolean;
"link-open": boolean;
memory: boolean;
auto_restore: boolean;
scrollafterchoice: boolean;
fadeforline: boolean;
cd_button: boolean;
auto_button: boolean;
auto_save: boolean;
"auto-restore": boolean;
"scroll-after-choice": boolean;
"fade-effect": boolean;
"cd-button": boolean;
"auto-button": boolean;
"auto-save": boolean;
}
export interface Settings extends PluginSettings {
@ -19,14 +19,14 @@ export interface Settings extends PluginSettings {
export const DEFAULT_SETTINGS: Settings = {
audio: true,
image: true,
linkopen: false,
"link-open": false,
memory: true,
auto_restore: true,
scrollafterchoice: true,
fadeforline: true,
cd_button: false,
auto_button: false,
auto_save: false,
"auto-restore": true,
"scroll-after-choice": true,
"fade-effect": true,
"cd-button": false,
"auto-button": false,
"auto-save": false,
linedelay: 0.1,
debug: false,
};

View file

@ -0,0 +1,68 @@
import { describe, expect, it } from "bun:test";
import { resolve } from "../deps";
describe("pluginDependencies", () => {
it("should enable memory when enabling auto-save", () => {
const currentSettings = {
audio: true,
image: true,
"link-open": false,
memory: false,
"auto-restore": false,
"scroll-after-choice": true,
"fade-effect": true,
"cd-button": false,
"auto-button": false,
"auto-save": false,
};
const newSettings = resolve(currentSettings, "auto-save", true);
expect(newSettings["auto-save"]).toBe(true);
expect(newSettings.memory).toBe(true); // memory should be enabled as dependency
});
it("should disable auto-save and auto-restore when disabling memory", () => {
const currentSettings = {
audio: true,
image: true,
"link-open": false,
memory: true,
"auto-restore": true,
"scroll-after-choice": true,
"fade-effect": true,
"cd-button": false,
"auto-button": false,
"auto-save": true,
};
const newSettings = resolve(currentSettings, "memory", false);
expect(newSettings.memory).toBe(false);
expect(newSettings["auto-save"]).toBe(false); // auto-save depends on memory
expect(newSettings["auto-restore"]).toBe(false); // auto-restore depends on memory
});
it("should not affect unrelated plugins", () => {
const currentSettings = {
audio: true,
image: true,
"link-open": false,
memory: true,
"auto-restore": true,
"scroll-after-choice": true,
"fade-effect": true,
"cd-button": false,
"auto-button": false,
"auto-save": true,
};
const newSettings = resolve(currentSettings, "audio", false);
expect(newSettings.audio).toBe(false);
// Other plugins should remain unchanged
expect(newSettings.image).toBe(true);
expect(newSettings.memory).toBe(true);
expect(newSettings["auto-save"]).toBe(true);
});
});

77
src/utils/deps.ts Normal file
View file

@ -0,0 +1,77 @@
import type { Plugin } from "@inkweave/core";
import {
audioPlugin,
autoButtonPlugin,
autoRestorePlugin,
autoSavePlugin,
cdButtonPlugin,
classTagPlugin,
fadeEffectPlugin,
imagePlugin,
linkOpenPlugin,
memoryPlugin,
scrollAfterChoicePlugin,
} from "@inkweave/plugins";
import type { PluginSettings } from "../types";
export const plugins: Plugin[] = [
audioPlugin,
autoButtonPlugin,
autoRestorePlugin,
autoSavePlugin,
cdButtonPlugin,
classTagPlugin,
fadeEffectPlugin,
imagePlugin,
linkOpenPlugin,
memoryPlugin,
scrollAfterChoicePlugin,
];
const deps: Record<string, string[]> = {};
const rdeps: Record<string, string[]> = {};
for (const p of plugins) {
deps[p.id] = p.dependencies || [];
rdeps[p.id] = [];
}
for (const p of plugins) {
for (const d of p.dependencies || []) {
if (rdeps[d]) rdeps[d].push(p.id);
}
}
const reachable = (start: string, edges: Record<string, string[]>): Set<string> => {
const found = new Set<string>();
const seen = new Set<string>();
const queue = [start];
let head = 0;
while (head < queue.length) {
const id = queue[head++];
if (id === undefined || seen.has(id)) continue;
seen.add(id);
for (const next of edges[id] || []) {
if (!found.has(next)) {
found.add(next);
queue.push(next);
}
}
}
return found;
};
export const resolve = (
settings: Record<string, boolean>,
id: string,
on: boolean,
): Partial<PluginSettings> => {
const next = { ...settings };
if (on) {
for (const dep of reachable(id, deps)) next[dep] = true;
next[id] = true;
} else {
for (const dep of reachable(id, rdeps)) next[dep] = false;
next[id] = false;
}
return next;
};