mirror of
https://github.com/insile/OpenWords.git
synced 2026-07-22 05:48:39 +00:00
2025/6/21
This commit is contained in:
parent
823d7ba084
commit
89f3491fac
10 changed files with 244 additions and 118 deletions
26
README.md
26
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# OpenWords
|
||||
|
||||
**OpenWords** 是一个用于背单词和单词管理的 Obsidian 插件, 与 [OpenText](https://github.com/insile/OpenText) 单词库配套设计, 已上线[社区插件市场](obsidian://show-plugin?id=openwords), 安装后插件入口是左侧工具栏上的一个按钮再加上两个绑定命令, 主要功能有生成索引, 背单词, 默写单词, 添加双链等, 其中单词文件使用 OpenText 中的单词库, 而背单词的核心逻辑是 SuperMemo 2 间隔重复算法. 本插件完全依赖单词文件及其属性, 没有外部存储文件没有记录回答历史 (考虑中), 所以不能做一些记忆曲线拟合, 单词调度看起来很简单朴素, 仅依靠上次回答记录. 插件启动时会读取所有单词缓存, 更改时会即时写入相应的单词文件并更新缓存. 插件没有设计过多的搜索功能, 提倡使用 Obsidian 内置的搜索功能. 如果您认为本插件有帮助, 欢迎微信赞赏支持. 目前等待 Obsidian Bases 正式上线后进行功能更新
|
||||
**OpenWords** 是一个用于背单词和单词管理的 Obsidian 插件, 与 [OpenText](https://github.com/insile/OpenText) 单词库配套设计, 已上线[社区插件市场](obsidian://show-plugin?id=openwords), 安装后插件入口是左侧工具栏上的一个按钮再加上两个绑定命令, 主要功能有生成索引, 背单词, 默写单词, 添加双链等, 其中单词文件使用 OpenText 中的单词库, 而背单词的核心逻辑是 SuperMemo 2 间隔重复算法. 本插件完全依赖单词文件及其属性, 没有外部存储记录回答历史, 所以不能做一些记忆曲线拟合, 单词调度看起来很简单朴素, 仅依靠上次回答记录. 插件启动时会读取所有单词缓存, 更改时会即时写入相应的单词文件并更新缓存. 插件没有设计过多的搜索功能, 提倡使用 Obsidian 内置的搜索功能. 如果您认为本插件有帮助, 欢迎微信赞赏支持. 目前等待 Obsidian Bases 正式上线后进行功能更新
|
||||
|
||||

|
||||
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
### 掌握列表
|
||||
|
||||
掌握列表能够即时快速修改单词的 `掌握` 属性, 首先将自己无比熟悉的单词设置为掌握, 例如 hello, that, my 等, 可以防止它们出现在后续功能中
|
||||
掌握列表能够即时快速修改单词的 `掌握` 属性, 可以首先将自己无比熟悉的单词设置为掌握, 例如 hello, that, my 等, 可以防止它们出现在后续功能中, 到后期再将新学会的单词设置为掌握
|
||||
|
||||

|
||||
|
||||
|
|
@ -53,19 +53,33 @@
|
|||
|
||||
### 背单词
|
||||
|
||||
背单词目标是尽可能的提高每个单词的重复次数和易记因子, 详细计算过程可查阅 SM-2 文档. 新词指重复次数为 0 的单词, 它可能是从未被学习过的, 也可能是前面学后面忘了的. 新词调度中 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机; 旧词指重复次数大于 0 的单词, 它是之前回答正确的. 旧词调度从已过期的单词中 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机. 回答的 0-5 分已与数字键绑定方便键盘控制
|
||||
背单词目标是尽可能的提高每个单词的重复次数和易记因子, 详细计算过程可查阅 SM-2 文档, 随机调度概率设置每次抽卡时随机调度的概率, 其余为优先调度概率. 默认 70 % 随机调度, 30% 优先调度. `0-5` 数字键评分, `Tab` 键显示答案
|
||||
- 新词指重复次数为 0 的单词, 它可能是从未被学习过的, 也可能是前面学后面忘了的. 新词间隔只能是 0 或 1, 其中 0 是完全没有学习过的, 1 是至少学过一次的. 新词优先调度遵从间隔降序和易记因子升序, 从前 1% 中随机
|
||||
- 旧词指重复次数大于 0 的单词, 它是之前回答正确的. 旧词优先调度遵从过期的易记因子和重复次数升序, 从前 1% 中随机
|
||||
|
||||
回答评分, 答错降低易记因子, 重复次数和间隔重置, 答对易记因子增长缓慢, 间隔逐渐延长
|
||||
- 评分 0, 易记因子 -0.8
|
||||
- 评分 1, 易记因子 -0.54
|
||||
- 评分 2, 易记因子 -0.32
|
||||
- 评分 3, 易记因子 -0.14
|
||||
- 评分 4, 易记因子 -0
|
||||
- 评分 5, 易记因子 +0.1
|
||||
|
||||

|
||||
|
||||
### 默写单词
|
||||
|
||||
默写单词是从作用域中随机抽取单词进行拼写, 目前只是一个练习功能, 不会更改任何单词属性, 按下 `Tab` 键显示答案
|
||||
默写单词是从易记因子最低的 50% 旧词中随机抽取单词进行拼写, 会影响易记因子, 按下 `Tab` 键显示答案
|
||||
- 没有看答案, 直接拼写正确, 易记因子 + 0.15
|
||||
- 没有看答案, 经过几次错误后拼写正确, 易记因子 + 0.05
|
||||
- 看答案, 易记因子 - 0.02
|
||||
- 易记因子最低不低于 1.3
|
||||
|
||||

|
||||
|
||||
### 添加双链
|
||||
|
||||
为当前活动页面笔记中的单词添加双链, 单词范围是作用域中易记因子小于等于 2.5 的, 所以默认情况所有单词都会添加. 词形还原准确率在 90 %. 需要文章尽可能规范, 减少不必要的标点符号. 目前只能通过 `ctrl z` 回撤, 请谨慎使用
|
||||
为当前活动页面笔记中的单词添加双链, 单词范围是作用域中易记因子小于等于设置的参数, 所以默认情况所有单词都会添加. 词形还原准确率在 90 %. 需要文章尽可能规范, 减少不必要的标点符号. 可以将参数设为 1.2 进行还原, 因为没有单词易记因子小于 1.3
|
||||
|
||||

|
||||
|
||||
|
|
@ -118,4 +132,4 @@ function supermemo(item, grade) {
|
|||
};
|
||||
}
|
||||
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "openwords",
|
||||
"name": "OpenWords",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"minAppVersion": "1.8.10",
|
||||
"description": "用于英语学习中背单词与单词管理的插件",
|
||||
"author": "insile",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "openwords",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "用于英语学习中背单词与单词管理的插件",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
10
src/main.ts
10
src/main.ts
|
|
@ -18,6 +18,8 @@ interface OpenWordsSettings {
|
|||
enableWords6: boolean;
|
||||
enableWords7: boolean;
|
||||
enableWords8: boolean;
|
||||
randomRatio: number;
|
||||
maxEfactorForLink: number;
|
||||
}
|
||||
|
||||
// 默认设置
|
||||
|
|
@ -32,6 +34,8 @@ const DEFAULT_SETTINGS: OpenWordsSettings = {
|
|||
enableWords6: false,
|
||||
enableWords7: false,
|
||||
enableWords8: false,
|
||||
randomRatio: 0.7,
|
||||
maxEfactorForLink: 2.6,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -257,13 +261,9 @@ export default class OpenWords extends Plugin {
|
|||
// 建立单词双链
|
||||
async addDoubleBrackets() {
|
||||
const unmasteredWords = new Set(Array.from(this.enabledCards.values())
|
||||
.filter(card => card.efactor <= 2.5)
|
||||
.filter(card => card.efactor <= this.settings.maxEfactorForLink) // 只选择易记因子小于等于设置值的单词
|
||||
.map(card => card.front)
|
||||
);
|
||||
if (unmasteredWords.size === 0) {
|
||||
new Notice("没有单词需要添加双链!");
|
||||
return;
|
||||
}
|
||||
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (activeView) {
|
||||
|
|
|
|||
|
|
@ -21,75 +21,22 @@ export class LearningModal extends Modal {
|
|||
}
|
||||
|
||||
onOpen() {
|
||||
// 加载组件并选卡
|
||||
this.component.load()
|
||||
this.pickNextCard();
|
||||
if (!this.currentCard) return;
|
||||
|
||||
// 创建卡片容器和设置容器
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
if (!this.currentCard) return;
|
||||
|
||||
// 创建卡片容器并固定样式
|
||||
const cardContainer = contentEl.createDiv({ cls: 'openwords-card' });
|
||||
const settingsContainer = contentEl.createDiv({ cls: 'openwords-card-settings' });
|
||||
this.renderSettings(settingsContainer, cardContainer);
|
||||
this.renderCard(cardContainer);
|
||||
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 0: 回答错误, 完全不会`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('0')
|
||||
.onClick(() => this.rateCard(0 as SuperMemoGrade, cardContainer)))
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 1: 回答错误, 看到正确答案后感觉很熟悉`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('1')
|
||||
.onClick(() => this.rateCard(1 as SuperMemoGrade, cardContainer)))
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 2: 回答错误, 看到正确答案后感觉很容易记住`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('2')
|
||||
.onClick(() => this.rateCard(2 as SuperMemoGrade, cardContainer)))
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 3: 回答正确, 需要花费很大力气才能回忆起来`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('3')
|
||||
.onClick(() => this.rateCard(3 as SuperMemoGrade, cardContainer)))
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 4: 回答正确, 需要经过一番犹豫才做出反应`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('4')
|
||||
.onClick(() => this.rateCard(4 as SuperMemoGrade, cardContainer)))
|
||||
new Setting(settingsContainer)
|
||||
.setName(`评分 5: 回答正确, 完美响应`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText('5')
|
||||
.onClick(() => this.rateCard(5 as SuperMemoGrade, cardContainer)))
|
||||
this.render(cardContainer);
|
||||
|
||||
// 监听数字键 0-5 的按键事件
|
||||
const handleKeydown = async (event: KeyboardEvent) => {
|
||||
if (event.key >= '0' && event.key <= '5') {
|
||||
if (this.isRating) return;
|
||||
this.isRating = true;
|
||||
this.currentRatingKey = event.key
|
||||
const grade = parseInt(event.key) as SuperMemoGrade;
|
||||
await this.plugin.updateCard(this.currentCard, grade);
|
||||
}
|
||||
};
|
||||
const handleKeyup = async (event: KeyboardEvent) => {
|
||||
if (
|
||||
this.isRating &&
|
||||
this.currentRatingKey !== null &&
|
||||
event.key === this.currentRatingKey
|
||||
) {
|
||||
this.isRating=false
|
||||
this.currentRatingKey = null
|
||||
this.pickNextCard()
|
||||
this.render(cardContainer)
|
||||
}
|
||||
};
|
||||
|
||||
this.plugin.registerDomEvent(contentEl, 'keydown', handleKeydown);
|
||||
this.plugin.registerDomEvent(contentEl, 'keyup', handleKeyup);
|
||||
|
||||
// 注册数字键和回车按键事件
|
||||
this.registerRatingKeyEvents(contentEl, cardContainer);
|
||||
this.registerRenderKeyEvents(cardContainer);
|
||||
}
|
||||
|
||||
pickNextCard() {
|
||||
|
|
@ -104,6 +51,15 @@ export class LearningModal extends Modal {
|
|||
new Notice("已完成所有新词!");
|
||||
return;
|
||||
}
|
||||
// 新词排序:间隔降序,间隔相同则易记因子升序
|
||||
pool.sort((a, b) => {
|
||||
const intervalA = Number(a.interval);
|
||||
const intervalB = Number(b.interval);
|
||||
if (intervalA !== intervalB) return intervalB - intervalA; // 降序
|
||||
const efactorA = Number(a.efactor);
|
||||
const efactorB = Number(b.efactor);
|
||||
return efactorA - efactorB; // 升序
|
||||
});
|
||||
} else {
|
||||
pool = Array.from(this.plugin.dueCards.values())
|
||||
.filter(card => window.moment(card.dueDate).isBefore(now)) // 筛选已过期的单词
|
||||
|
|
@ -112,40 +68,110 @@ export class LearningModal extends Modal {
|
|||
new Notice("没有需要复习的卡片!");
|
||||
return;
|
||||
}
|
||||
// 旧词排序:易记因子升序,易记因子相同则重复次数升序
|
||||
pool.sort((a, b) => {
|
||||
const efactorA = Number(a.efactor);
|
||||
const efactorB = Number(b.efactor);
|
||||
if (efactorA !== efactorB) return efactorA - efactorB; // 升序
|
||||
const repetitionA = Number(a.repetition);
|
||||
const repetitionB = Number(b.repetition);
|
||||
return repetitionA - repetitionB; // 升序
|
||||
});
|
||||
}
|
||||
// 单词调度 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机
|
||||
const randomMode = Math.random() < 0.7;
|
||||
if (randomMode) {
|
||||
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
|
||||
} else {
|
||||
const sortedPool = pool.sort((a, b) => {
|
||||
const efactorDiff = (a.efactor || 1) - (b.efactor || 1); // 易记因子升序
|
||||
if (efactorDiff !== 0) return efactorDiff;
|
||||
return a.repetition - b.repetition; // 重复次数升序
|
||||
});
|
||||
const halfPool = sortedPool.slice(0, Math.ceil(sortedPool.length / 5)); // 取前 20%
|
||||
this.currentCard = halfPool[Math.floor(Math.random() * halfPool.length)];
|
||||
|
||||
// 单词调度 70% 纯随机, 30% 从排序后前 1% 中随机
|
||||
const randomMode = Math.random() < this.plugin.settings.randomRatio;
|
||||
if (randomMode) {
|
||||
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
|
||||
} else {
|
||||
const topN = Math.max(1, Math.ceil(pool.length / 100));
|
||||
const topPool = pool.slice(0, topN);
|
||||
this.currentCard = topPool[Math.floor(Math.random() * topPool.length)];
|
||||
}
|
||||
}
|
||||
|
||||
renderSettings(settingsContainer: HTMLElement, cardContainer: HTMLDivElement) {
|
||||
const grades: { grade: SuperMemoGrade, label: string }[] = [
|
||||
{ grade: 0, label: '评分 0: 回答错误, 完全不会' },
|
||||
{ grade: 1, label: '评分 1: 回答错误, 看到正确答案后感觉很熟悉' },
|
||||
{ grade: 2, label: '评分 2: 回答错误, 看到正确答案后感觉很容易记住' },
|
||||
{ grade: 3, label: '评分 3: 回答正确, 需要花费很大力气才能回忆起来' },
|
||||
{ grade: 4, label: '评分 4: 回答正确, 需要经过一番犹豫才做出反应' },
|
||||
{ grade: 5, label: '评分 5: 回答正确, 完美响应' },
|
||||
];
|
||||
for (const { grade, label } of grades) {
|
||||
new Setting(settingsContainer)
|
||||
.setName(label)
|
||||
.addButton(btn => btn
|
||||
.setButtonText(String(grade))
|
||||
.onClick(() => this.rateCard(grade, cardContainer)));
|
||||
}
|
||||
}
|
||||
|
||||
render(cardContainer: HTMLDivElement) {
|
||||
cardContainer.empty()
|
||||
|
||||
// 标记当前是否显示 Markdown 内容
|
||||
let isShowingMarkdown = false;
|
||||
// 初始显示单词
|
||||
renderCard(cardContainer: HTMLDivElement) {
|
||||
cardContainer.empty();
|
||||
const wordContent = cardContainer.createDiv({ cls: 'openwords-card-content' });
|
||||
wordContent.textContent = this.currentCard.front;
|
||||
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
|
||||
if (!file) {return;}
|
||||
const fileCache = this.app.metadataCache.getFileCache(file);
|
||||
const frontMatter = fileCache?.frontmatter;
|
||||
if (!frontMatter) {return;}
|
||||
const tags: string[] = (frontMatter.tags || []).map((tag: string) => {
|
||||
const parts = tag.split('/');
|
||||
return parts.length > 1 ? parts[1] : tag;
|
||||
});
|
||||
const dueDate: string = frontMatter["到期日"]
|
||||
const interval: string = frontMatter["间隔"]
|
||||
const efactor: string = frontMatter["易记因子"]
|
||||
const repetition: string = frontMatter["重复次数"]
|
||||
|
||||
// 添加鼠标悬浮事件切换内容
|
||||
cardContainer.addEventListener('mouseenter', async () => {
|
||||
if (isShowingMarkdown) return; // 如果已经显示 Markdown 内容,则不重复加载
|
||||
const metaDiv = cardContainer.createDiv({ cls: 'openwords-card-meta' });
|
||||
|
||||
// 第一行:标签
|
||||
const tagsDiv = metaDiv.createDiv({ cls: 'openwords-card-meta-tags' });
|
||||
tagsDiv.textContent = `标签: ${tags.join(', ')}`;
|
||||
|
||||
// 第二行:其余元数据
|
||||
const infoDiv = metaDiv.createDiv({ cls: 'openwords-card-meta-info' });
|
||||
infoDiv.textContent = `易记因子: ${efactor} | 重复次数: ${repetition} | 到期日: ${dueDate} | 间隔: ${interval} `;
|
||||
}
|
||||
|
||||
registerRatingKeyEvents(contentEl: HTMLElement, cardContainer: HTMLDivElement) {
|
||||
const handleKeydown = async (event: KeyboardEvent) => {
|
||||
if (event.key >= '0' && event.key <= '5') {
|
||||
if (this.isRating) return;
|
||||
this.isRating = true;
|
||||
this.currentRatingKey = event.key;
|
||||
const grade = parseInt(event.key) as SuperMemoGrade;
|
||||
await this.plugin.updateCard(this.currentCard, grade);
|
||||
}
|
||||
};
|
||||
const handleKeyup = async (event: KeyboardEvent) => {
|
||||
if (
|
||||
this.isRating &&
|
||||
this.currentRatingKey !== null &&
|
||||
event.key === this.currentRatingKey
|
||||
) {
|
||||
this.isRating = false;
|
||||
this.currentRatingKey = null;
|
||||
this.pickNextCard();
|
||||
this.renderCard(cardContainer);
|
||||
}
|
||||
};
|
||||
this.plugin.registerDomEvent(contentEl, 'keydown', handleKeydown);
|
||||
this.plugin.registerDomEvent(contentEl, 'keyup', handleKeyup);
|
||||
}
|
||||
|
||||
registerRenderKeyEvents(cardContainer: HTMLDivElement) {
|
||||
// 标记当前是否显示 Markdown 内容
|
||||
let isShowingMarkdown = false;
|
||||
|
||||
const showMarkdown = async () => {
|
||||
if (isShowingMarkdown) return;
|
||||
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
|
||||
if (file instanceof TFile) {
|
||||
const markdownContent = await this.plugin.app.vault.cachedRead(file);
|
||||
|
||||
// 清空卡片内容并渲染 Markdown 内容
|
||||
cardContainer.empty();
|
||||
const markdownRenderContainer = cardContainer.createDiv({ cls: 'openwords-card-markdown' });
|
||||
await MarkdownRenderer.render(
|
||||
|
|
@ -155,24 +181,40 @@ export class LearningModal extends Modal {
|
|||
file.path,
|
||||
this.component
|
||||
);
|
||||
|
||||
isShowingMarkdown = true;
|
||||
} else {
|
||||
new Notice('无法加载单词的 Markdown 文件!');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cardContainer.addEventListener('mouseleave', () => {
|
||||
if (!isShowingMarkdown) return; // 如果已经显示单词内容,则不重复加载
|
||||
|
||||
// 切换回显示单词
|
||||
cardContainer.empty();
|
||||
const wordContent = cardContainer.createDiv({ cls: 'openwords-card-content' });
|
||||
wordContent.textContent = this.currentCard.front;
|
||||
const showWord = () => {
|
||||
if (!isShowingMarkdown) return;
|
||||
this.renderCard(cardContainer);
|
||||
isShowingMarkdown = false;
|
||||
});
|
||||
};
|
||||
|
||||
cardContainer.addEventListener('mouseenter', showMarkdown);
|
||||
cardContainer.addEventListener('mouseleave', showWord);
|
||||
|
||||
// 按下回车显示内容,释放回车显示单词
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
await showMarkdown();
|
||||
}
|
||||
};
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
showWord();
|
||||
}
|
||||
};
|
||||
this.plugin.registerDomEvent(cardContainer, 'keydown', handleKeyDown);
|
||||
this.plugin.registerDomEvent(cardContainer, 'keyup', handleKeyUp);
|
||||
|
||||
// 让卡片容器可聚焦以接收键盘事件
|
||||
cardContainer.tabIndex = 0;
|
||||
cardContainer.focus();
|
||||
}
|
||||
|
||||
async rateCard(grade: SuperMemoGrade, cardContainer: HTMLDivElement) {
|
||||
|
|
@ -180,7 +222,7 @@ export class LearningModal extends Modal {
|
|||
this.isRating = true;
|
||||
await this.plugin.updateCard(this.currentCard, grade);
|
||||
this.pickNextCard();
|
||||
this.render(cardContainer);
|
||||
this.renderCard(cardContainer);
|
||||
this.isRating = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class LearningTypeModal extends Modal {
|
|||
}));
|
||||
|
||||
new Setting(buttonContainer)
|
||||
.setName('默写单词 ( 启用单词 )')
|
||||
.setName('默写单词 ( 重复次数不为零 )')
|
||||
.addButton(btn => btn
|
||||
.setButtonText('开始')
|
||||
.onClick(() => {
|
||||
|
|
@ -68,7 +68,7 @@ export class LearningTypeModal extends Modal {
|
|||
}));
|
||||
|
||||
new Setting(buttonContainer)
|
||||
.setName(`添加双链 ( 启用单词中易记因子 <= 2.5 )`)
|
||||
.setName(`添加双链 ( 启用单词中易记因子 <= ${this.plugin.settings.maxEfactorForLink} )`)
|
||||
.addButton(btn => btn
|
||||
.setButtonText("开始")
|
||||
.onClick(async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Modal, MarkdownRenderer, TFile, Component } from 'obsidian';
|
||||
import { App, Modal, MarkdownRenderer, TFile, Component, Notice } from 'obsidian';
|
||||
import { CardInfo } from '../card';
|
||||
import OpenWords from '../main';
|
||||
|
||||
|
|
@ -8,6 +8,8 @@ export class SpellingModal extends Modal {
|
|||
plugin: OpenWords;
|
||||
component: Component;
|
||||
currentCard: CardInfo | null = null;
|
||||
hasPeeked: boolean = false; // 是否看过答案
|
||||
errorCount: number = 0; // 本轮错误次数
|
||||
|
||||
constructor(app: App, plugin: OpenWords) {
|
||||
super(app);
|
||||
|
|
@ -16,6 +18,7 @@ export class SpellingModal extends Modal {
|
|||
}
|
||||
|
||||
async onOpen() {
|
||||
// 加载组件
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
this.component.load()
|
||||
|
|
@ -34,6 +37,7 @@ export class SpellingModal extends Modal {
|
|||
event.preventDefault(); // 阻止页面滚动
|
||||
if (this.currentCard) {
|
||||
title.setText(`${this.currentCard.front}`); // 显示答案
|
||||
this.hasPeeked = true; // 标记已看答案
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -53,14 +57,19 @@ export class SpellingModal extends Modal {
|
|||
inputContainer: HTMLElement,
|
||||
feedbackContainer: HTMLElement
|
||||
) {
|
||||
const cards = Array.from(this.plugin.enabledCards.values());
|
||||
const cards = Array.from(this.plugin.dueCards.values());
|
||||
if (cards.length === 0) {
|
||||
feedbackContainer.setText('没有更多单词了!');
|
||||
return;
|
||||
}
|
||||
this.hasPeeked = false;
|
||||
this.errorCount = 0;
|
||||
|
||||
// 随机选择一个单词
|
||||
this.currentCard = cards[Math.floor(Math.random() * cards.length)];
|
||||
// 从易记因子最低的50%中随机选择一个单词
|
||||
const sorted = cards.slice().sort((a, b) => a.efactor - b.efactor);
|
||||
const half = Math.ceil(sorted.length / 2);
|
||||
const pool = sorted.slice(0, half);
|
||||
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
|
||||
|
||||
// 渲染单词的词义
|
||||
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
|
||||
|
|
@ -118,7 +127,7 @@ export class SpellingModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
checkSpelling(
|
||||
async checkSpelling(
|
||||
inputFields: HTMLInputElement[],
|
||||
word: string,
|
||||
feedbackContainer: HTMLElement,
|
||||
|
|
@ -129,11 +138,35 @@ export class SpellingModal extends Modal {
|
|||
if (userInput.length === word.length) {
|
||||
if (userInput.toLowerCase() === word.toLowerCase()) {
|
||||
feedbackContainer.setText('正确!');
|
||||
if (this.currentCard) {
|
||||
let efactor = this.currentCard.efactor;
|
||||
if (this.hasPeeked) {
|
||||
efactor -= 0.02;
|
||||
} else if (this.errorCount === 0) {
|
||||
efactor += 0.15;
|
||||
} else {
|
||||
efactor += 0.05;
|
||||
}
|
||||
if (efactor < 1.3) efactor = 1.3;
|
||||
this.currentCard.efactor = efactor;
|
||||
|
||||
// 同步到 frontmatter
|
||||
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
|
||||
if (file instanceof TFile) {
|
||||
await this.plugin.app.fileManager.processFrontMatter(file, (frontMatter) => {
|
||||
frontMatter["易记因子"] = Math.round(efactor * 100);
|
||||
});
|
||||
}
|
||||
|
||||
new Notice(`${this.currentCard.front} \n易记因子: ${efactor.toFixed(2)} \n重复次数: ${this.currentCard.repetition} \n间隔: ${this.currentCard.interval} \n到期日: ${this.currentCard.dueDate}`);
|
||||
|
||||
}
|
||||
setTimeout(async () => {
|
||||
await this.pickNextCard(wordMeaningContainer, inputContainer, feedbackContainer);
|
||||
}, 500);
|
||||
} else {
|
||||
feedbackContainer.setText('错误,请重试!');
|
||||
this.errorCount += 1;
|
||||
inputFields.forEach((field) => (field.value = '')); // 清空所有输入框
|
||||
inputFields[0].focus(); // 聚焦第一个输入框
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,34 @@ export class OpenWordsSettingTab extends PluginSettingTab {
|
|||
button.setButtonText('生成'); // 恢复按钮文本
|
||||
}
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName("随机调度概率")
|
||||
.setDesc("设置每次抽卡时随机调度的概率,其余为优先调度概率")
|
||||
.addDropdown(dropdown => {
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
const value = (i / 10).toFixed(1);
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.plugin.settings.randomRatio.toFixed(1));
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.randomRatio = Number(value);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("最大双链因子")
|
||||
.setDesc("易记因子小于等于该值的单词可添加双链")
|
||||
.addDropdown(dropdown => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const value = (1.2 + i * 0.2).toFixed(1);
|
||||
dropdown.addOption(value, value);
|
||||
}
|
||||
dropdown.setValue(this.plugin.settings.maxEfactorForLink.toFixed(1));
|
||||
dropdown.onChange(async (value) => {
|
||||
this.plugin.settings.maxEfactorForLink = Number(value);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName('重置单词属性')
|
||||
.setDesc('重置作用域单词的易记因子、重复次数、间隔和到期日')
|
||||
|
|
|
|||
11
styles.css
11
styles.css
|
|
@ -36,6 +36,7 @@ If your plugin does not need CSS, delete this file.
|
|||
width: 500px; /* 固定宽度 */
|
||||
height: 300px; /* 固定高度 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -50,9 +51,17 @@ If your plugin does not need CSS, delete this file.
|
|||
}
|
||||
|
||||
.openwords-card-content { /* 单词内容样式 */
|
||||
font-size: 2.5em; /* 设置单词字体大小 */
|
||||
font-size: 3em; /* 设置单词字体大小 */
|
||||
font-weight: bold; /* 设置单词加粗 */
|
||||
color: var(--text-normal); /* 设置单词颜色 */
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.openwords-card-meta {
|
||||
font-size: 0.85em;
|
||||
line-height: 1.5;
|
||||
margin-top: 24px;
|
||||
|
||||
}
|
||||
|
||||
.openwords-card-markdown { /* Markdown 内容样式 */
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.5": "1.8.10"
|
||||
"1.0.6": "1.8.10"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue