fix(settings): regroup card deck settings

中文: 重组卡片牌组设置分组,统一中文牌组文案,并将文件夹及文件名映射设为默认牌组模式。

English: Regroups card deck settings, normalizes Chinese deck copy, and makes folder-and-file mapping the default deck mode.
This commit is contained in:
Dusk 2026-04-26 09:35:19 +08:00
parent 77e6c33a7d
commit 271253c484
10 changed files with 369 additions and 71 deletions

View file

@ -0,0 +1,71 @@
# Card Deck Settings Regroup Decisions
## 1. Deck 卡标题与分组结构
Deck 卡最终标题固定为:
1. 中文:`卡片牌组设置`
2. 英文:`Card deck settings`
卡片内容固定分为 3 个小分组:
1. `1推荐用「文件夹及文件名」作为「Anki牌组」`
2. `2可打开「文件级自定义牌组」作为个性化定制`
3. `3默认牌组作为兜底`
每组分别承载:
1. 第 1 组:文件夹映射模式 + 两条映射示例
2. 第 2 组:文件级自定义牌组开关 + 条件展开项
3. 第 3 组:默认牌组输入 + 新兜底说明 + 最终优先级说明
## 2. 中文牌组文案规范
本次对中文 Deck 设置文案统一采用:
1. `牌组`
2. `牌组模板`
3. `牌组声明`
4. `文件夹映射牌组`
不翻译的技术值包括:
1. `folderDeckMode`
2. `off`
3. `folder`
4. `folder-and-file`
5. `yaml`
6. `body`
7. `TARGET DECK`
## 3. 默认 `folderDeckMode` 决策
新的默认行为固定为:
1. `DEFAULT_SETTINGS.folderDeckMode = "folder-and-file"`
由此带来的兼容性行为固定为:
1. 新安装用户默认就是 `folder-and-file`
2. legacy settings 缺失该字段时,通过 `mergePluginSettings()` 自动落到 `folder-and-file`
3. 已显式保存 `folderDeckMode` 的老用户保持原值不变
## 4. 保持不变的内容
本次明确不改:
1. deck resolution 顺序
2. 文件级牌组解析逻辑
3. 文件夹映射计算逻辑
4. 设置字段名与枚举值
5. 同步流程
6. `build:obsidian` 的安全复制约束
## 5. 与计划的显式偏差
没有行为偏差。
实现层面唯一说明是:
1. 现有 `build:obsidian` 已通过安全脚本复制 built plugin 文件
2. 因此最终同步直接复用仓库现有流程

View file

