mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
feat: use Obsidian theme colors for settings page action buttons and toggles
- Settings page action buttons now use Obsidian theme accent colors. - Settings toggles now have clearer themed states. - Styling is scoped to the plugin settings page. 功能:设置页面按钮和切换开关现在使用 Obsidian 主题色 - 设置页面的操作按钮现在使用 Obsidian 主题强调色。 - 设置切换开关现在具有更清晰的主题化状态。 - 样式已限定在插件设置页面范围内。
This commit is contained in:
parent
8357992ee3
commit
79ddbef838
4 changed files with 179 additions and 24 deletions
30
main.js
30
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,17 +1,32 @@
|
|||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { access, copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = resolve(scriptDir, "..");
|
||||
const outputDir = join(rootDir, "dist", "plugin");
|
||||
const stylesSourcePath = join(rootDir, "styles.css");
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
await Promise.all([
|
||||
let hasStylesheet = false;
|
||||
try {
|
||||
await access(stylesSourcePath);
|
||||
hasStylesheet = true;
|
||||
} catch {
|
||||
hasStylesheet = false;
|
||||
}
|
||||
|
||||
const stageTasks = [
|
||||
copyFile(join(rootDir, "manifest.json"), join(outputDir, "manifest.json")),
|
||||
copyFile(join(rootDir, "versions.json"), join(outputDir, "versions.json")),
|
||||
]);
|
||||
];
|
||||
|
||||
if (hasStylesheet) {
|
||||
stageTasks.push(copyFile(stylesSourcePath, join(outputDir, "styles.css")));
|
||||
}
|
||||
|
||||
await Promise.all(stageTasks);
|
||||
|
||||
await writeFile(
|
||||
join(outputDir, "README.md"),
|
||||
|
|
@ -25,6 +40,7 @@ await writeFile(
|
|||
"- manifest.json",
|
||||
"- main.js",
|
||||
"- versions.json",
|
||||
...(hasStylesheet ? ["- styles.css"] : []),
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,8 +36,35 @@ const {
|
|||
}
|
||||
}
|
||||
|
||||
class HoistedFakeClassList {
|
||||
private readonly values = new Set<string>();
|
||||
|
||||
add(...tokens: string[]): void {
|
||||
for (const token of tokens) {
|
||||
if (token) {
|
||||
this.values.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(...tokens: string[]): void {
|
||||
for (const token of tokens) {
|
||||
this.values.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
contains(token: string): boolean {
|
||||
return this.values.has(token);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return [...this.values].join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
class HoistedFakeElement {
|
||||
public readonly children: HoistedFakeElement[] = [];
|
||||
public readonly classList = new HoistedFakeClassList();
|
||||
public readonly dataset: Record<string, string> = {};
|
||||
public readonly style = new HoistedFakeStyle();
|
||||
public readonly ownedSettings: HoistedFakeSetting[] = [];
|
||||
|
|
@ -148,10 +175,13 @@ const {
|
|||
|
||||
class HoistedFakeButtonComponent {
|
||||
public text = "";
|
||||
public readonly buttonEl = new HoistedFakeElement(new HoistedFakeContainerEl(), "button", null);
|
||||
private onClickHandler?: () => void | Promise<void>;
|
||||
|
||||
setButtonText(text: string): this {
|
||||
this.text = text;
|
||||
this.buttonEl.text = text;
|
||||
this.buttonEl.textContent = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +248,7 @@ const {
|
|||
|
||||
class HoistedFakeToggleComponent {
|
||||
public value = false;
|
||||
public readonly toggleEl = new HoistedFakeElement(new HoistedFakeContainerEl(), "div", null);
|
||||
private onChangeHandler?: (value: boolean) => void | Promise<void>;
|
||||
|
||||
setValue(value: boolean): this {
|
||||
|
|
@ -351,7 +382,8 @@ import { AnkiHeadingSyncSettingTab } from "./PluginSettingTab";
|
|||
|
||||
type FakeContainer = InstanceType<typeof FakePluginSettingTab>["containerEl"];
|
||||
type FakeSettingInstance = InstanceType<typeof FakeSetting>;
|
||||
type FakeToggleInstance = { triggerChange(value: boolean): Promise<void> };
|
||||
type FakeButtonInstance = { click(): Promise<void>; buttonEl: FakeElementInstance };
|
||||
type FakeToggleInstance = { value: boolean; triggerChange(value: boolean): Promise<void>; toggleEl: FakeElementInstance };
|
||||
type FakeElementInstance = InstanceType<typeof FakeElement>;
|
||||
type QueryRoot = FakeContainer | FakeElementInstance | HTMLElement;
|
||||
|
||||
|
|
@ -540,6 +572,15 @@ function getToggle(setting: FakeSettingInstance): FakeToggleInstance {
|
|||
return toggle as FakeToggleInstance;
|
||||
}
|
||||
|
||||
function getButton(setting: FakeSettingInstance): FakeButtonInstance {
|
||||
const button = setting.controls.find((control) => typeof control === "object" && control !== null && "click" in control && "buttonEl" in control);
|
||||
if (!button) {
|
||||
throw new Error(`Button not found for setting: ${setting.name}`);
|
||||
}
|
||||
|
||||
return button as FakeButtonInstance;
|
||||
}
|
||||
|
||||
function collectTexts(container: QueryRoot): string[] {
|
||||
return findElements(container, () => true)
|
||||
.map((element) => element.textContent || element.text)
|
||||
|
|
@ -620,6 +661,7 @@ describe("PluginSettingTab", () => {
|
|||
|
||||
tab.display();
|
||||
|
||||
expect(tab.containerEl.classList.contains("anki-heading-sync-settings")).toBe(true);
|
||||
expect(queryByDataset(tab.containerEl, "settingsPageHeader", "true")).toBeDefined();
|
||||
const cards = queryAllByDataset(tab.containerEl, "settingsCard");
|
||||
expect(cards).toHaveLength(5);
|
||||
|
|
@ -795,6 +837,69 @@ describe("PluginSettingTab", () => {
|
|||
expect(plugin.updateCalls).toContainEqual({ convertHighlightsToCloze: false });
|
||||
});
|
||||
|
||||
it("applies the themed class only to settings action buttons", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = normalizePluginSettings({
|
||||
...plugin.settings,
|
||||
scopeMode: "include",
|
||||
});
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
tab.display();
|
||||
await expandCard(tab, "card-types");
|
||||
await expandCard(tab, "scope");
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByDataset(tab.containerEl, "cardTypesRefresh", "true").classList.contains("ahs-theme-action-button")).toBe(true);
|
||||
expect(queryByDataset(tab.containerEl, "scopeRefreshFolders", "true").classList.contains("ahs-theme-action-button")).toBe(true);
|
||||
|
||||
const cardHeaderButton = queryByDataset(tab.containerEl, "settingsCardToggle", "card-types");
|
||||
expect(cardHeaderButton.classList.contains("ahs-theme-action-button")).toBe(false);
|
||||
|
||||
const folderToggleButton = queryByDataset(tab.containerEl, "folderToggle", "notes");
|
||||
expect(folderToggleButton.classList.contains("ahs-theme-action-button")).toBe(false);
|
||||
|
||||
await expandCard(tab, "deck");
|
||||
const fileDeckSetting = findSetting(tab.containerEl, "开启文件级自定义牌组");
|
||||
await getToggle(fileDeckSetting).triggerChange(true);
|
||||
|
||||
const insertTemplateSetting = findSetting(tab.containerEl, "向当前文件插入牌组模板");
|
||||
const insertTemplateButton = getButton(insertTemplateSetting);
|
||||
expect(insertTemplateButton.buttonEl.classList.contains("ahs-theme-action-button")).toBe(true);
|
||||
|
||||
await insertTemplateButton.click();
|
||||
expect(plugin.insertDeckTemplateCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("marks every settings toggle with theme state and updates the dataset on change", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
tab.display();
|
||||
await expandCard(tab, "sync-content");
|
||||
await expandCard(tab, "deck");
|
||||
|
||||
const toggleSettings = [
|
||||
findSetting(tab.containerEl, "添加 Obsidian 回链"),
|
||||
findSetting(tab.containerEl, "同步 Obsidian 标签到 Anki"),
|
||||
findSetting(tab.containerEl, "在卡片正文中保留纯标签行"),
|
||||
findSetting(tab.containerEl, "高亮转填空题"),
|
||||
findSetting(tab.containerEl, "开启文件级自定义牌组"),
|
||||
];
|
||||
|
||||
for (const setting of toggleSettings) {
|
||||
const toggle = getToggle(setting);
|
||||
expect(toggle.toggleEl.classList.contains("ahs-theme-toggle")).toBe(true);
|
||||
expect(toggle.toggleEl.dataset.ahsToggleState).toBe(toggle.value ? "on" : "off");
|
||||
}
|
||||
|
||||
const highlightsToggle = getToggle(findSetting(tab.containerEl, "高亮转填空题"));
|
||||
const nextValue = !highlightsToggle.value;
|
||||
await highlightsToggle.triggerChange(nextValue);
|
||||
|
||||
expect(highlightsToggle.toggleEl.dataset.ahsToggleState).toBe(nextValue ? "on" : "off");
|
||||
});
|
||||
|
||||
it("auto saves toggle, heading, note type and field mapping edits", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PluginSettingTab, Setting } from "obsidian";
|
||||
import type { ButtonComponent, ToggleComponent } from "obsidian";
|
||||
|
||||
import {
|
||||
DEFAULT_OBSIDIAN_BACKLINK_LABEL,
|
||||
|
|
@ -49,6 +50,9 @@ const SETTINGS_PAGE_HEADER_PADDING_TOP = "12px";
|
|||
const SETTINGS_PAGE_HEADER_MASK_TOP = "-128px";
|
||||
const SETTINGS_PAGE_HEADER_MASK_SIDE = "-24px";
|
||||
const DECK_HELPER_TEXT_INDENT = "32px";
|
||||
const SETTINGS_ROOT_CLASS = "anki-heading-sync-settings";
|
||||
const THEME_ACTION_BUTTON_CLASS = "ahs-theme-action-button";
|
||||
const THEME_TOGGLE_CLASS = "ahs-theme-toggle";
|
||||
|
||||
type SettingsCardId = (typeof SETTINGS_CARD_ORDER)[number];
|
||||
type NoteTypeCacheCheckStatus = "idle" | "checking" | "same" | "changed" | "failed";
|
||||
|
|
@ -110,6 +114,8 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
const previousScrollTop = containerEl.scrollTop;
|
||||
|
||||
containerEl.classList.add(SETTINGS_ROOT_CLASS);
|
||||
|
||||
if (!this.displayInitialized) {
|
||||
containerEl.style.setProperty(SETTINGS_PAGE_HEADER_HEIGHT_VARIABLE, SETTINGS_PAGE_HEADER_HEIGHT_FALLBACK);
|
||||
containerEl.style.setProperty(SETTINGS_STICKY_CARD_GAP_VARIABLE, SETTINGS_STICKY_CARD_GAP);
|
||||
|
|
@ -162,6 +168,31 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
containerEl.scrollTop = previousScrollTop;
|
||||
}
|
||||
|
||||
private markThemeButton(buttonEl: HTMLButtonElement): void {
|
||||
buttonEl.classList.add(THEME_ACTION_BUTTON_CLASS);
|
||||
}
|
||||
|
||||
private markThemeButtonComponent(button: ButtonComponent): void {
|
||||
this.markThemeButton(button.buttonEl);
|
||||
}
|
||||
|
||||
private markThemeToggle(
|
||||
toggle: ToggleComponent,
|
||||
value: boolean,
|
||||
onChange: (value: boolean) => void | Promise<void>,
|
||||
): void {
|
||||
toggle.toggleEl.classList.add(THEME_TOGGLE_CLASS);
|
||||
this.syncThemeToggleState(toggle, value);
|
||||
toggle.setValue(value).onChange((nextValue) => {
|
||||
this.syncThemeToggleState(toggle, nextValue);
|
||||
return onChange(nextValue);
|
||||
});
|
||||
}
|
||||
|
||||
private syncThemeToggleState(toggle: ToggleComponent, value: boolean): void {
|
||||
toggle.toggleEl.dataset.ahsToggleState = value ? "on" : "off";
|
||||
}
|
||||
|
||||
private initializeCards(containerEl: HTMLElement): void {
|
||||
this.cardShells.clear();
|
||||
|
||||
|
|
@ -297,6 +328,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
? t("settings.cards.cardTypes.loadAnki.loading")
|
||||
: t("settings.cards.cardTypes.loadAnki.button"),
|
||||
}) as HTMLButtonElement;
|
||||
this.markThemeButton(loadButton);
|
||||
loadButton.type = "button";
|
||||
loadButton.dataset.cardTypesRefresh = "true";
|
||||
loadButton.disabled = this.ankiConfigLoading;
|
||||
|
|
@ -573,7 +605,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.syncOptions.addObsidianBacklink.name"))
|
||||
.setDesc(t("settings.syncOptions.addObsidianBacklink.desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.addObsidianBacklink).onChange((value) => {
|
||||
this.markThemeToggle(toggle, this.plugin.settings.addObsidianBacklink, (value) => {
|
||||
void this.plugin.updateSettings({ addObsidianBacklink: value });
|
||||
});
|
||||
});
|
||||
|
|
@ -613,7 +645,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.syncOptions.syncObsidianTagsToAnki.name"))
|
||||
.setDesc(t("settings.syncOptions.syncObsidianTagsToAnki.desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.syncObsidianTagsToAnki).onChange((value) => {
|
||||
this.markThemeToggle(toggle, this.plugin.settings.syncObsidianTagsToAnki, (value) => {
|
||||
void this.plugin.updateSettings({ syncObsidianTagsToAnki: value });
|
||||
});
|
||||
});
|
||||
|
|
@ -622,7 +654,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.syncOptions.keepPureTagLinesInCardBody.name"))
|
||||
.setDesc(t("settings.syncOptions.keepPureTagLinesInCardBody.desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.keepPureTagLinesInCardBody).onChange((value) => {
|
||||
this.markThemeToggle(toggle, this.plugin.settings.keepPureTagLinesInCardBody, (value) => {
|
||||
void this.plugin.updateSettings({ keepPureTagLinesInCardBody: value });
|
||||
});
|
||||
});
|
||||
|
|
@ -632,7 +664,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.syncOptions.highlightsToCloze.name"))
|
||||
.setDesc(t("settings.syncOptions.highlightsToCloze.desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.convertHighlightsToCloze).onChange((value) => {
|
||||
this.markThemeToggle(toggle, this.plugin.settings.convertHighlightsToCloze, (value) => {
|
||||
void this.plugin.updateSettings({ convertHighlightsToCloze: value });
|
||||
});
|
||||
});
|
||||
|
|
@ -678,6 +710,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
|
||||
const refreshRow = containerEl.createDiv();
|
||||
const refreshButton = refreshRow.createEl("button", { text: t("settings.cards.scope.refreshFolders") }) as HTMLButtonElement;
|
||||
this.markThemeButton(refreshButton);
|
||||
refreshButton.type = "button";
|
||||
refreshButton.dataset.scopeRefreshFolders = "true";
|
||||
refreshButton.disabled = Boolean(this.folderTreeLoadPromise);
|
||||
|
|
@ -731,7 +764,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.deck.fileDeckEnabled.name"))
|
||||
.setDesc(t("settings.deck.fileDeckEnabled.desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.fileDeckEnabled).onChange(async (value) => {
|
||||
this.markThemeToggle(toggle, this.plugin.settings.fileDeckEnabled, async (value) => {
|
||||
await this.plugin.updateSettings({ fileDeckEnabled: value });
|
||||
this.renderCard("deck");
|
||||
});
|
||||
|
|
@ -794,6 +827,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.setName(t("settings.deck.insertTemplate.name"))
|
||||
.setDesc(t("settings.deck.insertTemplate.desc"))
|
||||
.addButton((button) => {
|
||||
this.markThemeButtonComponent(button);
|
||||
button.setButtonText(t("settings.deck.insertTemplate.button")).onClick(() => {
|
||||
void this.plugin.insertDeckTemplateToCurrentFile();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue