mirror of
https://github.com/insile/OpenWords.git
synced 2026-07-22 05:48:39 +00:00
228 lines
9.5 KiB
TypeScript
228 lines
9.5 KiB
TypeScript
import { Setting, Notice, MarkdownRenderer, TFile } from 'obsidian';
|
|
import { SuperMemoGrade } from 'supermemo';
|
|
import { CardInfo } from '../utils/Card';
|
|
import { MainView } from './MainView';
|
|
import { asArray, asDateString, asNumber } from 'utils/Converters';
|
|
|
|
export async function LearningPage(this: MainView) {
|
|
|
|
this.currentLearingCard = await this.pickNextLCard();
|
|
if (!this.currentLearingCard) return;
|
|
|
|
this.viewContainer.empty();
|
|
const learningContainer = this.viewContainer.createDiv({ cls: 'openwords-learning-container' });
|
|
const backBtn = learningContainer.createEl('button', { cls: 'openwords-back-btn' });
|
|
const cardContainer = learningContainer.createDiv({ cls: 'openwords-learning-card' });
|
|
const settingsContainer = learningContainer.createDiv({ cls: 'openwords-learning-settings' });
|
|
|
|
backBtn.textContent = '返回';
|
|
backBtn.onclick = async () => {
|
|
this.page = 'type';
|
|
await this.render();
|
|
};
|
|
|
|
// ESC 快捷键返回
|
|
learningContainer.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
backBtn.click();
|
|
}
|
|
});
|
|
|
|
this.renderCard(cardContainer);
|
|
this.renderSettings(cardContainer, settingsContainer);
|
|
|
|
this.registerRatingKeyEvents(learningContainer, cardContainer);
|
|
this.registerRenderKeyEvents(learningContainer, cardContainer);
|
|
learningContainer.tabIndex = 0;
|
|
learningContainer.focus();
|
|
}
|
|
|
|
// 选卡逻辑
|
|
export async function pickNextLCard(this: MainView) {
|
|
const now = window.moment();
|
|
let pool: CardInfo[];
|
|
if (this.page === 'new') { // 新词排序:间隔降序,间隔相同则易记因子升序
|
|
pool = Array.from(this.plugin.newCards.values());
|
|
if (pool.length === 0) {
|
|
new Notice("已完成所有新词!");
|
|
this.page = 'type';
|
|
await this.render();
|
|
return null;
|
|
}
|
|
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));
|
|
if (pool.length === 0) {
|
|
new Notice("没有需要复习的卡片!");
|
|
this.page = 'type';
|
|
await this.render();
|
|
return null;
|
|
}
|
|
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;
|
|
});
|
|
}
|
|
const randomMode = Math.random() < this.plugin.settings.randomRatio;
|
|
if (randomMode) {
|
|
return pool[Math.floor(Math.random() * pool.length)] ?? null;
|
|
} else {
|
|
const topN = Math.max(1, Math.ceil(pool.length / 100));
|
|
const topPool = pool.slice(0, topN);
|
|
return topPool[Math.floor(Math.random() * topPool.length)] ?? null;
|
|
}
|
|
}
|
|
|
|
// 卡片内容渲染
|
|
export function renderCard(this: MainView, cardContainer: HTMLDivElement) {
|
|
cardContainer.empty();
|
|
if (!this.currentLearingCard) return;
|
|
const wordContent = cardContainer.createDiv({ cls: 'openwords-card-content' });
|
|
wordContent.textContent = this.currentLearingCard.front;
|
|
const file = this.plugin.app.vault.getFileByPath(this.currentLearingCard.path);
|
|
if (!file) { return; }
|
|
const fileCache = this.plugin.app.metadataCache.getFileCache(file);
|
|
const frontMatter = fileCache?.frontmatter;
|
|
if (!frontMatter) { return; }
|
|
const tags: string[] = asArray(frontMatter.tags)
|
|
.map((tag: string) => {
|
|
const parts = tag.split('/');
|
|
return parts.at(-1) ?? tag; // 如果 at(-1) 失败,退回到原标签
|
|
});
|
|
const dueDate: string = asDateString(frontMatter["到期日"]);
|
|
const interval: number = asNumber(frontMatter["间隔"], 0);
|
|
const efactor: number = asNumber(frontMatter["易记因子"], 0);
|
|
const repetition: number = asNumber(frontMatter["重复次数"], 0);
|
|
|
|
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} `;
|
|
}
|
|
|
|
// 评分按钮渲染
|
|
export function renderSettings(this: MainView, cardContainer: HTMLDivElement, settingsContainer: HTMLElement) {
|
|
const grades: { grade: SuperMemoGrade, label: string }[] = [
|
|
{ grade: 0, label: '评分 1: 回答错误, 完全不会' },
|
|
{ grade: 1, label: '评分 2: 回答错误, 看到正确答案后感觉很熟悉' },
|
|
{ grade: 2, label: '评分 3: 回答错误, 看到正确答案后感觉很容易记住' },
|
|
{ grade: 3, label: '评分 4: 回答正确, 需要花费很大力气才能回忆起来' },
|
|
{ grade: 4, label: '评分 5: 回答正确, 需要经过一番犹豫才做出反应' },
|
|
{ grade: 5, label: '评分 6: 回答正确, 完美响应' },
|
|
];
|
|
for (const { grade, label } of grades) {
|
|
new Setting(settingsContainer)
|
|
.setName(label)
|
|
.addButton(btn => {
|
|
btn.setButtonText(String(grade + 1));
|
|
btn.buttonEl.setAttribute('data-grade', String(grade + 1));
|
|
btn.onClick(() => this.rateCard(grade, cardContainer));
|
|
});
|
|
}
|
|
}
|
|
|
|
// 评分逻辑
|
|
export async function rateCard(this: MainView, grade: SuperMemoGrade, cardContainer: HTMLDivElement) {
|
|
if (this.isRating || !this.currentLearingCard) return;
|
|
this.isRating = true;
|
|
await this.plugin.updateCard(this.currentLearingCard, grade, this.page);
|
|
this.currentLearingCard = await this.pickNextLCard();
|
|
this.renderCard(cardContainer);
|
|
this.isRating = false;
|
|
}
|
|
|
|
// 数字键评分
|
|
export function registerRatingKeyEvents(this: MainView, learningContainer: HTMLDivElement, cardContainer: HTMLDivElement) {
|
|
const handleKeydown = async (event: KeyboardEvent) => {
|
|
if (event.key >= '1' && event.key <= '6') {
|
|
if (this.isRating || !this.currentLearingCard) return;
|
|
this.isRating = true;
|
|
this.currentRatingKey = event.key;
|
|
const btn = learningContainer.querySelector(`button[data-grade="${event.key}"]`);
|
|
if (btn) btn.classList.add('active');
|
|
const grade = parseInt(event.key) - 1 as SuperMemoGrade;
|
|
await this.plugin.updateCard(this.currentLearingCard, grade, this.page);
|
|
}
|
|
};
|
|
const handleKeyup = async (event: KeyboardEvent) => {
|
|
if (
|
|
this.isRating &&
|
|
this.currentRatingKey !== null &&
|
|
event.key === this.currentRatingKey
|
|
) {
|
|
const btn = learningContainer.querySelector(`button[data-grade="${event.key}"]`);
|
|
if (btn) btn.classList.remove('active');
|
|
this.isRating = false;
|
|
this.currentRatingKey = null;
|
|
this.currentLearingCard = await this.pickNextLCard();
|
|
this.renderCard(cardContainer);
|
|
}
|
|
};
|
|
this.plugin.registerDomEvent(learningContainer, 'keydown', handleKeydown);
|
|
this.plugin.registerDomEvent(learningContainer, 'keyup', handleKeyup);
|
|
}
|
|
|
|
// 答案渲染逻辑
|
|
export function registerRenderKeyEvents(this: MainView, learningContainer: HTMLDivElement, cardContainer: HTMLDivElement) {
|
|
let isShowingMarkdown = false;
|
|
const showMarkdown = async () => {
|
|
if (isShowingMarkdown || !this.currentLearingCard) return;
|
|
const file = this.plugin.app.vault.getFileByPath(this.currentLearingCard.path);
|
|
if (file instanceof TFile) {
|
|
const markdownContent = await this.plugin.app.vault.cachedRead(file);
|
|
cardContainer.empty();
|
|
const markdownRenderContainer = cardContainer.createDiv({ cls: 'openwords-card-markdown' });
|
|
await MarkdownRenderer.render(
|
|
this.plugin.app,
|
|
markdownContent,
|
|
markdownRenderContainer,
|
|
file.path,
|
|
this.component
|
|
);
|
|
isShowingMarkdown = true;
|
|
} else {
|
|
new Notice('无法加载单词的 Markdown 文件!');
|
|
}
|
|
};
|
|
const showWord = () => {
|
|
if (!isShowingMarkdown) return;
|
|
this.renderCard(cardContainer);
|
|
isShowingMarkdown = false;
|
|
};
|
|
|
|
// 延迟1秒后注册鼠标事件
|
|
setTimeout(() => {
|
|
cardContainer.addEventListener('mouseenter', () => { void showMarkdown(); });
|
|
cardContainer.addEventListener('mouseleave', showWord);
|
|
}, 0);
|
|
|
|
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(learningContainer, 'keydown', handleKeyDown);
|
|
this.plugin.registerDomEvent(learningContainer, 'keyup', handleKeyUp);
|
|
|
|
}
|
|
|