@ -0,0 +1,103 @@
# Card Deck Settings Regroup Gap Report
## 审查范围
本次按真实仓库实现审查了以下位置:
1. [src/presentation/settings/PluginSettingTab.ts](src/presentation/settings/PluginSettingTab.ts)
2. [src/presentation/i18n/messages/zh.ts](src/presentation/i18n/messages/zh.ts)
3. [src/presentation/i18n/messages/en.ts](src/presentation/i18n/messages/en.ts)
4. [src/application/config/PluginSettings.ts](src/application/config/PluginSettings.ts)
5. [src/application/config/PluginSettings.test.ts](src/application/config/PluginSettings.test.ts)
6. [src/infrastructure/persistence/DataJsonPluginConfigRepository.ts](src/infrastructure/persistence/DataJsonPluginConfigRepository.ts)
7. [src/infrastructure/persistence/DataJsonPluginConfigRepository.test.ts](src/infrastructure/persistence/DataJsonPluginConfigRepository.test.ts)
8. [scripts/sync-plugin-dist.mjs](scripts/sync-plugin-dist.mjs)
## 当前实现确认
### 1. Deck 卡目前仍是线性渲染
当前 `renderDeckCard()` 的真实顺序是:
1. 顶部说明段落
2. `默认牌组`
3. `开启文件级自定义牌组`
4. 文件级自定义牌组展开项
5. `文件夹映射模式`
6. 两条映射示例
7. 优先级说明
还没有:
1. 三个带序号的小分组
2. `data-deck-section`
3. `data-deck-section-title`
### 2. 顶部说明仍在渲染
`renderDeckCard()` 当前第一行仍然是:
1. `containerEl.createEl("p", { text: t("settings.cards.deck.desc") })`
因此旧说明 `保留现有 deck 行为;切换文件级 deck 开关时只刷新这一张卡片。` 仍会显示。
### 3. 中文 Deck 设置文案仍有明显中英文混杂
当前中文 `settings.deck.*` / `settings.cards.deck.*` 中仍存在这些混用示例:
1. `Deck 与牌组规则`
2. `文件级 deck`
3. `deck 模板`
4. `deck 声明`
5. `最终 deck 优先级`
这与本轮“中文设置页统一使用牌组”的目标不一致。
### 4. 新安装默认 `folderDeckMode` 仍是 `off`
当前 [src/application/config/PluginSettings.ts](src/application/config/PluginSettings.ts) 中:
1. `DEFAULT_SETTINGS.folderDeckMode = "off"`
因此:
1. 新安装默认行为与“推荐用文件夹及文件名映射”的 UI 不一致
2. legacy settings 缺失该字段时,也会通过 `mergePluginSettings()` 合并到 `off`
### 5. legacy 缺字段行为当前由默认合并控制
[src/infrastructure/persistence/DataJsonPluginConfigRepository.ts](src/infrastructure/persistence/DataJsonPluginConfigRepository.ts) 的 `load()` 直接调用:
1. `mergePluginSettings(snapshot.settings)`
因此只要 `DEFAULT_SETTINGS.folderDeckMode` 改为 `folder-and-file`
1. 缺字段 legacy snapshot 会自动得到新默认值
2. 已显式保存该字段的用户值不会被覆盖
### 6. 当前已有局部刷新行为可直接复用
当前 `开启文件级自定义牌组` toggle 已经:
1. 更新 `fileDeckEnabled`
2. 只调用 `this.renderCard("deck")`
这说明本次只需改 UI 分组和文案,不需要动刷新机制。
## 建议的最小落点
1. 在 `PluginSettingTab.ts` 新增轻量 `createDeckSection()` helper
2. 在 `renderDeckCard()` 内把现有控件重新分配到 3 个分组
3. 删除顶部说明的渲染
4. 统一 `settings.deck.*``settings.cards.deck.*` 的中文牌组文案
5. 把 `DEFAULT_SETTINGS.folderDeckMode` 改为 `folder-and-file`
6. 在 `PluginSettingTab.test.ts`、`PluginSettings.test.ts`、`DataJsonPluginConfigRepository.test.ts` 补对应断言
## 与计划的显式偏差
没有行为偏差。
唯一需要记录的是:
1. 当前仓库的 built-plugin sync 脚本已经符合“只复制 main.js / manifest.json / 可选 styles.css不碰 data.json”的要求
2. 因此最终同步继续复用现有 `build:obsidian` 路径

File diff suppressed because one or more lines are too long

View file

