diff --git a/arrangement.ts b/arrangement.ts index ab5c1e3..3f4b4e6 100644 --- a/arrangement.ts +++ b/arrangement.ts @@ -4,21 +4,31 @@ import i18next from "i18next" import { Pattern } from "Pattern" class ArrangementItem { - Name:string + Name: string Count: number Display: string - constructor(name:string, count:number, display:string) { + constructor(name: string, count: number, display: string) { this.Name = name this.Count = count this.Display = display } } +const NEWTAG = "new" +const REVIEWTAG = "review" +const LEARNTAG = "learn" + +export class Stats { + NewCount: number + ReviewCount: number + LearnCount: number +} + export class PatternIter { pattern: Pattern index: number total: number - constructor(pattern:Pattern,index:number,total:number) { + constructor(pattern: Pattern, index: number, total: number) { this.pattern = pattern this.index = index this.total = total @@ -26,11 +36,18 @@ export class PatternIter { } abstract class ArrangementBase { - abstract PatternSequence(Name:string):AsyncGenerator - abstract ArrangementList():ArrangementItem[] + abstract PatternSequence(Name: string): AsyncGenerator + abstract ArrangementList(): ArrangementItem[] + abstract stats(): Stats } -export class Arrangement implements ArrangementBase{ +function isToday(date: moment.Moment): boolean { + const todayStart = window.moment().startOf('day') + const todayEnd = window.moment().endOf('day') + return date.isBetween(todayStart, todayEnd, null, '[]'); +} + +export class Arrangement implements ArrangementBase { private allPattern: Pattern[] private newPattern: Pattern[] private needReviewPattern: Pattern[] @@ -44,7 +61,7 @@ export class Arrangement implements ArrangementBase{ } async init() { let search = NewCardSearch() - let allcards = await search.search() + let allcards = await search.search() this.allPattern = [] this.newPattern = [] this.needReviewPattern = [] @@ -56,16 +73,42 @@ export class Arrangement implements ArrangementBase{ } this.sort() } + stats(): Stats { + let newCount = 0 + let reviewCount = 0 + let learnCount = 0 + for (let p of this.allPattern) { + if (p.schedule.Last && p.schedule.Last != "") { + if (isToday(p.schedule.LastTime)) { + if (p.schedule.Opts.length == 1) { + newCount++ + } else { + reviewCount++ + } + } + } + if (p.schedule.Learned && p.schedule.Learned != "") { + if (isToday(p.schedule.LearnedTime)) { + learnCount++ + } + } + } + let stats = new Stats + stats.LearnCount = learnCount + stats.NewCount = newCount + stats.ReviewCount = reviewCount + return stats + } ArrangementList(): ArrangementItem[] { - let retlist:ArrangementItem[] = [] + let retlist: ArrangementItem[] = [] if (this.newPattern.length > 0) { - retlist.push(new ArrangementItem("new", this.newPattern.length, i18next.t('StartTextNew'))) + retlist.push(new ArrangementItem(NEWTAG, this.newPattern.length, i18next.t('StartTextNew'))) } - if (this.needReviewPattern.length > 0 ) { - retlist.push(new ArrangementItem("review", this.needReviewPattern.length, i18next.t('StartTextReview'))) + if (this.needReviewPattern.length > 0) { + retlist.push(new ArrangementItem(REVIEWTAG, this.needReviewPattern.length, i18next.t('StartTextReview'))) } - if (this.needLearn.length > 0 ) { - retlist.push(new ArrangementItem("learn", this.needLearn.length, i18next.t('StartTextLearn'))) + if (this.needLearn.length > 0) { + retlist.push(new ArrangementItem(LEARNTAG, this.needLearn.length, i18next.t('StartTextLearn'))) } return retlist } @@ -85,13 +128,13 @@ export class Arrangement implements ArrangementBase{ this.needLearn.push(p) } } - this.newPattern.sort(()=>{ + this.newPattern.sort(() => { return .5 - Math.random() }) - this.needReviewPattern.sort(()=>{ + this.needReviewPattern.sort(() => { return .5 - Math.random() }) - this.needLearn.sort((a,b)=>{ + this.needLearn.sort((a, b) => { if (a.schedule.LearnedTime.isAfter(b.schedule.LearnedTime)) { return 1 } @@ -113,9 +156,9 @@ export class Arrangement implements ArrangementBase{ } return } - async *PatternSequence(name:string) { - if (name == "review") { - for (let i=0;i { + if (translations[lang]) { + i18n.addResourceBundle(lang, 'translation', { + [key]: translations[lang], + }, true, true); + } + }); +} + export function initLanguage() { i18n.use(initReactI18next).init( { @@ -146,10 +157,10 @@ export function initLanguage() { 'pt-BR': { translation: ptBRTranslation }, - am:{ + am: { translation: amTranslation }, - da:{ + da: { translation: daTranslation } }, @@ -160,6 +171,8 @@ export function initLanguage() { }, } ) + addTranslation("TodayStats", todayStatic) + addTranslation("StartReview", StartReview) const lang = window.localStorage.getItem('language') || "en" i18n.changeLanguage(lang).catch(err => { console.error('Failed to change language:', err); @@ -993,4 +1006,64 @@ const ptBRTranslation = { SettingTextHardChoiceDesc: "Quando você escolhe uma opção difícil, antecipa o próximo tempo de revisão. Um valor maior resulta em uma revisão anterior. Recomendado: 1.", SettingTextWaitting: "Tempo Limite de Espera", SettingTextWaittingDesc: "O tempo limite de espera tem dois propósitos: 1) Durante o período de espera, ele te obriga a dedicar tempo à revisão, contemplação e memorização. 2) Mais importante, ele ajuda você a distinguir opções com mais precisão. Você vai perceber que quando tiver alguma ideia sobre a resposta, se escolher a opção errada, seu tempo de penalidade se torna mais longo. Portanto, você tem mais probabilidade de escolher a opção apropriada em vez de adivinhar aleatoriamente a questão, já que essa adivinhação é altamente imprecisa. Por exemplo, quando você não tem certeza sobre a resposta, é melhor escolher a opção 'Não Tenho Certeza'. Caso contrário, se escolher a opção 'Eu Sei', mas a resposta estiver errada, o tempo de espera será longo. Você pode ajustar essa opção com base na sua própria situação para estender ou encurtar a duração do tempo limite de espera, ou pode desabilitá-lo completamente. Os valores recomendados para o tempo limite de espera devem ser o número de segundos necessários para você lembrar uma palavra de seis letras dentro de três horas sem esquecê-la." +} + +const todayStatic = { + "en": "Today's Learning Progress Statistics", + "zh": "今日学习进度统计", + "ja": "今日の学習進度統計", + "zh-TW": "今日學習進度統計", + "ko": "오늘의 학습 진행 상황 통계", + "ar": "إحصائيات تقدم التعلم اليوم", + "pt": "Estatísticas de Progresso de Aprendizado de Hoje", + "de": "Statistiken zum Lernfortschritt von heute", + "ru": "Статистика учебного прогресса на сегодня", + "fr": "Statistiques d'avancement des apprentissages du jour", + "es": "Estadísticas del Progreso de Aprendizaje de Hoy", + "it": "Statistiche del progresso di apprendimento di oggi", + "id": "Statistik Kemajuan Belajar Hari Ini", + "ro": "Statisticile de progres al învățării de astăzi", + "cs": "Dnešní statistiky učebního pokroku", + "no": "Dagens statistikk for læringsfremgang", + "pl": "Dzisiejsze statystyki postępów w nauce", + "uk": "Статистика навчального прогресу на сьогодні", + "sq": "Statistikat e Progresit të Mësimit të Sotëm", + "th": "สถิติความคืบหน้าในการเรียนรู้วันนี้", + "fa": "آمار پیشرفت آموزش امروز", + "tr": "Bugünkü Öğrenme İlerleme İstatistikleri", + "nl": "Statistieken van de leerprogressie van vandaag", + "ms": "Statistik Kemajuan Pembelajaran Hari Ini", + "pt-BR": "Estatísticas de Progresso de Aprendizado de Hoje", + "am": "የዛሬ የትምህርት እርስዎ ግብር ሪፖርት", + "da": "Dagens statistik for læringens fremgang" +} + +const StartReview = { + "en": "Start Reviewing", + "zh": "开始复习", + "ja": "復習を始める", + "zh-TW": "開始複習", + "ko": "복습 시작하기", + "ar": "ابدأ المراجعة", + "pt": "Iniciar Revisão", + "de": "Mit dem Review beginnen", + "ru": "Начать повторение", + "fr": "Commencer la révision", + "es": "Comenzar a revisar", + "it": "Inizia la revisione", + "id": "Mulai Meninjau", + "ro": "Începeți revizuirea", + "cs": "Začít opakování", + "no": "Start gjennomgangen", + "pl": "Rozpocznij powtórkę", + "uk": "Почати повторення", + "sq": "Fillo rishikimin", + "th": "เริ่มการทบทวน", + "fa": "شروع مرور", + "tr": "İncelemeye Başla", + "nl": "Begin met herziening", + "ms": "Mula Menyemak Semula", + "pt-BR": "Iniciar Revisão", + "am": "መሰረታውን ጀምር", + "da": "Start med at gennemgå" } \ No newline at end of file diff --git a/manifest.json b/manifest.json index 04a3e63..689f33f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "aosr", "name": "Aosr", - "version": "1.0.33", + "version": "1.0.34", "minAppVersion": "0.12.0", "description": "Another obsidian spaced repetition", "author": "linanwx", diff --git a/schedule.ts b/schedule.ts index 8b25af5..fdf60cd 100644 --- a/schedule.ts +++ b/schedule.ts @@ -77,7 +77,7 @@ export interface PatternYaml { Opts: string // 上次被标记为忘记,最后一次复习的时间 Learned: string | null - // 上次被标记为忘记,之后复习的次数 + // 上次被标记为忘记,之后复习的进度 默认为0 范围从-2到+2 LearnedCount: number | null // 用于读取存储的schedule yaml格式 需要复制对象 copy(v: PatternYaml): void @@ -185,33 +185,48 @@ export class defaultSchedule implements PatternSchedule { } else if (this.Opts.at(-1) == String(ReviewEnum.FAIR)) { } else if (this.LearnedCount && this.LearnedCount >= 2) { } else { - // 中期记忆内的信息不需要学习 - // 这部分内容会在过了中期记忆从记忆区中清空后重新安排学习 - let checkPoint = window.moment().add(-3, "hours") - if (this.LearnedTime.isAfter(checkPoint)) { - info.IsWait = true - } else { + if (this.LearnedCount == null) { + // 标记为HARD和FORGET后 + // 没有学习记录立即复习一次 info.IsLearn = true + } else { + let checkPoint: moment.Moment + if (this.LearnedCount <= -2) { + checkPoint = window.moment().add(-10, "minutes") + } else if (this.LearnedCount <= -1) { + checkPoint = window.moment().add(-1, "hours") + } else if (this.LearnedCount <= 0) { + checkPoint = window.moment().add(-3, "hours") + } else if (this.LearnedCount <= 1) { + checkPoint = window.moment().add(-12, "hours") + } else { + checkPoint = window.moment().add(-36, "hours") + } + if (this.LearnedTime.isAfter(checkPoint)) { + info.IsWait = true + } else { + info.IsLearn = true + } } } return info } private getLearnResult(opt: LearnEnum) { - let learnCount = 0 + let learnCount = -2 if (this.LearnedCount) { learnCount = this.LearnedCount } if (opt == LearnEnum.FAIR) { - learnCount++ + learnCount += 1 } if (opt == LearnEnum.HARD) { - learnCount-- + learnCount -= 1 } if (opt == LearnEnum.FORGET) { learnCount -= 2 } if (opt == LearnEnum.EASY) { - learnCount += 2 + learnCount += 1.5 } learnCount = Math.max(-2, learnCount) learnCount = Math.min(2, learnCount) diff --git a/tag.ts b/tag.ts index 7c7494e..76541ac 100644 --- a/tag.ts +++ b/tag.ts @@ -30,7 +30,7 @@ export class emojiplugin implements PluginValue { this.decorations = this.buildDecorations(view); } update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged || update.selectionSet) { + if (update.docChanged || update.viewportChanged || update.selectionSet || update.heightChanged) { this.decorations = this.buildDecorations(update.view); } } @@ -39,16 +39,17 @@ export class emojiplugin implements PluginValue { buildDecorations(view: EditorView): DecorationSet { const builder = new RangeSetBuilder(); const docText = view.state.doc.toString(); - for (let { from, to } of view.visibleRanges) { - syntaxTree(view.state).iterate({ + let tree = syntaxTree(view.state) + if (tree === null) { + continue + } + tree.iterate({ from, to, enter(node) { - view.visibleRanges - let text = docText.substring(node.from, node.to) - // console.log(`name: ${node.name}, text: ${text}`) - if (node.name.startsWith("hashtag")) { + if (node.name.startsWith("hashtag") && node.name.contains("AOSR")) { + let text = docText.substring(node.from, node.to) if (text.startsWith("AOSR/")) { if (!inSelection(view.state.selection, node.from, node.to)) { builder.add( diff --git a/view.tsx b/view.tsx index 0e0759c..e28fb44 100644 --- a/view.tsx +++ b/view.tsx @@ -7,7 +7,7 @@ import CircularProgress from '@mui/material/CircularProgress'; import LinearProgress from '@mui/material/LinearProgress'; import Typography from '@mui/material/Typography'; import { Pattern } from "Pattern"; -import { Arrangement, PatternIter } from 'arrangement'; +import { Arrangement, PatternIter, Stats } from 'arrangement'; import { MarkdownRenderComponent } from 'markdown'; import { EditorPosition, ItemView, MarkdownView, TFile } from 'obsidian'; import * as React from "react"; @@ -174,6 +174,55 @@ function findOutline(file: TFile, offset: number): string { return currentOutline.join(" > ") } +async function openUnpinnedFile(note: TFile) { + let leaf = app.workspace.getLeavesOfType("markdown").at(0) + if (!leaf || leaf.getViewState()?.pinned == true) { + leaf = app.workspace.getLeaf(true) + } + await leaf.openFile(note) + return leaf +} + + +async function openPatternFile(pattern: Pattern) { + // 打开文件 + let leaf = await openUnpinnedFile(pattern.card.note) + if (!(leaf.view instanceof MarkdownView)) { + return + } + let view = leaf.view + // 读取文件找到tag + let noteText = view.data + let index = noteText.indexOf(pattern.TagID) + let length = pattern.TagID.length + // 处理Tag不存在的情况 + if (index < 0) { + index = pattern.card.indexBuff + length = 0 + } + // 换算位置 + let tagpos = view.editor.offsetToPos(index) + let tagposEnd : EditorPosition = { + line: tagpos.line, + ch: tagpos.ch + length + } + // 滚动 + if (view.getMode() == "preview") { + view.currentMode.applyScroll(tagpos.line) + } else { + view.editor.setSelection(tagpos, tagposEnd) + view.editor.scrollIntoView({ from: tagpos, to: tagposEnd }, true) + view.editor.setSelection(tagpos, tagposEnd) + view.editor.scrollIntoView({ from: tagpos, to: tagposEnd }, true) + + // 避免某些情况仍然没有正确定位 + setTimeout(function() { + view.editor.setSelection(tagpos, tagposEnd) + view.editor.scrollIntoView({ from: tagpos, to: tagposEnd }, true) + }, 800) + } +} + class Reviewing extends React.Component { initFlag: boolean lastPattern: Pattern | undefined @@ -238,46 +287,47 @@ class Reviewing extends React.Component { if (!pattern) { return } - let leaf = app.workspace.getLeavesOfType("markdown").at(0) - if (!leaf || leaf.getViewState()?.pinned == true) { - leaf = app.workspace.getLeaf(true) - } - await leaf.openFile(pattern.card.note) - let view = app.workspace.getActiveViewOfType(MarkdownView) - if (!view) { - return - } - // 优先使用Tag的位置,如果tag不存在,则使用卡片缓存的位置 - let noteText = await app.vault.read(pattern.card.note) - let index = noteText.indexOf(pattern.TagID) - let offset = 0 - let length = 0 - if (index >= 0) { - offset = index - length = pattern.TagID.length - } else { - offset = pattern.card.indexBuff - length = pattern.card.cardText.length - } - let range1 = view.editor.offsetToPos(offset) - let range2 = view.editor.offsetToPos(offset + length) - let range2next: EditorPosition = { - line: range2.line + 1, - ch: 0, - } - let range3: EditorPosition - if (index >= 0) { - range3 = range2 - } else { - range3 = range2next - } - view.currentMode.applyScroll(range1.line); - view.editor.setSelection(range3, range1) - await new Promise(resolve => setTimeout(resolve, 100)); - view.editor.scrollIntoView({ - from: range1, - to: range3, - }, true) + await openPatternFile(pattern) + // let leaf = app.workspace.getLeavesOfType("markdown").at(0) + // if (!leaf || leaf.getViewState()?.pinned == true) { + // leaf = app.workspace.getLeaf(true) + // } + // await leaf.openFile(pattern.card.note) + // let view = app.workspace.getActiveViewOfType(MarkdownView) + // if (!view) { + // return + // } + // // 优先使用Tag的位置,如果tag不存在,则使用卡片缓存的位置 + // let noteText = await app.vault.read(pattern.card.note) + // let index = noteText.indexOf(pattern.TagID) + // let offset = 0 + // let length = 0 + // if (index >= 0) { + // offset = index + // length = pattern.TagID.length + // } else { + // offset = pattern.card.indexBuff + // length = pattern.card.cardText.length + // } + // let range1 = view.editor.offsetToPos(offset) + // let range2 = view.editor.offsetToPos(offset + length) + // let range2next: EditorPosition = { + // line: range2.line + 1, + // ch: 0, + // } + // let range3: EditorPosition + // if (index >= 0) { + // range3 = range2 + // } else { + // range3 = range2next + // } + // view.currentMode.applyScroll(range1.line); + // view.editor.setSelection(range3, range1) + // await new Promise(resolve => setTimeout(resolve, 100)); + // view.editor.scrollIntoView({ + // from: range1, + // to: range3, + // }, true) } PatternComponent = () => { if (this.state.nowPattern) { @@ -443,6 +493,11 @@ class MaindeskComponent extends React.Component { super(props) } render(): React.ReactNode { + let showStats: boolean = false + let stats = this.props.arrangement.stats() + if (stats.NewCount > 0 || stats.LearnCount > 0 || stats.ReviewCount > 0) { + showStats = true + } return