diff --git a/README.md b/README.md index f228de9..f13d39d 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,10 @@ ![p10](README/10.gif) +为当前活动页面选中的单词添加双链, 命令调用或者右键菜单 + +![p13](README/13.gif) + ### 恢复默认设置 重置所有设置选项为默认值, 请谨慎使用 diff --git a/README/13.gif b/README/13.gif new file mode 100644 index 0000000..3e03fdf Binary files /dev/null and b/README/13.gif differ diff --git a/manifest.json b/manifest.json index b94c0ad..3c29dd2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "openwords", "name": "OpenWords", - "version": "1.0.10", + "version": "1.0.11", "minAppVersion": "1.10.0", "description": "用于英语学习中背单词与单词管理的插件", "author": "insile", diff --git a/package-lock.json b/package-lock.json index 883f41c..4ed559f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openwords", - "version": "1.0.8", + "version": "1.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openwords", - "version": "1.0.8", + "version": "1.0.11", "license": "MIT", "dependencies": { "obsidian": "latest", diff --git a/package.json b/package.json index ab6585e..37d528c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openwords", - "version": "1.0.10", + "version": "1.0.11", "description": "用于英语学习中背单词与单词管理的插件", "main": "main.js", "type": "module", diff --git a/src/main.ts b/src/main.ts index 46bc1fb..45a8a95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,13 +5,14 @@ import { MainView, MAIN_VIEW, PageType } from './views/MainView'; import { CardInfo } from './utils/Card'; import { supermemo, SuperMemoGrade } from 'supermemo'; import posTagger from 'wink-pos-tagger'; +import { asArray, asNumber, asDateString } from 'utils/Converters'; // 插件主类 export default class OpenWords extends Plugin { settings: OpenWordsSettings; // 插件设置 settingsSnapshot: OpenWordsSettings; // 插件设置快照 - tagger: posTagger = new posTagger(); // 词性标注器实例 + tagger: posTagger = new posTagger(); // 词性标注器实例 allCards: Map = new Map(); // 所有单词 masterCards: Map = new Map(); // 掌握单词 enabledCards: Map = new Map(); // 启用单词 @@ -26,6 +27,7 @@ export default class OpenWords extends Plugin { // 加载插件设置参数, 设置选项卡, 左侧工具栏按钮, 注册主视图 await this.loadSettings(); this.addSettingTab(new OpenWordsSettingTab(this.app, this)); + // eslint-disable-next-line obsidianmd/ui/sentence-case this.addRibbonIcon('slack', 'OpenWords', async () => { await this.activateView(); }); this.registerView(MAIN_VIEW, (leaf) => new MainView(leaf, this)); @@ -35,18 +37,43 @@ export default class OpenWords extends Plugin { this.addCommand({ id: 'LearningTypeModal', name: '学习视图', - callback: () => { - this.activateView(); + callback: async () => { + await this.activateView(); } }); // 添加双链命令 this.addCommand({ id: 'addDoubleBrackets', - name: '添加双链', - editorCallback: () => { - this.addDoubleBrackets(); + name: '文档双链', + editorCallback: async () => { + await this.addDoubleBrackets(); } }); + // 添加单词双链命令 + this.addCommand({ + id: 'addDoubleBracketsWord', + name: '单词双链', + editorCallback: async () => { + await this.autoDoubleLinkWord(); + } + }); + // 注册编辑器菜单 + this.registerEvent( + this.app.workspace.on("editor-menu", (menu, editor, view) => { + const selection = editor.getSelection().trim(); + if (!selection) return; + + menu.addItem((item) => { + item + .setTitle(`链接 "${selection}" 到单词库`) + .setIcon("link") + .onClick(async () => { + // 执行上面提到的搜索和替换逻辑 + await this.autoDoubleLinkWord(); + }); + }); + }) + ); // 注册文件监听器 this.registerFileWatchers(); // 扫描所有单词文件, 更新状态栏 @@ -75,13 +102,13 @@ export default class OpenWords extends Plugin { // 加载单词元数据 async loadWordMetadata(file: TFile, single = true) { const allCards = this.allCards; - const masterCards = this.masterCards; + const masterCards = this.masterCards; const enabledCards = this.enabledCards; const newCards = this.newCards; const dueCards = this.dueCards; // 先删除旧的单词元数据 - masterCards.delete(file.basename); + masterCards.delete(file.basename); enabledCards.delete(file.basename); newCards.delete(file.basename); dueCards.delete(file.basename); @@ -95,45 +122,31 @@ export default class OpenWords extends Plugin { allCards.delete(file.basename); if (single) { this.updateStatusBar(); } return; - } + } - const tags: string[] = frontMatter.tags || []; + // 解析 FrontMatter 中的单词属性 + const tags: string[] = asArray(frontMatter.tags); const isMastered = frontMatter["掌握"] === true; const isTagged = tags.some(tag => this.settings.enabledTags.includes(tag)); const card: CardInfo = { front: file.basename, path: file.path, - dueDate: frontMatter["到期日"], - interval: frontMatter["间隔"], - efactor: frontMatter["易记因子"] * 0.01, - repetition: frontMatter["重复次数"], + dueDate: asDateString(frontMatter["到期日"]), + interval: asNumber(frontMatter["间隔"], 0), + efactor: asNumber(frontMatter["易记因子"], 250) * 0.01, + repetition: asNumber(frontMatter["重复次数"], 0), isMastered: isMastered, }; - if ( - !card.front || - !card.path || - card.dueDate === undefined || - card.interval === undefined || - card.efactor === undefined || - card.repetition === undefined || - Number.isNaN(card.efactor) || - Number.isNaN(card.interval) || - Number.isNaN(card.repetition) - ) { - allCards.delete(file.basename); - if (single) { this.updateStatusBar(); } - return; + + // 更新所有单词 + allCards.set(card.front, card); + + // 更新掌握单词 + if (isMastered) { + masterCards.set(card.front, card); } - // 更新所有单词 - allCards.set(card.front, card); - - // 更新掌握单词 - if (isMastered) { - masterCards.set(card.front, card); - } - // 更新启用单词, 新单词, 旧单词 if (isTagged && !isMastered) { enabledCards.set(card.front, card); @@ -151,19 +164,19 @@ export default class OpenWords extends Plugin { // 扫描所有单词文件 async scanAllNotes() { this.allCards.clear(); - this.masterCards.clear(); + this.masterCards.clear(); this.enabledCards.clear(); this.newCards.clear(); this.dueCards.clear(); - const normalized = this.settings.folderPath.endsWith("/") - ? this.settings.folderPath - : this.settings.folderPath + "/"; + const normalized = this.settings.folderPath.endsWith("/") + ? this.settings.folderPath + : this.settings.folderPath + "/"; const files = this.app.vault.getMarkdownFiles(); const filteredFiles = files.filter(file => file.path.startsWith(normalized)); if (filteredFiles.length === 0) { new Notice('指定的文件夹下没有 Markdown 文件!'); - this.updateStatusBar() + this.updateStatusBar() return; } @@ -186,28 +199,38 @@ export default class OpenWords extends Plugin { const newDate = window.moment().add(result.interval, 'day').format('YYYY-MM-DD'); const file = this.app.vault.getFileByPath(card.path); - if (!file) { + if (!file) { new Notice(`文件 ${card.path} 不存在!`); return; } - await this.app.fileManager.processFrontMatter(file, (frontMatter) => { + await this.app.fileManager.processFrontMatter(file, (frontMatter: Record) => { frontMatter["到期日"] = newDate; frontMatter["间隔"] = result.interval; frontMatter["易记因子"] = Math.round(result.efactor * 100); frontMatter["重复次数"] = result.repetition; }); - new Notice(`${card.front} \n易记因子: ${result.efactor.toFixed(2)} \n重复次数: ${result.repetition} \n间隔: ${result.interval} \n到期日: ${newDate}`); + new Notice(`${card.front} \n易记因子: ${result.efactor.toFixed(2)} \n重复次数: ${result.repetition} \n间隔: ${result.interval} \n到期日: ${newDate}`); // 等待元数据缓存更新(最多等待 1 秒) let attempts = 0; - while (attempts < 5) { - const updated = this.app.metadataCache.getFileCache(file); - if (updated?.frontmatter?.["到期日"] === newDate) { - break; // 缓存已更新 + while (attempts < 20) { + if (mode === 'new') { + if (grade >= 3 && this.dueCards.has(card.front)) { + break; // 以移出新单词池 + } else if (grade < 3) { + break; + } } - await new Promise(resolve => setTimeout(resolve, 200)); + else { + if (grade < 3 && this.newCards.has(card.front)) { + break; // 以移出旧单词池 + } else if (grade >= 3 && this.dueCards.get(card.front)?.efactor === result.efactor) { + break; + } + } + await new Promise(resolve => setTimeout(resolve, 50)); attempts++; } } @@ -218,10 +241,10 @@ export default class OpenWords extends Plugin { new Notice("没有单词需要重置!"); return; } - + // // 注销监听器,避免重置过程中触发大量缓存更新事件 // this.unregisterFileWatchers(); - + const notice = new Notice('重置中...', 0); // 创建一个持续显示的 Notice let count = 0; // 计数器 const cardList = Array.from(this.enabledCards.values()); // 将 Map 转换为数组 @@ -229,7 +252,7 @@ export default class OpenWords extends Plugin { const concurrency = 10; // 并发处理数量 const todayDate = window.moment().format('YYYY-MM-DD'); // 提前计算日期,避免重复计算 let updateFrequency = Math.max(1, Math.floor(total / 50)); // 减少更新频率到 2% - + // 创建并发处理任务 const processTasks = async () => { for (let i = 0; i < cardList.length; i += concurrency) { @@ -237,7 +260,7 @@ export default class OpenWords extends Plugin { await Promise.all(chunk.map(async (card) => { const file = this.app.vault.getFileByPath(card.path); if (file) { - await this.app.fileManager.processFrontMatter(file, (frontMatter) => { + await this.app.fileManager.processFrontMatter(file, (frontMatter: Record) => { frontMatter["到期日"] = todayDate; frontMatter["间隔"] = 0; frontMatter["易记因子"] = 250; @@ -257,7 +280,8 @@ export default class OpenWords extends Plugin { await processTasks(); notice.setMessage(`重置完成!共 ${count} 个单词`); } catch (error) { - new Notice(`重置出错: ${error}`); + const message = error instanceof Error ? error.message : String(error); + new Notice(`重置出错: ${message}`); console.error('Reset card error:', error); } finally { // // 重新注册监听器 @@ -268,7 +292,7 @@ export default class OpenWords extends Plugin { setTimeout(() => notice.hide(), 2000); // 2 秒后自动隐藏 Notice } } - + // 建立单词双链 async addDoubleBrackets() { const unmasteredWords = new Set(Array.from(this.enabledCards.values()) @@ -307,7 +331,7 @@ export default class OpenWords extends Plugin { return word; // 否则返回原始单词 } }); - const updatedContent = updatedWords.join(''); + const updatedContent = updatedWords.join(''); doc.setValue(updatedContent); // 用更新后的内容替换原文 new Notice('已添加双链!'); @@ -317,7 +341,7 @@ export default class OpenWords extends Plugin { // 生成单词索引 .base 文件 async generateIndex() { const notice = new Notice('索引中...', 0); - + // 从设置读取标签并转换为配置对象(将多级标签的 '/' 替换为 '.') const levelConfigs = this.settings.enabledTags.map(tag => { const name = tag.replace(/\//g, '.'); @@ -352,10 +376,11 @@ export default class OpenWords extends Plugin { const frontMatter = this.app.metadataCache.getFileCache(file)?.frontmatter; if (!frontMatter) return; - const tags: string[] = frontMatter.tags || []; + const tags: string[] = asArray(frontMatter.tags); for (const level of levelConfigs) { if (tags.includes(level.tag)) { - levelWordCounts[level.tag] = (levelWordCounts[level.tag] ?? 0) + 1; } + levelWordCounts[level.tag] = (levelWordCounts[level.tag] ?? 0) + 1; + } } }); @@ -409,8 +434,8 @@ views: - 间隔 - 到期日 ${Array.from({ length: 26 }, (_, i) => { - const letter = String.fromCharCode(65 + i); - return ` - type: table + const letter = String.fromCharCode(65 + i); + return ` - type: table name: 总计.${letter} filters: and: @@ -422,7 +447,7 @@ ${Array.from({ length: 26 }, (_, i) => { - 重复次数 - 间隔 - 到期日`; -}).join('\n')} + }).join('\n')} `; await this.writeFile(allBasePath, allBaseContent); @@ -446,8 +471,8 @@ views: - 间隔 - 到期日 ${Array.from({ length: 26 }, (_, i) => { - const letter = String.fromCharCode(65 + i); - return ` - type: table + const letter = String.fromCharCode(65 + i); + return ` - type: table name: ${level.name.split('.').pop()}.${letter} filters: and: @@ -459,7 +484,7 @@ ${Array.from({ length: 26 }, (_, i) => { - 重复次数 - 间隔 - 到期日`; -}).join('\n')} + }).join('\n')} `; await this.writeFile(basePath, baseContent); @@ -598,12 +623,36 @@ ${tagFilters} await this.writeFile(basePath, baseContent); } + // 自动为选中单词添加双链 + async autoDoubleLinkWord() { + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + if (!activeView) { + new Notice("请在 Markdown 编辑器中使用此命令"); + return; + } + const editor = activeView.editor; + const selection = editor.getSelection().trim(); + if (!selection) { + new Notice("请先选中一个单词"); + return; + } + const word = this.tagger.tagSentence(selection.toLowerCase())[0]; + const lemma = word?.lemma ?? selection.toLowerCase(); + if (this.allCards.has(lemma)) { + editor.replaceSelection(`[[${lemma}|${selection}]]`); + new Notice(`已成功链接到: ${selection}`); + } else { + // 4. 如果文件不存在,可以提示或提供创建选项 + new Notice(`单词库中未找到 "${selection}"`); + } + } + // 更新状态栏 - updateStatusBar(){ + updateStatusBar() { this.app.workspace.getLeavesOfType(MAIN_VIEW).forEach(leaf => { const view = leaf.view; if (view instanceof MainView) { - void view.updateStatusBar(); + view.updateStatusBar(); } }); } @@ -619,9 +668,9 @@ ${tagFilters} : this.settings.folderPath + "/"; // 监听单词文件创建事件 - const createRef = this.app.vault.on("create", (file: TFile) => { + const createRef = this.app.vault.on("create", async (file: TFile) => { if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) { - this.loadWordMetadata(file); + await this.loadWordMetadata(file); } }); this.fileWatcherRefs.push(createRef); @@ -640,9 +689,9 @@ ${tagFilters} this.fileWatcherRefs.push(deleteRef); // 监听单词文件缓存修改事件 - const changedRef = this.app.metadataCache.on("changed", (file) => { + const changedRef = this.app.metadataCache.on("changed", async (file) => { if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) { - this.loadWordMetadata(file); + await this.loadWordMetadata(file); } }); this.fileWatcherRefs.push(changedRef); @@ -662,13 +711,13 @@ ${tagFilters} this.registerFileWatchers(); } - onunload() { + onunload() { // 清理文件监听器 this.unregisterFileWatchers(); } async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial); } async saveSettings() { diff --git a/src/settings/SettingTab.ts b/src/settings/SettingTab.ts index dda02a7..f02b232 100644 --- a/src/settings/SettingTab.ts +++ b/src/settings/SettingTab.ts @@ -1,4 +1,4 @@ -import { App, Notice, PluginSettingTab, Setting, normalizePath } from 'obsidian'; +import { App, Notice, PluginSettingTab, Setting, TextComponent, normalizePath } from 'obsidian'; import { FolderSuggest, TagHistorySuggest } from '../utils/InputSuggest'; import { DEFAULT_SETTINGS } from './SettingData'; import OpenWords from '../main'; @@ -7,6 +7,7 @@ import OpenWords from '../main'; // 插件设置选项卡 export class OpenWordsSettingTab extends PluginSettingTab { plugin: OpenWords; + tagTextInput: TextComponent | null = null; constructor(app: App, plugin: OpenWords) { super(app, plugin); @@ -14,7 +15,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { } display(): void { - const {containerEl} = this; + const { containerEl } = this; containerEl.empty(); // 初始化设置快照 @@ -75,74 +76,74 @@ export class OpenWordsSettingTab extends PluginSettingTab { } }; + // 添加标签的处理函数 + const handleAddTag = (val: string) => { + const value = val.trim(); + if (!value) { + new Notice('请输入标签名称!'); + return; + } + + const { enabledTags, tagHistory } = this.plugin.settingsSnapshot; + + if (enabledTags.includes(value)) { + new Notice('该标签已存在!'); + return; + } + + // 更新数据 + enabledTags.push(value); + if (!tagHistory.includes(value)) { + tagHistory.push(value); + } + + // 渲染 UI 并清空输入(清空逻辑在回调中处理) + renderTags(); + return true; // 表示添加成功 + }; + // 添加标签输入与历史建议 new Setting(containerEl) .setName('添加标签') .setDesc('输入新的标签名称,例如 "级别/小学"') .addText(text => { text.setPlaceholder('输入标签名称'); - text.onChange(async (value) => { - (text.inputEl as any).currentValue = value; - }); + // 使用该引用,不再需要 (text.inputEl as any).currentValue + const inputEl = text.inputEl; + + // 初始化建议器 new TagHistorySuggest( this.app, - text.inputEl, + inputEl, this.plugin.settingsSnapshot.tagHistory || [], - (selectedValue) => text.setValue(selectedValue), + (selectedValue) => void text.setValue(selectedValue), async (valueToDelete) => { - const index = this.plugin.settingsSnapshot.tagHistory.indexOf(valueToDelete); - if (index > -1) { - this.plugin.settingsSnapshot.tagHistory.splice(index, 1); - } + const history = this.plugin.settingsSnapshot.tagHistory; + const index = history.indexOf(valueToDelete); + if (index > -1) history.splice(index, 1); } ); - text.inputEl.addEventListener('keydown', (e) => { + // 监听回车键 + inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { - const value = (text.inputEl as any).value?.trim(); - if (value && !this.plugin.settingsSnapshot.enabledTags.includes(value)) { - this.plugin.settingsSnapshot.enabledTags.push(value); - if (!this.plugin.settingsSnapshot.tagHistory.includes(value)) { - this.plugin.settingsSnapshot.tagHistory.push(value); - } - renderTags(); - text.setValue(''); - } else if (value && this.plugin.settingsSnapshot.enabledTags.includes(value)) { - new Notice('该标签已存在!'); + if (handleAddTag(text.getValue())) { + text.setValue(''); // 添加成功后清空 } } }); + + // 存储 text 引用供外部按钮使用(可选,但更优雅) + this.tagTextInput = text; }) .addButton(button => button .setButtonText('添加') + .setCta() // 设置为强调色按钮 .onClick(() => { - const inputs = containerEl.querySelectorAll('input[type="text"]'); - let tagInput = ''; - for (let i = inputs.length - 1; i >= 0; i--) { - const input = inputs[i] as HTMLInputElement; - if (input.placeholder === '输入标签名称') { - tagInput = input.value?.trim() || ''; - break; - } - } - if (tagInput && !this.plugin.settingsSnapshot.enabledTags.includes(tagInput)) { - this.plugin.settingsSnapshot.enabledTags.push(tagInput); - if (!this.plugin.settingsSnapshot.tagHistory.includes(tagInput)) { - this.plugin.settingsSnapshot.tagHistory.push(tagInput); - } - renderTags(); - for (let i = inputs.length - 1; i >= 0; i--) { - const input = inputs[i] as HTMLInputElement; - if (input.placeholder === '输入标签名称') { - input.value = ''; - break; - } - } - } else if (tagInput && this.plugin.settingsSnapshot.enabledTags.includes(tagInput)) { - new Notice('该标签已存在!'); - } else { - new Notice('请输入标签名称!'); + // 直接利用上文的 text 引用 + if (this.tagTextInput && handleAddTag(this.tagTextInput.getValue())) { + this.tagTextInput.setValue(''); } })); @@ -161,7 +162,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { button.setDisabled(true); button.setButtonText('处理中...'); try { - this.plugin.settings = {...this.plugin.settingsSnapshot}; + this.plugin.settings = { ...this.plugin.settingsSnapshot }; this.plugin.settings.enabledTags = [...this.plugin.settingsSnapshot.enabledTags]; this.plugin.settings.tagHistory = [...(this.plugin.settingsSnapshot.tagHistory || this.plugin.settingsSnapshot.enabledTags)]; await this.plugin.saveSettings(); @@ -178,7 +179,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { button.setDisabled(false); button.setButtonText('扫描'); } - })); + })); new Setting(containerEl) .setName('生成索引') @@ -198,7 +199,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { button.setDisabled(false); button.setButtonText('生成'); } - })); + })); new Setting(containerEl) .setName("随机调度概率") @@ -245,7 +246,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { new Notice('已恢复默认设置'); // 重新渲染设置页以反映默认值 this.display(); - this.plugin.settings = {...this.plugin.settingsSnapshot}; + this.plugin.settings = { ...this.plugin.settingsSnapshot }; this.plugin.settings.enabledTags = [...this.plugin.settingsSnapshot.enabledTags]; this.plugin.settings.tagHistory = [...(this.plugin.settingsSnapshot.tagHistory || this.plugin.settingsSnapshot.enabledTags)]; await this.plugin.saveSettings(); @@ -282,6 +283,6 @@ export class OpenWordsSettingTab extends PluginSettingTab { button.setDisabled(false); button.setButtonText('重置单词属性'); } - })); + })); } } diff --git a/src/utils/Converters.ts b/src/utils/Converters.ts new file mode 100644 index 0000000..4954e67 --- /dev/null +++ b/src/utils/Converters.ts @@ -0,0 +1,13 @@ +export function asArray(value: unknown, defaultValue: T[] = []): T[] { + if (Array.isArray(value)) return value as T[]; + if (value !== null && value !== undefined) return [value as T]; + return defaultValue; +} + +export function asNumber(value: unknown, defaultValue = 0): number { + return typeof value === 'number' ? value : defaultValue; +} + +export function asDateString(value: unknown, defaultValue = window.moment().format('YYYY-MM-DD')): string { + return typeof value === 'string' ? value : defaultValue; +} diff --git a/src/views/LearningPage.ts b/src/views/LearningPage.ts index 5c1aaf7..6165c52 100644 --- a/src/views/LearningPage.ts +++ b/src/views/LearningPage.ts @@ -2,10 +2,11 @@ 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 = this.pickNextLCard(); + this.currentLearingCard = await this.pickNextLCard(); if (!this.currentLearingCard) return; this.viewContainer.empty(); @@ -15,9 +16,9 @@ export async function LearningPage(this: MainView) { const settingsContainer = learningContainer.createDiv({ cls: 'openwords-learning-settings' }); backBtn.textContent = '返回'; - backBtn.onclick = () => { + backBtn.onclick = async () => { this.page = 'type'; - this.render(); + await this.render(); }; // ESC 快捷键返回 @@ -37,7 +38,7 @@ export async function LearningPage(this: MainView) { } // 选卡逻辑 -export function pickNextLCard(this: MainView) { +export async function pickNextLCard(this: MainView) { const now = window.moment(); let pool: CardInfo[]; if (this.page === 'new') { // 新词排序:间隔降序,间隔相同则易记因子升序 @@ -45,7 +46,7 @@ export function pickNextLCard(this: MainView) { if (pool.length === 0) { new Notice("已完成所有新词!"); this.page = 'type'; - void this.render(); + await this.render(); return null; } pool.sort((a, b) => { @@ -62,7 +63,7 @@ export function pickNextLCard(this: MainView) { if (pool.length === 0) { new Notice("没有需要复习的卡片!"); this.page = 'type'; - void this.render(); + await this.render(); return null; } pool.sort((a, b) => { @@ -95,14 +96,15 @@ export function renderCard(this: MainView, cardContainer: HTMLDivElement) { const fileCache = this.plugin.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["重复次数"]; + 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' }); @@ -125,8 +127,8 @@ export function renderSettings(this: MainView, cardContainer: HTMLDivElement, se new Setting(settingsContainer) .setName(label) .addButton(btn => { - btn.setButtonText(String(grade+1)); - btn.buttonEl.setAttribute('data-grade', String(grade+1)); + btn.setButtonText(String(grade + 1)); + btn.buttonEl.setAttribute('data-grade', String(grade + 1)); btn.onClick(() => this.rateCard(grade, cardContainer)); }); } @@ -137,7 +139,7 @@ export async function rateCard(this: MainView, grade: SuperMemoGrade, cardContai if (this.isRating || !this.currentLearingCard) return; this.isRating = true; await this.plugin.updateCard(this.currentLearingCard, grade, this.page); - this.currentLearingCard = this.pickNextLCard(); + this.currentLearingCard = await this.pickNextLCard(); this.renderCard(cardContainer); this.isRating = false; } @@ -146,12 +148,12 @@ export async function rateCard(this: MainView, grade: SuperMemoGrade, cardContai 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; + 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; + const grade = parseInt(event.key) - 1 as SuperMemoGrade; await this.plugin.updateCard(this.currentLearingCard, grade, this.page); } }; @@ -165,7 +167,7 @@ export function registerRatingKeyEvents(this: MainView, learningContainer: HTMLD if (btn) btn.classList.remove('active'); this.isRating = false; this.currentRatingKey = null; - this.currentLearingCard = this.pickNextLCard(); + this.currentLearingCard = await this.pickNextLCard(); this.renderCard(cardContainer); } }; @@ -177,7 +179,7 @@ export function registerRatingKeyEvents(this: MainView, learningContainer: HTMLD export function registerRenderKeyEvents(this: MainView, learningContainer: HTMLDivElement, cardContainer: HTMLDivElement) { let isShowingMarkdown = false; const showMarkdown = async () => { - if (isShowingMarkdown || !this.currentLearingCard) return; + 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); @@ -200,10 +202,10 @@ export function registerRenderKeyEvents(this: MainView, learningContainer: HTMLD this.renderCard(cardContainer); isShowingMarkdown = false; }; - + // 延迟1秒后注册鼠标事件 setTimeout(() => { - cardContainer.addEventListener('mouseenter', showMarkdown); + cardContainer.addEventListener('mouseenter', () => { void showMarkdown(); }); cardContainer.addEventListener('mouseleave', showWord); }, 0); diff --git a/src/views/MainView.ts b/src/views/MainView.ts index 1cf408c..cd6a0b6 100644 --- a/src/views/MainView.ts +++ b/src/views/MainView.ts @@ -46,6 +46,7 @@ export class MainView extends ItemView { } getViewType() { return MAIN_VIEW; } + // eslint-disable-next-line obsidianmd/ui/sentence-case getDisplayText() { return "OpenWords"; } getIcon(): string { return "slack"; } @@ -55,7 +56,7 @@ export class MainView extends ItemView { this.viewContainer = container.createDiv({ cls: 'openwords-view-container' }); this.statusBarEl = container.createDiv({ cls: 'openwords-view-statusbar' }); await this.render(); - await this.updateStatusBar(); + this.updateStatusBar(); } async onClose() { @@ -82,9 +83,9 @@ export class MainView extends ItemView { } } - async updateStatusBar() { + updateStatusBar() { if (!this.statusBarEl) return; - + // 计算各分类的单词数 const newCount = this.plugin.newCards.size; const dueTodayCount = Array.from(this.plugin.dueCards.values()) @@ -93,43 +94,54 @@ export class MainView extends ItemView { const masteredCount = this.plugin.masterCards.size; const disabledCount = this.plugin.allCards.size - this.plugin.enabledCards.size; const totalCount = this.plugin.allCards.size; - + // 清空状态栏 this.statusBarEl.empty(); - + // 创建第一行:待学习、待复习、今日到期、已掌握、未启用 const row1 = this.statusBarEl.createDiv({ cls: 'openwords-statusbar-row' }); - + this.createStatItem(row1, '待学习', newCount); this.createStatItem(row1, '待复习', reviewCount); this.createStatItem(row1, '今日到期', dueTodayCount); this.createStatItem(row1, '已掌握', masteredCount); this.createStatItem(row1, '未启用', disabledCount); - + // 创建第二行:总计 const row2 = this.statusBarEl.createDiv({ cls: 'openwords-statusbar-row openwords-statusbar-total' }); this.createStatItem(row2, '总计', totalCount, true); } - + private createStatItem(container: HTMLElement, label: string, count: number, isTotal: boolean = false) { const item = container.createDiv({ cls: isTotal ? 'openwords-stat-item total' : 'openwords-stat-item' }); item.createSpan({ cls: 'openwords-stat-label', text: label }); item.createSpan({ cls: 'openwords-stat-count', text: count.toString() }); - + // 添加点击事件,打开对应的视图 if (isTotal) { - item.addEventListener('click', async () => { - // 创建单词状态 .base 文件 - await this.plugin.createWordStatusBaseFile(); - const basePath = `${this.plugin.settings.indexPath}/英语单词状态.base`; - const file = this.app.vault.getFileByPath(basePath); - if (!file) { - new Notice('单词状态文件不存在,请先生成索引'); - return; - } - // 打开 .base 文件 - const leaf = this.app.workspace.getLeaf(false); - await leaf.openFile(file); + item.addEventListener('click', () => { + // 使用立即执行的异步函数,并标记为 void + void (async () => { + try { + // 创建单词状态 .base 文件 + await this.plugin.createWordStatusBaseFile(); + + const basePath = `${this.plugin.settings.indexPath}/英语单词状态.base`; + const file = this.app.vault.getFileByPath(basePath); + + if (!file) { + new Notice('单词状态文件不存在,请先生成索引'); + return; + } + + // 打开 .base 文件 + const leaf = this.app.workspace.getLeaf(false); + await leaf.openFile(file); + } catch (error) { + new Notice(`打开文件失败: ${String(error)}`); + console.error('Open base file error:', error); + } + })(); }); }; } diff --git a/src/views/SpellingPage.ts b/src/views/SpellingPage.ts index 2d6fedc..2eb7b4d 100644 --- a/src/views/SpellingPage.ts +++ b/src/views/SpellingPage.ts @@ -4,7 +4,7 @@ import { MainView } from './MainView'; export async function SpellingPage(this: MainView) { - this.currentSpellingCard = this.pickNextSCard(); + this.currentSpellingCard = await this.pickNextSCard(); if (!this.currentSpellingCard) return; this.viewContainer.empty(); @@ -16,9 +16,9 @@ export async function SpellingPage(this: MainView) { const feedbackContainer = spellingContainer.createDiv({ cls: 'openwords-spelling-feedback' }); backBtn.textContent = '返回'; - backBtn.onclick = () => { + backBtn.onclick = async () => { this.page = 'type'; - this.render(); + await this.render(); }; // ESC 快捷键返回 @@ -51,7 +51,7 @@ export async function SpellingPage(this: MainView) { } -export function pickNextSCard(this: MainView) { +export async function pickNextSCard(this: MainView) { // 从易记因子最低的50%中随机选择一个单词 this.hasPeeked = false; this.errorCount = 0; @@ -60,7 +60,7 @@ export function pickNextSCard(this: MainView) { if (cards.length === 0) { new Notice("已完成所有单词!"); this.page = 'type'; - void this.render(); + await this.render(); return null; } const sorted = cards.slice().sort((a, b) => a.efactor - b.efactor); @@ -115,7 +115,7 @@ export async function renderInput( if (inputField.value.length === 1 && i < word.length - 1) { inputFields[i + 1]?.focus(); } - this.checkSpelling(inputFields, word, feedbackContainer, wordMeaningContainer, inputContainer); + void this.checkSpelling(inputFields, word, feedbackContainer, wordMeaningContainer, inputContainer); }); inputField.addEventListener('keydown', (event) => { @@ -144,11 +144,10 @@ export async function checkSpelling( if (!this.plugin.dueCards.has(word)) { new Notice("当前单词不属于本模式范围, 评分无效并跳过"); - setTimeout(async () => { - this.currentSpellingCard = this.pickNextSCard(); - if (!this.currentSpellingCard) return; - await this.renderInput(wordMeaningContainer, inputContainer, feedbackContainer); - }, 500); + await new Promise(resolve => setTimeout(resolve, 500)); + this.currentSpellingCard = await this.pickNextSCard(); + if (!this.currentSpellingCard) return; + await this.renderInput(wordMeaningContainer, inputContainer, feedbackContainer); return } @@ -167,18 +166,17 @@ export async function checkSpelling( // 同步到 frontmatter const file = this.plugin.app.vault.getFileByPath(this.currentSpellingCard.path); if (file instanceof TFile) { - await this.plugin.app.fileManager.processFrontMatter(file, (frontMatter) => { + await this.plugin.app.fileManager.processFrontMatter(file, (frontMatter: Record) => { frontMatter["易记因子"] = Math.round(efactor * 100); }); } new Notice(`${this.currentSpellingCard.front} \n易记因子: ${efactor.toFixed(2)} \n重复次数: ${this.currentSpellingCard.repetition} \n间隔: ${this.currentSpellingCard.interval} \n到期日: ${this.currentSpellingCard.dueDate}`); } - setTimeout(async () => { - this.currentSpellingCard = this.pickNextSCard(); - if (!this.currentSpellingCard) return; - await this.renderInput(wordMeaningContainer, inputContainer, feedbackContainer); - }, 500); + await new Promise(resolve => setTimeout(resolve, 500)); + this.currentSpellingCard = await this.pickNextSCard(); + if (!this.currentSpellingCard) return; + await this.renderInput(wordMeaningContainer, inputContainer, feedbackContainer); } else { feedbackContainer.setText('错误,请重试!'); this.errorCount += 1; diff --git a/src/views/TypePage.ts b/src/views/TypePage.ts index 0365f4a..7dfdf9c 100644 --- a/src/views/TypePage.ts +++ b/src/views/TypePage.ts @@ -15,10 +15,10 @@ export function TypePage(this: MainView) { .addButton(btn => { const button = btn .setButtonText("开始") - .onClick(() => { + .onClick(async () => { if (this.plugin.newCards.size > 0) { this.page = 'new'; - void this.render(); + await this.render(); } else { new Notice("没有新词可学了!"); } @@ -32,10 +32,10 @@ export function TypePage(this: MainView) { .addButton(btn => { const button = btn .setButtonText("开始") - .onClick(() => { + .onClick(async () => { if (this.plugin.dueCards.size > 0) { this.page = 'old'; - void this.render(); + await this.render(); } else { new Notice("没有需要复习的单词!"); } @@ -49,10 +49,10 @@ export function TypePage(this: MainView) { .addButton(btn => { const button = btn .setButtonText('开始') - .onClick(() => { + .onClick(async () => { if (this.plugin.dueCards.size > 0) { this.page = 'spelling'; - void this.render(); + await this.render(); } else { new Notice("没有需要默写的单词!"); } diff --git a/versions.json b/versions.json index 2bd7b26..7f1c40b 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.0.10": "1.10.0" + "1.0.11": "1.10.0" }