@ -72,7 +72,7 @@ describe("PluginSettings", () => {
expect(DEFAULT_SETTINGS.fileDeckMarker).toBe("TARGET DECK");
expect(DEFAULT_SETTINGS.fileDeckTemplate).toBe("obsidian::filename");
expect(DEFAULT_SETTINGS.fileDeckInsertLocation).toBe("body");
expect(DEFAULT_SETTINGS.folderDeckMode).toBe("off");
expect(DEFAULT_SETTINGS.folderDeckMode).toBe("folder-and-file");
expect(DEFAULT_SETTINGS.qaGroupMarker).toBe("#anki-list");
expect(DEFAULT_SETTINGS.cardAnswerCutoffMode).toBe("heading-block");
expect(DEFAULT_SETTINGS.obsidianBacklinkLabel).toBe(DEFAULT_OBSIDIAN_BACKLINK_LABEL);

View file

@ -90,7 +90,7 @@ export const DEFAULT_SETTINGS: PluginSettings = {
fileDeckMarker: "TARGET DECK",
fileDeckTemplate: "obsidian::filename",
fileDeckInsertLocation: "body",
folderDeckMode: "off",
folderDeckMode: "folder-and-file",
scopeMode: "all",
includeFolders: [],
excludeFolders: [],

View file

@ -40,7 +40,7 @@ describe("DataJsonPluginConfigRepository", () => {
expect(settings.fileDeckMarker).toBe("TARGET DECK");
expect(settings.fileDeckTemplate).toBe("obsidian::filename");
expect(settings.fileDeckInsertLocation).toBe("body");
expect(settings.folderDeckMode).toBe("off");
expect(settings.folderDeckMode).toBe("folder-and-file");
expect(settings.qaGroupMarker).toBe("#anki-list");
expect(settings.cardAnswerCutoffMode).toBe("heading-block");
expect(settings.semanticQaMarker).toBe("#anki-list-qa");
@ -50,6 +50,20 @@ describe("DataJsonPluginConfigRepository", () => {
expect(settings.ankiModelFieldCache).toEqual({});
});
it("preserves an explicit existing folderDeckMode value on load", async () => {
const repository = new DataJsonPluginConfigRepository(
new InMemoryPluginDataStore({
settings: {
folderDeckMode: "off",
},
}),
);
const settings = await repository.load();
expect(settings.folderDeckMode).toBe("off");
});
it("normalizes blank backlink labels on load and save", async () => {
const store = new InMemoryPluginDataStore({
settings: {

View file

@ -187,8 +187,9 @@ export const en = {
},
folderExample: "Folder-based mapping example: 数学/第一章/第一节.md -> 数学::第一章",
folderAndFileExample: "Folder-and-file mapping example: 数学/第一章/第一节.md -> 数学::第一章::第一节",
fallbackDesc: "If neither folder-and-file mapping nor file-level custom decks are used, the default deck name is used as the Anki deck.",
priorityTitle: "Final priority",
priorityDesc: "Final deck priority: file-level custom deck > folder-mapped deck > default deck. Existing-card deck behavior stays aligned with the current sync implementation.",
priorityDesc: "Final deck priority: file-level custom deck > folder-mapped deck > default deck.",
},
scope: {
name: "Plugin run scope",
@ -289,8 +290,13 @@ export const en = {
refreshFolders: "Refresh folder list",
},
deck: {
title: "Deck and deck rules",
desc: "Keep the current deck behavior, but refresh only this card when file-level deck mode changes.",
title: "Card deck settings",
desc: "Card deck settings are grouped by recommended workflow.",
sections: {
folderMapping: "1. Recommended: use folder-and-file mapping for Anki decks",
fileDeck: "2. Optional: enable file-level custom decks",
defaultDeck: "3. Fallback: use the default deck",
},
},
commands: {
title: "Command guide",

View file

@ -145,16 +145,16 @@ export const zh = {
defaultSectionTitle: "默认牌组",
defaultDeck: {
name: "默认牌组",
desc: "优先级最低:当文件级 deck 与文件夹映射都未命中时使用。",
desc: "优先级最低:当文件级自定义牌组与文件夹映射牌组都未命中时使用。",
},
fileDeckSectionTitle: "文件级自定义牌组",
fileDeckEnabled: {
name: "开启文件级自定义牌组",
desc: "开启后,可在 YAML 或正文中用统一 marker 为整篇笔记指定一个 deck。",
desc: "开启后,可在 YAML 或正文中用统一识别名为整篇笔记指定一个牌组。",
},
marker: {
name: "牌组识别名",
desc: "YAML key 与正文 marker 共用同一个识别名。默认值是 TARGET DECK。",
desc: "YAML key 与正文标记共用同一个识别名。默认值是 TARGET DECK。",
},
template: {
name: "默认牌组模板",
@ -162,31 +162,32 @@ export const zh = {
},
insertLocation: {
name: "模板插入位置",
desc: "选择将 deck 模板写入 YAML frontmatter 还是正文前部。",
desc: "选择将牌组模板写入 YAML frontmatter 还是正文前部。",
options: {
body: "正文前部",
yaml: "YAML frontmatter",
},
},
insertTemplate: {
name: "向当前文件插入 deck 模板",
desc: "按当前 marker、模板和插入位置把 deck 声明写入当前活动 Markdown 文件。",
button: "插入 deck 模板",
name: "向当前文件插入牌组模板",
desc: "按当前识别名、模板和插入位置,把牌组声明写入当前活动 Markdown 文件。",
button: "插入牌组模板",
},
folderMappingSectionTitle: "高级:文件夹映射",
folderMappingSectionTitle: "高级:文件夹映射牌组",
folderDeckMode: {
name: "文件夹映射模式",
desc: "关闭 / 仅父文件夹 / 父文件夹加当前文件名。",
desc: "关闭 / 仅父文件夹 / 文件夹及文件名。",
options: {
off: "关闭",
folder: "文件夹级映射",
folderAndFile: "文件夹及文件名级映射",
},
},
folderExample: "文件夹级映射示例:数学/第一章/第一节.md -> 数学::第一章",
folderAndFileExample: "文件夹及文件名级映射示例:数学/第一章/第一节.md -> 数学::第一章::第一节",
priorityTitle: "最终优先级说明",
priorityDesc: "最终 deck 优先级:文件级自定义牌组 > 文件夹映射 deck > 默认牌组。旧卡的 deck 行为保持与当前同步实现一致。",
folderExample: "文件夹映射牌组示例:数学/第一章/第一节.md -> 数学::第一章",
folderAndFileExample: "文件夹及文件名映射牌组示例:数学/第一章/第一节.md -> 数学::第一章::第一节",
fallbackDesc: "若均不打开「文件夹及文件名」及「文件级」牌组,则以「默认牌组名称」作为 Anki 的牌组名。",
priorityTitle: "最终牌组优先级说明",
priorityDesc: "最终牌组优先级:文件级自定义牌组 > 文件夹映射牌组 > 默认牌组。",
},
scope: {
name: "插件运行范围",
@ -287,8 +288,13 @@ export const zh = {
refreshFolders: "刷新文件夹列表",
},
deck: {
title: "Deck 与牌组规则",
desc: "保留现有 deck 行为;切换文件级 deck 开关时只刷新这一张卡片。",
title: "卡片牌组设置",
desc: "卡片牌组设置按推荐路径分组展示。",
sections: {
folderMapping: "1推荐用「文件夹及文件名」作为「Anki牌组」",
fileDeck: "2可打开「文件级自定义牌组」作为个性化定制",
defaultDeck: "3默认牌组作为兜底",
},
},
commands: {
title: "命令说明",
@ -323,12 +329,12 @@ export const zh = {
currentFileSyncFailed: "当前文件同步失败。",
vaultSyncFailed: "全库同步失败。",
rebuildFailed: "卡片索引重建失败。",
noActiveMarkdownForDeckTemplateInsertion: "当前没有可用于插入 deck 模板的活动 Markdown 文件。",
noActiveMarkdownForDeckTemplateInsertion: "当前没有可用于插入牌组模板的活动 Markdown 文件。",
vaultGatewayNotInitialized: "Vault 网关尚未初始化。",
markdownFileNotFound: "未找到 Markdown 文件:{{filePath}}",
deckTemplateAlreadyCurrent: "当前文件中的 deck 模板已是最新,无需重复写入。",
deckTemplateInserted: "已向当前文件写入 deck 模板:{{deck}}",
deckTemplateInsertionFailed: "插入 deck 模板失败。",
deckTemplateAlreadyCurrent: "当前文件中的牌组模板已是最新,无需重复写入。",
deckTemplateInserted: "已向当前文件写入牌组模板:{{deck}}",
deckTemplateInsertionFailed: "插入牌组模板失败。",
noActiveMarkdownForReset: "当前没有可用于重置的活动 Markdown 文件。",
clearCurrentFileUseCaseNotInitialized: "清空当前文件用例尚未初始化。",
noTrackedCardsInCurrentFile: "当前文件没有已同步卡片。",
@ -353,7 +359,7 @@ export const zh = {
errors: {
currentFileOutOfScope: "当前文件不在插件作用范围内:{{filePath}}",
deck: {
emptyName: "Deck 不能为空,请检查文件级 deck 声明或默认 deck 设置。",
emptyName: "牌组不能为空,请检查文件级牌组声明或默认牌组设置。",
},
settings: {
headingLevelsRange: "标题层级必须是 1 到 6 之间的整数。",
@ -372,13 +378,13 @@ export const zh = {
qaGroupMarkerConflict: "QA Group 标记必须和语义 QA 标记不同。",
semanticQaNoteTypeRequired: "语义 QA 笔记类型不能为空。",
defaultDeckRequired: "默认牌组不能为空。",
fileDeckEnabledBoolean: "文件级 deck 开关必须是布尔值。",
fileDeckEnabledBoolean: "文件级牌组开关必须是布尔值。",
syncObsidianTagsToAnkiBoolean: "同步 Obsidian 标签到 Anki 开关必须是布尔值。",
keepPureTagLinesInCardBodyBoolean: "保留正文纯标签行开关必须是布尔值。",
fileDeckMarkerString: "文件级 deck marker 必须是字符串。",
fileDeckTemplateString: "文件级 deck 模板必须是字符串。",
fileDeckInsertLocationInvalid: "文件级 deck 模板插入位置只能是 yaml 或 body。",
folderDeckModeInvalid: "文件夹 deck 模式只能是 off、folder 或 folder-and-file。",
fileDeckMarkerString: "文件级牌组识别名必须是字符串。",
fileDeckTemplateString: "文件级牌组模板必须是字符串。",
fileDeckInsertLocationInvalid: "文件级牌组模板插入位置只能是 yaml 或 body。",
folderDeckModeInvalid: "文件夹牌组模式只能是 off、folder 或 folder-and-file。",
scopeModeInvalid: "运行范围模式只能是 all、include 或 exclude。",
obsidianBacklinkPlacementInvalid: "Obsidian 回链放置位置只能是 question-last-line、answer-first-line 或 answer-last-line。",
includeFoldersArray: "包含文件夹必须是数组。",
@ -455,8 +461,8 @@ export const zh = {
deck: {
conflictYamlBody: "检测到 YAML 和正文中的 {{marker}} 声明互相冲突,本次同步已使用 YAML 的值。",
multipleBodyDeclarations: "检测到文件中存在多个 {{marker}} 声明,本次同步只使用第一个正文声明。",
invalidFolderSegment: "检测到文件夹名或文件名包含 ::,已跳过文件夹映射并回退到默认 deck。",
fallbackDefault: "该文件没有显式 deck且无法生成文件夹映射 deck已回退到默认 deck。",
invalidFolderSegment: "检测到文件夹名或文件名包含 ::,已跳过文件夹映射并回退到默认牌组。",
fallbackDefault: "该文件没有显式牌组,且无法生成文件夹映射牌组,已回退到默认牌组。",
},
},
} satisfies typeof en;

View file

@ -551,6 +551,16 @@ function collectSettingNames(container: QueryRoot): string[] {
return asFakeContainer(container).settings.map((setting) => setting.name);
}
function collectOwnedSettingNames(container: FakeElementInstance): string[] {
return container.ownedSettings.map((setting) => setting.name);
}
function collectOwnedSettingDescs(container: FakeElementInstance): string[] {
return container.ownedSettings
.map((setting) => setting.desc)
.filter((desc): desc is string => typeof desc === "string");
}
function collectOptionValues(selectEl: FakeElementInstance): string[] {
return selectEl.children
.filter((element) => element.tag === "option")
@ -1375,4 +1385,74 @@ describe("PluginSettingTab", () => {
expect(plugin.settings.fileDeckEnabled).toBe(true);
expect(getEmptyCallCount(tab.containerEl)).toBe(initialEmptyCount);
});
it("renders the deck card with regrouped sections and normalized Chinese copy", async () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
expect(queryByDataset(tab.containerEl, "settingsCardToggle", "deck").textContent).toContain("卡片牌组设置");
expect(queryByDataset(tab.containerEl, "settingsCardToggle", "deck").textContent).not.toContain("Deck 与牌组规则");
await queryByDataset(tab.containerEl, "settingsCardToggle", "deck").trigger("click");
const deckBody = queryByDataset(tab.containerEl, "settingsCardBody", "deck");
const sectionTitles = queryAllByDataset(deckBody, "deckSectionTitle").map((element) => element.textContent || element.text);
expect(sectionTitles).toEqual([
"1推荐用「文件夹及文件名」作为「Anki牌组」",
"2可打开「文件级自定义牌组」作为个性化定制",
"3默认牌组作为兜底",
]);
const section1 = queryByDataset(deckBody, "deckSection", "folder-mapping");
const section2 = queryByDataset(deckBody, "deckSection", "file-deck");
const section3 = queryByDataset(deckBody, "deckSection", "default-deck");
expect(collectTexts(deckBody)).not.toContain("保留现有 deck 行为;切换文件级 deck 开关时只刷新这一张卡片。");
expect(collectTexts(deckBody)).toContain("若均不打开「文件夹及文件名」及「文件级」牌组,则以「默认牌组名称」作为 Anki 的牌组名。");
expect(collectTexts(deckBody)).toContain("最终牌组优先级:文件级自定义牌组 > 文件夹映射牌组 > 默认牌组。");
expect(collectOwnedSettingNames(section1)).toEqual(["文件夹映射模式"]);
expect(collectOwnedSettingNames(section2)).toEqual(["开启文件级自定义牌组"]);
expect(collectOwnedSettingNames(section3)).toEqual(["默认牌组"]);
const visibleDeckCopy = [
...collectTexts(deckBody),
...collectOwnedSettingNames(section1),
...collectOwnedSettingNames(section2),
...collectOwnedSettingNames(section3),
...collectOwnedSettingDescs(section1),
...collectOwnedSettingDescs(section2),
...collectOwnedSettingDescs(section3),
].join("\n");
expect(visibleDeckCopy).not.toContain("文件级 deck");
expect(visibleDeckCopy).not.toContain("deck 模板");
expect(visibleDeckCopy).not.toContain("deck 声明");
expect(visibleDeckCopy).not.toContain("最终 deck 优先级");
expect(visibleDeckCopy).not.toContain("Deck 与牌组规则");
expect(findSetting(tab.containerEl, "默认牌组")).toBeDefined();
expect(findSetting(tab.containerEl, "文件夹映射模式")).toBeDefined();
expect(findSetting(tab.containerEl, "开启文件级自定义牌组")).toBeDefined();
});
it("keeps file-level custom deck controls in section 2 when enabled", async () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await queryByDataset(tab.containerEl, "settingsCardToggle", "deck").trigger("click");
const fileDeckSetting = findSetting(tab.containerEl, "开启文件级自定义牌组");
await getToggle(fileDeckSetting).triggerChange(true);
const deckBody = queryByDataset(tab.containerEl, "settingsCardBody", "deck");
const section2 = queryByDataset(deckBody, "deckSection", "file-deck");
expect(collectOwnedSettingNames(section2)).toEqual([
"开启文件级自定义牌组",
"牌组识别名",
"默认牌组模板",
"模板插入位置",
"向当前文件插入牌组模板",
]);
});
});

View file

@ -650,28 +650,26 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
}
private renderDeckCard(containerEl: HTMLElement): void {
containerEl.createEl("p", { text: t("settings.cards.deck.desc") });
const folderMappingSection = this.createDeckSection(containerEl, "folder-mapping", t("settings.cards.deck.sections.folderMapping"));
new Setting(folderMappingSection)
.setName(t("settings.deck.folderDeckMode.name"))
.setDesc(t("settings.deck.folderDeckMode.desc"))
.addDropdown((dropdown) => {
this.populateFolderDeckModeDropdown(dropdown, this.plugin.settings.folderDeckMode);
dropdown.onChange((value) => {
if (value !== "off" && value !== "folder" && value !== "folder-and-file") {
return;
}
new Setting(containerEl)
.setName(t("settings.deck.defaultDeck.name"))
.setDesc(t("settings.deck.defaultDeck.desc"))
.addText((text) => {
text.setValue(this.getDraftValue("default-deck", this.plugin.settings.defaultDeck)).onChange((value) => {
this.scheduleDebouncedTextSave("default-deck", value, async (draftValue) => {
const nextValue = draftValue.trim();
if (!nextValue) {
this.textDraftValues.delete("default-deck");
this.renderCard("deck");
return;
}
await this.plugin.updateSettings({ defaultDeck: nextValue });
this.textDraftValues.delete("default-deck");
});
void this.plugin.updateSettings({ folderDeckMode: value as FolderDeckMode });
});
});
new Setting(containerEl)
folderMappingSection.createEl("p", { text: t("settings.deck.folderExample") });
folderMappingSection.createEl("p", { text: t("settings.deck.folderAndFileExample") });
const fileDeckSection = this.createDeckSection(containerEl, "file-deck", t("settings.cards.deck.sections.fileDeck"));
new Setting(fileDeckSection)
.setName(t("settings.deck.fileDeckEnabled.name"))
.setDesc(t("settings.deck.fileDeckEnabled.desc"))
.addToggle((toggle) => {
@ -682,7 +680,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
});
if (this.plugin.settings.fileDeckEnabled) {
new Setting(containerEl)
new Setting(fileDeckSection)
.setName(t("settings.deck.marker.name"))
.setDesc(t("settings.deck.marker.desc"))
.addText((text) => {
@ -701,7 +699,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
new Setting(fileDeckSection)
.setName(t("settings.deck.template.name"))
.setDesc(t("settings.deck.template.desc"))
.addText((text) => {
@ -720,7 +718,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
new Setting(fileDeckSection)
.setName(t("settings.deck.insertLocation.name"))
.setDesc(t("settings.deck.insertLocation.desc"))
.addDropdown((dropdown) => {
@ -734,7 +732,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
new Setting(fileDeckSection)
.setName(t("settings.deck.insertTemplate.name"))
.setDesc(t("settings.deck.insertTemplate.desc"))
.addButton((button) => {
@ -744,23 +742,43 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
});
}
new Setting(containerEl)
.setName(t("settings.deck.folderDeckMode.name"))
.setDesc(t("settings.deck.folderDeckMode.desc"))
.addDropdown((dropdown) => {
this.populateFolderDeckModeDropdown(dropdown, this.plugin.settings.folderDeckMode);
dropdown.onChange((value) => {
if (value !== "off" && value !== "folder" && value !== "folder-and-file") {
return;
}
const defaultDeckSection = this.createDeckSection(containerEl, "default-deck", t("settings.cards.deck.sections.defaultDeck"));
new Setting(defaultDeckSection)
.setName(t("settings.deck.defaultDeck.name"))
.setDesc(t("settings.deck.defaultDeck.desc"))
.addText((text) => {
text.setValue(this.getDraftValue("default-deck", this.plugin.settings.defaultDeck)).onChange((value) => {
this.scheduleDebouncedTextSave("default-deck", value, async (draftValue) => {
const nextValue = draftValue.trim();
if (!nextValue) {
this.textDraftValues.delete("default-deck");
this.renderCard("deck");
return;
}
void this.plugin.updateSettings({ folderDeckMode: value as FolderDeckMode });
await this.plugin.updateSettings({ defaultDeck: nextValue });
this.textDraftValues.delete("default-deck");
});
});
});
containerEl.createEl("p", { text: t("settings.deck.folderExample") });
containerEl.createEl("p", { text: t("settings.deck.folderAndFileExample") });
containerEl.createEl("p", { text: t("settings.deck.priorityDesc") });
defaultDeckSection.createEl("p", { text: t("settings.deck.fallbackDesc") });
defaultDeckSection.createEl("p", { text: t("settings.deck.priorityDesc") });
}
private createDeckSection(containerEl: HTMLElement, sectionId: string, title: string): HTMLElement {
const sectionEl = containerEl.createDiv();
sectionEl.dataset.deckSection = sectionId;
sectionEl.style.display = "flex";
sectionEl.style.flexDirection = "column";
sectionEl.style.gap = "8px";
sectionEl.style.marginTop = "16px";
const titleEl = sectionEl.createEl("h4", { text: title });
titleEl.dataset.deckSectionTitle = sectionId;
titleEl.style.margin = "0";
return sectionEl;
}
private renderCommandsCard(containerEl: HTMLElement): void {