mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
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:
parent
77e6c33a7d
commit
271253c484
10 changed files with 369 additions and 71 deletions
71
docs/card-deck-settings-regroup-decisions.md
Normal file
71
docs/card-deck-settings-regroup-decisions.md
Normal 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. 因此最终同步直接复用仓库现有流程
|
||||
103
docs/card-deck-settings-regroup-gap-report.md
Normal file
103
docs/card-deck-settings-regroup-gap-report.md
Normal 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` 路径
|
||||
6
main.js
6
main.js
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
"开启文件级自定义牌组",
|
||||
"牌组识别名",
|
||||
"默认牌组模板",
|
||||
"模板插入位置",
|
||||
"向当前文件插入牌组模板",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue