mirror of
https://github.com/insile/OpenWords.git
synced 2026-07-22 05:48:39 +00:00
2026/2/19
This commit is contained in:
parent
3eecf75779
commit
aca53a6204
6 changed files with 611 additions and 198 deletions
177
src/main.ts
177
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import {MarkdownView, Notice, Plugin, TFile, TFolder, Vault} from 'obsidian';
|
||||
import {EventRef, MarkdownView, Notice, Plugin, TFile, TFolder, Vault} from 'obsidian';
|
||||
import { OpenWordsSettings, DEFAULT_SETTINGS } from "./settings/SettingData";
|
||||
import { OpenWordsSettingTab } from './settings/SettingTab';
|
||||
import { MainView, MAIN_VIEW, PageType } from './views/MainView';
|
||||
|
|
@ -17,6 +17,9 @@ export default class OpenWords extends Plugin {
|
|||
enabledCards: Map<string, CardInfo> = new Map(); // 启用单词
|
||||
newCards: Map<string, CardInfo> = new Map(); // 新单词
|
||||
dueCards: Map<string, CardInfo> = new Map(); // 旧单词
|
||||
currentFolderPath: string = ""; // 当前监听的文件夹路径
|
||||
fileWatcherRefs: EventRef[] = []; // 存储文件监听器的 EventRef 以便注销
|
||||
|
||||
|
||||
// 插件加载时执行的操作
|
||||
async onload() {
|
||||
|
|
@ -28,10 +31,6 @@ export default class OpenWords extends Plugin {
|
|||
|
||||
// 等待布局完成
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
const normalized = this.settings.folderPath.endsWith("/")
|
||||
? this.settings.folderPath
|
||||
: this.settings.folderPath + "/";
|
||||
|
||||
// 激活视图命令
|
||||
this.addCommand({
|
||||
id: 'LearningTypeModal',
|
||||
|
|
@ -48,29 +47,8 @@ export default class OpenWords extends Plugin {
|
|||
this.addDoubleBrackets();
|
||||
}
|
||||
});
|
||||
// 监听单词文件创建事件
|
||||
this.registerEvent(this.app.vault.on("create", (file: TFile) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(normalized)) {
|
||||
this.loadWordMetadata(file);
|
||||
}
|
||||
}));
|
||||
// 监听单词文件删除事件
|
||||
this.registerEvent(this.app.vault.on("delete", (file: TFile) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(normalized)) {
|
||||
this.allCards.delete(file.basename);
|
||||
this.masterCards.delete(file.basename);
|
||||
this.enabledCards.delete(file.basename);
|
||||
this.newCards.delete(file.basename);
|
||||
this.dueCards.delete(file.basename);
|
||||
this.updateStatusBar()
|
||||
}
|
||||
}));
|
||||
// 监听单词文件缓存修改事件
|
||||
this.registerEvent(this.app.metadataCache.on("changed", (file) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(normalized)) {
|
||||
this.loadWordMetadata(file);
|
||||
}
|
||||
}));
|
||||
// 注册文件监听器
|
||||
this.registerFileWatchers();
|
||||
// 扫描所有单词文件, 更新状态栏
|
||||
await this.scanAllNotes();
|
||||
this.updateStatusBar();
|
||||
|
|
@ -121,15 +99,7 @@ export default class OpenWords extends Plugin {
|
|||
|
||||
const tags: string[] = frontMatter.tags || [];
|
||||
const isMastered = frontMatter["掌握"] === true;
|
||||
const isTagged =
|
||||
(this.settings.enableWords1 && tags.includes('级别/小学')) ||
|
||||
(this.settings.enableWords2 && tags.includes('级别/中考')) ||
|
||||
(this.settings.enableWords3 && tags.includes('级别/高考四级')) ||
|
||||
(this.settings.enableWords4 && tags.includes('级别/考研')) ||
|
||||
(this.settings.enableWords5 && tags.includes('级别/六级')) ||
|
||||
(this.settings.enableWords6 && tags.includes('级别/雅思')) ||
|
||||
(this.settings.enableWords7 && tags.includes('级别/托福')) ||
|
||||
(this.settings.enableWords8 && tags.includes('级别/GRE'));
|
||||
const isTagged = tags.some(tag => this.settings.enabledTags.includes(tag));
|
||||
|
||||
const card: CardInfo = {
|
||||
front: file.basename,
|
||||
|
|
@ -218,7 +188,7 @@ export default class OpenWords extends Plugin {
|
|||
(mode === 'new' && !this.newCards.has(card.front)) ||
|
||||
(mode === 'old' && !this.dueCards.has(card.front))
|
||||
) {
|
||||
new Notice("当前单词不属于本模式范围, 评分无效并跳过");
|
||||
new Notice(`${card.front} 不属于本模式范围 \n评分无效并跳过`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +210,16 @@ export default class OpenWords extends Plugin {
|
|||
|
||||
new Notice(`${card.front} \n易记因子: ${result.efactor.toFixed(2)} \n重复次数: ${result.repetition} \n间隔: ${result.interval} \n到期日: ${newDate}`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
// 等待元数据缓存更新(最多等待 1 秒)
|
||||
let attempts = 0;
|
||||
while (attempts < 10) {
|
||||
const updated = this.app.metadataCache.getFileCache(file);
|
||||
if (updated?.frontmatter?.["易记因子"] === Math.round(result.efactor * 100)) {
|
||||
break; // 缓存已更新
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置单词属性
|
||||
|
|
@ -249,28 +228,57 @@ 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 转换为数组
|
||||
for (const card of cardList) {
|
||||
const file = this.app.vault.getFileByPath(card.path);
|
||||
if (!file) {
|
||||
new Notice(`文件 ${card.path} 不存在!`);
|
||||
continue;
|
||||
const total = cardList.length;
|
||||
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) {
|
||||
const chunk = cardList.slice(i, i + concurrency);
|
||||
await Promise.all(chunk.map(async (card) => {
|
||||
const file = this.app.vault.getFileByPath(card.path);
|
||||
if (file) {
|
||||
await this.app.fileManager.processFrontMatter(file, (frontMatter) => {
|
||||
frontMatter["到期日"] = todayDate;
|
||||
frontMatter["间隔"] = 0;
|
||||
frontMatter["易记因子"] = 250;
|
||||
frontMatter["重复次数"] = 0;
|
||||
});
|
||||
}
|
||||
count++;
|
||||
// 定期更新 Notice,减少 UI 更新频率
|
||||
if (count % updateFrequency === 0 || count === total) {
|
||||
notice.setMessage(`重置中... ${count}/${total}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
await this.app.fileManager.processFrontMatter(file, (frontMatter) => {
|
||||
frontMatter["到期日"] = window.moment().format('YYYY-MM-DD');
|
||||
frontMatter["间隔"] = 0;
|
||||
frontMatter["易记因子"] = 250;
|
||||
frontMatter["重复次数"] = 0;
|
||||
});
|
||||
count++; // 增加计数器
|
||||
notice.setMessage(`重置中... ${count}/${this.enabledCards.size}`); // 更新 Notice 的消息
|
||||
}
|
||||
notice.setMessage(`重置完成!共 ${count} 个单词`); // 完成后更新消息
|
||||
setTimeout(() => notice.hide(), 2000); // 2 秒后自动隐藏 Notice
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await processTasks();
|
||||
notice.setMessage(`重置完成!共 ${count} 个单词`);
|
||||
} catch (error) {
|
||||
new Notice(`重置出错: ${error}`);
|
||||
console.error('Reset card error:', error);
|
||||
} finally {
|
||||
// 重新注册监听器
|
||||
this.registerFileWatchers();
|
||||
// 重新扫描所有单词文件,确保数据同步
|
||||
await this.scanAllNotes();
|
||||
this.updateStatusBar();
|
||||
setTimeout(() => notice.hide(), 2000); // 2 秒后自动隐藏 Notice
|
||||
}
|
||||
}
|
||||
|
||||
// 建立单词双链
|
||||
async addDoubleBrackets() {
|
||||
const unmasteredWords = new Set(Array.from(this.enabledCards.values())
|
||||
|
|
@ -536,8 +544,63 @@ export default class OpenWords extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 注册文件监听器
|
||||
private registerFileWatchers() {
|
||||
// 先清理旧的监听器
|
||||
this.unregisterFileWatchers();
|
||||
|
||||
// 更新当前监听的文件夹路径
|
||||
this.currentFolderPath = this.settings.folderPath.endsWith("/")
|
||||
? this.settings.folderPath
|
||||
: this.settings.folderPath + "/";
|
||||
|
||||
// 监听单词文件创建事件
|
||||
const createRef = this.app.vault.on("create", (file: TFile) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) {
|
||||
this.loadWordMetadata(file);
|
||||
}
|
||||
});
|
||||
this.fileWatcherRefs.push(createRef);
|
||||
|
||||
// 监听单词文件删除事件
|
||||
const deleteRef = this.app.vault.on("delete", (file: TFile) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) {
|
||||
this.allCards.delete(file.basename);
|
||||
this.masterCards.delete(file.basename);
|
||||
this.enabledCards.delete(file.basename);
|
||||
this.newCards.delete(file.basename);
|
||||
this.dueCards.delete(file.basename);
|
||||
this.updateStatusBar()
|
||||
}
|
||||
});
|
||||
this.fileWatcherRefs.push(deleteRef);
|
||||
|
||||
// 监听单词文件缓存修改事件
|
||||
const changedRef = this.app.metadataCache.on("changed", (file) => {
|
||||
if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) {
|
||||
this.loadWordMetadata(file);
|
||||
}
|
||||
});
|
||||
this.fileWatcherRefs.push(changedRef);
|
||||
}
|
||||
|
||||
// 注销文件监听器
|
||||
private unregisterFileWatchers() {
|
||||
this.fileWatcherRefs.forEach(ref => {
|
||||
this.app.vault.offref(ref);
|
||||
this.app.metadataCache.offref(ref);
|
||||
});
|
||||
this.fileWatcherRefs = [];
|
||||
}
|
||||
|
||||
// 重新初始化文件监听器(供设置改变时调用)
|
||||
async reinitializeFileWatchers() {
|
||||
this.registerFileWatchers();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 清理文件监听器
|
||||
this.unregisterFileWatchers();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,8 @@ import { normalizePath } from "obsidian";
|
|||
export interface OpenWordsSettings {
|
||||
folderPath: string;
|
||||
indexPath: string;
|
||||
enableWords1: boolean;
|
||||
enableWords2: boolean;
|
||||
enableWords3: boolean;
|
||||
enableWords4: boolean;
|
||||
enableWords5: boolean;
|
||||
enableWords6: boolean;
|
||||
enableWords7: boolean;
|
||||
enableWords8: boolean;
|
||||
enabledTags: string[]; // 用户选择的标签列表
|
||||
tagHistory: string[]; // 标签输入历史记录
|
||||
randomRatio: number;
|
||||
maxEfactorForLink: number;
|
||||
}
|
||||
|
|
@ -22,14 +16,26 @@ export interface OpenWordsSettings {
|
|||
export const DEFAULT_SETTINGS: OpenWordsSettings = {
|
||||
folderPath: normalizePath(""),
|
||||
indexPath: normalizePath("索引"),
|
||||
enableWords1: false,
|
||||
enableWords2: false,
|
||||
enableWords3: false,
|
||||
enableWords4: false,
|
||||
enableWords5: false,
|
||||
enableWords6: false,
|
||||
enableWords7: false,
|
||||
enableWords8: false,
|
||||
enabledTags: [
|
||||
"级别/小学",
|
||||
"级别/中考",
|
||||
"级别/高考四级",
|
||||
"级别/考研",
|
||||
"级别/六级",
|
||||
"级别/雅思",
|
||||
"级别/托福",
|
||||
"级别/GRE",
|
||||
],
|
||||
tagHistory: [
|
||||
"级别/小学",
|
||||
"级别/中考",
|
||||
"级别/高考四级",
|
||||
"级别/考研",
|
||||
"级别/六级",
|
||||
"级别/雅思",
|
||||
"级别/托福",
|
||||
"级别/GRE",
|
||||
],
|
||||
randomRatio: 0.7,
|
||||
maxEfactorForLink: 2.6,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { App, Notice, PluginSettingTab, Setting, normalizePath } from 'obsidian';
|
||||
import { FolderSuggest } from '../utils/InputSuggest';
|
||||
import { FolderSuggest, TagHistorySuggest } from '../utils/InputSuggest';
|
||||
import { DEFAULT_SETTINGS } from './SettingData';
|
||||
import OpenWords from '../main';
|
||||
|
||||
|
||||
|
|
@ -15,140 +16,189 @@ export class OpenWordsSettingTab extends PluginSettingTab {
|
|||
display(): void {
|
||||
const {containerEl} = this;
|
||||
containerEl.empty();
|
||||
const textContainer = containerEl.createDiv({ cls: 'openwords-text-grid' });
|
||||
const toggleContainer = containerEl.createDiv({ cls: 'openwords-toggle-grid' });
|
||||
this.plugin.settingsSnapshot = Object.assign({}, this.plugin.settings); // 创建设置快照
|
||||
new Setting(textContainer)
|
||||
|
||||
// 初始化设置快照
|
||||
this.plugin.settingsSnapshot = Object.assign({}, this.plugin.settings);
|
||||
this.plugin.settingsSnapshot.enabledTags = [...this.plugin.settings.enabledTags];
|
||||
this.plugin.settingsSnapshot.tagHistory = [...(this.plugin.settings.tagHistory || this.plugin.settings.enabledTags)];
|
||||
|
||||
// 初始化设置界面
|
||||
this.createPathSettings(containerEl);
|
||||
this.createTagManagement(containerEl);
|
||||
this.createActionButtons(containerEl);
|
||||
}
|
||||
|
||||
private createPathSettings(container: HTMLElement) {
|
||||
new Setting(container)
|
||||
.setName('单词文件夹路径')
|
||||
.setDesc('指定存放单词文件的文件夹路径,默认为仓库:/')
|
||||
.setDesc('指定存放单词文件的文件夹路径,默认为 "仓库:/"')
|
||||
.addText(text => {
|
||||
const input = text.setValue(this.plugin.settings.folderPath);
|
||||
new FolderSuggest(this.app, input.inputEl); // 添加文件夹路径自动补全支持
|
||||
new FolderSuggest(this.app, input.inputEl);
|
||||
input.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.folderPath = normalizePath(value);
|
||||
});
|
||||
});
|
||||
new Setting(textContainer)
|
||||
new Setting(container)
|
||||
.setName('索引文件夹路径')
|
||||
.setDesc('指定存放索引文件的文件夹路径,默认为仓库:索引')
|
||||
.setDesc('指定存放索引文件的文件夹路径,默认为 "仓库:索引"')
|
||||
.addText(text => {
|
||||
const input = text.setValue(this.plugin.settings.indexPath);
|
||||
new FolderSuggest(this.app, input.inputEl); // 添加文件夹路径自动补全支持
|
||||
new FolderSuggest(this.app, input.inputEl);
|
||||
input.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.indexPath = normalizePath(value);
|
||||
});
|
||||
});
|
||||
new Setting(toggleContainer)
|
||||
.setName('小学')
|
||||
.setDesc('启用小学单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords1)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords1 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('中考')
|
||||
.setDesc('启用中考单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords2)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords2 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('高四')
|
||||
.setDesc('启用高四单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords3)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords3 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('考研')
|
||||
.setDesc('启用考研单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords4)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords4 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('六级')
|
||||
.setDesc('启用六级单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords5)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords5 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('雅思')
|
||||
.setDesc('启用雅思单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords6)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords6 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('托福')
|
||||
.setDesc('启用托福单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords7)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords7 = value;
|
||||
}));
|
||||
new Setting(toggleContainer)
|
||||
.setName('GRE')
|
||||
.setDesc('启用GRE单词')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWords8)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settingsSnapshot.enableWords8 = value;
|
||||
}));
|
||||
}
|
||||
|
||||
private createTagManagement(containerEl: HTMLElement) {
|
||||
const tagsSetting = new Setting(containerEl);
|
||||
tagsSetting.setName('启用的标签')
|
||||
.setDesc('选择要启用的标签,点击下方按钮可添加更多标签');
|
||||
|
||||
const tagsContainer = containerEl.createDiv({ cls: 'openwords-tags-container' });
|
||||
tagsSetting.settingEl.appendChild(tagsContainer);
|
||||
|
||||
const renderTags = () => {
|
||||
tagsContainer.empty();
|
||||
if (this.plugin.settingsSnapshot.enabledTags.length === 0) {
|
||||
tagsContainer.createEl('p', { text: '暂无启用的标签', cls: 'openwords-empty-tags' });
|
||||
} else {
|
||||
this.plugin.settingsSnapshot.enabledTags.forEach((tag, index) => {
|
||||
const tagEl = tagsContainer.createEl('div', { cls: 'openwords-tag-item' });
|
||||
tagEl.createEl('span', { text: tag });
|
||||
tagEl.createEl('button', { text: '删除' }).onclick = () => {
|
||||
this.plugin.settingsSnapshot.enabledTags.splice(index, 1);
|
||||
renderTags();
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 添加标签输入与历史建议
|
||||
new Setting(containerEl)
|
||||
.setName('添加标签')
|
||||
.setDesc('输入新的标签名称,例如 "级别/小学"')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('输入标签名称');
|
||||
text.onChange(async (value) => {
|
||||
(text.inputEl as any).currentValue = value;
|
||||
});
|
||||
|
||||
new TagHistorySuggest(
|
||||
this.app,
|
||||
text.inputEl,
|
||||
this.plugin.settingsSnapshot.tagHistory || [],
|
||||
(selectedValue) => text.setValue(selectedValue),
|
||||
async (valueToDelete) => {
|
||||
const index = this.plugin.settingsSnapshot.tagHistory.indexOf(valueToDelete);
|
||||
if (index > -1) {
|
||||
this.plugin.settingsSnapshot.tagHistory.splice(index, 1);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
text.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('该标签已存在!');
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.addButton(button => button
|
||||
.setButtonText('添加')
|
||||
.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('请输入标签名称!');
|
||||
}
|
||||
}));
|
||||
|
||||
renderTags();
|
||||
}
|
||||
|
||||
private createActionButtons(containerEl: HTMLElement) {
|
||||
new Setting(containerEl)
|
||||
.setName('扫描单词')
|
||||
.setDesc('按路径和级别设置扫描并缓存单词')
|
||||
.addButton(button => button
|
||||
.setButtonText('扫描')
|
||||
.setCta() // 设置为主要按钮样式
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
if (button.disabled) return; // 如果按钮已禁用,直接返回
|
||||
|
||||
button.setDisabled(true); // 禁用按钮
|
||||
button.setButtonText('处理中...'); // 更新按钮文本
|
||||
|
||||
if (button.disabled) return;
|
||||
button.setDisabled(true);
|
||||
button.setButtonText('处理中...');
|
||||
try {
|
||||
this.plugin.settings = {...this.plugin.settingsSnapshot}; // 更新插件设置
|
||||
await this.plugin.saveSettings(); // 保存设置
|
||||
await this.plugin.scanAllNotes(); // 重新缓存单词
|
||||
new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`); // 完成后更新消息
|
||||
this.plugin.updateStatusBar(); // 更新状态栏
|
||||
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();
|
||||
// 重新初始化文件监听器
|
||||
await this.plugin.reinitializeFileWatchers();
|
||||
await this.plugin.scanAllNotes();
|
||||
new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`);
|
||||
this.plugin.updateStatusBar();
|
||||
} catch (error) {
|
||||
console.error('缓存过程中发生错误:', error);
|
||||
new Notice('缓存失败,请检查控制台日志!');
|
||||
} finally {
|
||||
button.setDisabled(false); // 重新启用按钮
|
||||
button.setButtonText('扫描'); // 恢复按钮文本
|
||||
button.setDisabled(false);
|
||||
button.setButtonText('扫描');
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('生成索引')
|
||||
.setDesc('按级别生成所有单词的首字母索引文件')
|
||||
.addButton(button => button
|
||||
.setButtonText('索引')
|
||||
.onClick(async () => {
|
||||
if (button.disabled) return; // 如果按钮已禁用,直接返回
|
||||
|
||||
button.setDisabled(true); // 禁用按钮
|
||||
button.setButtonText('处理中...'); // 更新按钮文本
|
||||
|
||||
if (button.disabled) return;
|
||||
button.setDisabled(true);
|
||||
button.setButtonText('处理中...');
|
||||
try {
|
||||
await this.plugin.generateIndex(); // 生成索引文件
|
||||
await this.plugin.generateIndex();
|
||||
} catch (error) {
|
||||
console.error('生成索引过程中发生错误:', error);
|
||||
new Notice('生成索引失败,请检查控制台日志!');
|
||||
} finally {
|
||||
button.setDisabled(false); // 重新启用按钮
|
||||
button.setButtonText('生成'); // 恢复按钮文本
|
||||
button.setDisabled(false);
|
||||
button.setButtonText('生成');
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("随机调度概率")
|
||||
.setDesc("设置每次抽卡时随机调度的概率,其余为优先调度概率")
|
||||
|
|
@ -163,6 +213,7 @@ export class OpenWordsSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("最大双链因子")
|
||||
.setDesc("易记因子小于等于该值的单词可添加双链")
|
||||
|
|
@ -177,25 +228,57 @@ export class OpenWordsSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('恢复默认设置')
|
||||
.setDesc('将所有插件设置恢复为默认值')
|
||||
.addButton(button => button
|
||||
.setButtonText('恢复默认设置')
|
||||
.onClick(async () => {
|
||||
if (button.disabled) return;
|
||||
button.setDisabled(true);
|
||||
button.setButtonText('处理中...');
|
||||
try {
|
||||
this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
await this.plugin.saveSettings();
|
||||
new Notice('已恢复默认设置');
|
||||
// 重新渲染设置页以反映默认值
|
||||
this.display();
|
||||
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();
|
||||
// 重新初始化文件监听器
|
||||
await this.plugin.reinitializeFileWatchers();
|
||||
await this.plugin.scanAllNotes();
|
||||
new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`);
|
||||
this.plugin.updateStatusBar();
|
||||
} catch (error) {
|
||||
console.error('恢复默认设置失败:', error);
|
||||
new Notice('恢复失败,请检查控制台日志!');
|
||||
} finally {
|
||||
button.setDisabled(false);
|
||||
button.setButtonText('恢复默认设置');
|
||||
}
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('重置单词属性')
|
||||
.setDesc('重置作用域单词的易记因子、重复次数、间隔和到期日')
|
||||
.addButton(button => button
|
||||
.setButtonText('重置')
|
||||
.setButtonText('重置单词属性')
|
||||
.onClick(async () => {
|
||||
if (button.disabled) return; // 如果按钮已禁用,直接返回
|
||||
|
||||
button.setDisabled(true); // 禁用按钮
|
||||
button.setButtonText('处理中...'); // 更新按钮文本
|
||||
|
||||
if (button.disabled) return;
|
||||
button.setDisabled(true);
|
||||
button.setButtonText('处理中...');
|
||||
try {
|
||||
await this.plugin.resetCard(); // 重置单词属性
|
||||
await this.plugin.resetCard();
|
||||
} catch (error) {
|
||||
console.error('重置过程中发生错误:', error);
|
||||
new Notice('重置失败,请检查控制台日志!');
|
||||
} finally {
|
||||
button.setDisabled(false); // 重新启用按钮
|
||||
button.setButtonText('重置'); // 恢复按钮文本
|
||||
button.setDisabled(false);
|
||||
button.setButtonText('重置单词属性');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,62 @@ export class FolderSuggest extends AbstractInputSuggest<string> {
|
|||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 标签历史建议类,用于显示标签输入历史
|
||||
export class TagHistorySuggest extends AbstractInputSuggest<string> {
|
||||
inputEl: HTMLInputElement;
|
||||
tagHistory: string[];
|
||||
onSelectCallback: (value: string) => void;
|
||||
onDeleteCallback: (value: string) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
inputEl: HTMLInputElement,
|
||||
tagHistory: string[],
|
||||
onSelectCallback: (value: string) => void,
|
||||
onDeleteCallback: (value: string) => Promise<void>
|
||||
) {
|
||||
super(app, inputEl);
|
||||
this.inputEl = inputEl;
|
||||
this.tagHistory = tagHistory;
|
||||
this.onSelectCallback = onSelectCallback;
|
||||
this.onDeleteCallback = onDeleteCallback;
|
||||
}
|
||||
|
||||
getSuggestions(query: string): string[] {
|
||||
if (!query.trim()) {
|
||||
// 如果查询为空,返回所有历史记录(反向排序,最新的在前)
|
||||
return [...this.tagHistory].reverse();
|
||||
}
|
||||
// 否则按查询过滤,然后反向排序
|
||||
return this.tagHistory.filter(tag =>
|
||||
tag.toLowerCase().includes(query.toLowerCase())
|
||||
).reverse();
|
||||
}
|
||||
|
||||
renderSuggestion(value: string, el: HTMLElement): void {
|
||||
const container = el.createDiv({ cls: 'openwords-tag-history-item' });
|
||||
|
||||
container.createDiv({ text: value, cls: 'openwords-tag-history-text' });
|
||||
|
||||
const deleteBtn = container.createEl('button', {
|
||||
cls: 'openwords-tag-history-delete',
|
||||
text: '删除',
|
||||
});
|
||||
|
||||
deleteBtn.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
await this.onDeleteCallback(value);
|
||||
// 删除该条历史记录的 DOM 元素,保持提示框打开
|
||||
el.remove();
|
||||
};
|
||||
}
|
||||
|
||||
selectSuggestion(value: string): void {
|
||||
this.inputEl.value = value;
|
||||
this.inputEl.trigger('input');
|
||||
this.close();
|
||||
this.onSelectCallback(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,9 +84,36 @@ export class MainView extends ItemView {
|
|||
|
||||
async updateStatusBar() {
|
||||
if (!this.statusBarEl) return;
|
||||
this.statusBarEl.setText(
|
||||
`${this.plugin.newCards.size} + ${Array.from(this.plugin.dueCards.values())
|
||||
.filter(card => window.moment(card.dueDate).isBefore(window.moment())).length} / ${this.plugin.dueCards.size} + ${this.plugin.allCards.size - this.plugin.enabledCards.size} = ${this.plugin.allCards.size}`
|
||||
);
|
||||
|
||||
// 计算各分类的单词数
|
||||
const newCount = this.plugin.newCards.size;
|
||||
const dueTodayCount = Array.from(this.plugin.dueCards.values())
|
||||
.filter(card => window.moment(card.dueDate).isBefore(window.moment())).length;
|
||||
const reviewCount = this.plugin.dueCards.size - dueTodayCount;
|
||||
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() });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
209
styles.css
209
styles.css
|
|
@ -8,18 +8,6 @@ If your plugin does not need CSS, delete this file.
|
|||
*/
|
||||
|
||||
|
||||
/* 插件设置选项卡 */
|
||||
.openwords-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr); /* 每行 4 列 */
|
||||
gap: 10px; /* 每个 toggle 之间的间距 */
|
||||
/* margin-top: 10px; */
|
||||
}
|
||||
.openwords-toggle-grid .setting-item:first-child {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--background-modifier-border)
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.openwords-back-btn {
|
||||
display: block;
|
||||
|
|
@ -29,13 +17,130 @@ If your plugin does not need CSS, delete this file.
|
|||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.openwords-view-statusbar {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 32px auto 0 auto;
|
||||
font-size: 1.1em;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin: 20px auto 0 auto;
|
||||
padding: 20px;
|
||||
width: 70%;
|
||||
max-width: 700px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, var(--background-secondary) 0%, var(--background-primary) 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.openwords-statusbar-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.openwords-statusbar-total {
|
||||
justify-content: center;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.openwords-stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
padding: 2px 2px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--divider-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.openwords-stat-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.openwords-stat-item.total {
|
||||
padding: 2px 2px;
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.openwords-stat-item.total:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.openwords-stat-label {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.openwords-stat-item.total .openwords-stat-label {
|
||||
font-size: 0.8em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.openwords-stat-count {
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item:nth-child(1) .openwords-stat-count {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item:nth-child(2) .openwords-stat-count {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item:nth-child(3) .openwords-stat-count {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item:nth-child(4) .openwords-stat-count {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item:nth-child(5) .openwords-stat-count {
|
||||
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.openwords-stat-item.total .openwords-stat-count {
|
||||
font-size: 1.6em;
|
||||
color: var(--text-normal);
|
||||
background: none;
|
||||
-webkit-text-fill-color: initial;
|
||||
}
|
||||
|
||||
/* 学习模式页面 */
|
||||
|
|
@ -179,3 +284,73 @@ If your plugin does not need CSS, delete this file.
|
|||
font-size: 1em;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
|
||||
/* 插件设置标签管理 */
|
||||
.openwords-tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.openwords-tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.openwords-tag-item button {
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
color: inherit;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.openwords-tag-item button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.openwords-empty-tags {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 标签历史建议样式 */
|
||||
.openwords-tag-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* padding: 4px 0; */
|
||||
}
|
||||
|
||||
.openwords-tag-history-text {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.openwords-tag-history-delete {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.openwords-tag-history-delete:hover {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
Loading…
Reference in a new issue