From 388b1ec13b247a7b6c80925ee2fca636dc6e37bb Mon Sep 17 00:00:00 2001 From: Newdea <9208450+Newdea@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:40:36 +0800 Subject: [PATCH 1/3] =?UTF-8?q?-=20=20fix:=20=E5=BD=93=E5=A4=A9=E6=96=B0?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=A4=8D=E4=B9=A0=E5=90=8E=EF=BC=8C=E5=BD=93?= =?UTF-8?q?=E5=A4=A9=E6=97=A0=E6=B3=95=E5=86=8D=E6=AC=A1=E5=A4=8D=E4=B9=A0?= =?UTF-8?q?=20=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=20-=20=20fix:=20?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=BB=9F=E8=AE=A1=E8=A1=A8=E4=B8=AD=EF=BC=8C?= =?UTF-8?q?=E5=BD=93=E5=A4=A9=E5=A4=8D=E4=B9=A0=E6=95=B0=E6=8D=AE=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E6=9B=B4=E6=96=B0=20=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CardSchedule.ts | 42 ++-- src/Question.ts | 2 +- src/ReviewDeck.ts | 2 + src/algorithms/balance/balance.ts | 2 +- src/dataStore/data.ts | 2 +- src/dataStore/itemToDecks.ts | 17 +- src/dataStore/location_switch.ts | 17 +- src/dataStore/repetitionItem.ts | 40 +++- src/gui/flashcard-modal.tsx | 8 +- src/gui/info.ts | 14 +- src/lang/locale/zh-cn.ts | 22 +- src/lang/locale/zh-tw.ts | 22 +- src/main.ts | 320 +++++++++++------------------- src/reviewNote/review-note.ts | 36 +++- src/stats.ts | 4 +- src/util/DateProvider.ts | 10 +- tests/unit/data.test.ts | 6 +- 17 files changed, 292 insertions(+), 274 deletions(-) diff --git a/src/CardSchedule.ts b/src/CardSchedule.ts index 3da99a77..54dae2e4 100644 --- a/src/CardSchedule.ts +++ b/src/CardSchedule.ts @@ -9,6 +9,7 @@ import { ReviewResponse, schedule } from "./scheduling"; import { SRSettings } from "./settings"; import { formatDate_YYYY_MM_DD } from "./util/utils"; import { DateUtil, globalDateProvider } from "./util/DateProvider"; +import { DateUtils } from "./util/utils_recall"; export class CardScheduleInfo { dueDate: Moment; @@ -37,18 +38,22 @@ export class CardScheduleInfo { } isDue(): boolean { - return this.dueDate.isSameOrBefore(globalDateProvider.today); + // return this.dueDate.isSameOrBefore(globalDateProvider.today); + return ( + this.dueDate.isSameOrBefore(globalDateProvider.today) || + (this.dueDate.isSameOrBefore(globalDateProvider.endofToday) && this.interval >= 1) + ); } isDummyScheduleForNewCard(): boolean { return this.formatDueDate() == CardScheduleInfo.dummyDueDateForNewCard; } - static getDummyScheduleForNewCard(settings: SRSettings): CardScheduleInfo { + static getDummyScheduleForNewCard(baseEase: number): CardScheduleInfo { return CardScheduleInfo.fromDueDateStr( CardScheduleInfo.dummyDueDateForNewCard, CardScheduleInfo.initialInterval, - settings.baseEase, + baseEase, 0, ); } @@ -186,19 +191,24 @@ export class NoteCardScheduleParser { const result: CardScheduleInfo[] = []; for (let i = 0; i < scheduling.length; i++) { const match: RegExpMatchArray = scheduling[i]; - const dueDateNum = parseInt(match[1]); - const interval = parseInt(match[2]); - const ease = parseInt(match[3]); - const dueDate: Moment = window.moment(dueDateNum); - const delayBeforeReviewTicks: number = dueDateNum - globalDateProvider.today.valueOf(); - - const info: CardScheduleInfo = new CardScheduleInfo( - dueDate, - interval, - ease, - delayBeforeReviewTicks, - ); - result.push(info); + if (match == null) { + result.push(CardScheduleInfo.getDummyScheduleForNewCard(0)); + } else { + const dueDateNum = parseInt(match[1]); + const interval = parseInt(match[2]); + const ease = parseInt(match[3]); + const dueDate: Moment = window.moment(dueDateNum); + const delayBeforeReviewTicks: number = + dueDateNum - globalDateProvider.today.valueOf(); + + const info: CardScheduleInfo = new CardScheduleInfo( + dueDate, + interval, + ease, + delayBeforeReviewTicks, + ); + result.push(info); + } } return result; } diff --git a/src/Question.ts b/src/Question.ts index d13be0ad..cee21ee9 100644 --- a/src/Question.ts +++ b/src/Question.ts @@ -174,7 +174,7 @@ export class Question { const card: Card = this.cards[i]; const schedule: CardScheduleInfo = card.hasSchedule ? card.scheduleInfo - : CardScheduleInfo.getDummyScheduleForNewCard(settings); + : CardScheduleInfo.getDummyScheduleForNewCard(settings.baseEase); result += schedule.formatSchedule(); } result += SR_HTML_COMMENT_END; diff --git a/src/ReviewDeck.ts b/src/ReviewDeck.ts index 1b1c290e..dc4cff06 100644 --- a/src/ReviewDeck.ts +++ b/src/ReviewDeck.ts @@ -7,6 +7,8 @@ export interface SchedNote { note: TFile; item?: RepetitionItem; dueUnix?: number; + interval?: number; + ease?: number; } export class ReviewDeck { diff --git a/src/algorithms/balance/balance.ts b/src/algorithms/balance/balance.ts index 856fe34f..18b1abfe 100644 --- a/src/algorithms/balance/balance.ts +++ b/src/algorithms/balance/balance.ts @@ -56,7 +56,7 @@ export function balance( if (isChange) { const msg = `balance: interval from ${beforeIntvl} balance to ${interval} days.`; console.debug(msg); - new Notice(msg); + // new Notice(msg); } else { interval = beforeIntvl; } diff --git a/src/dataStore/data.ts b/src/dataStore/data.ts index a6f02a7c..92ecec61 100644 --- a/src/dataStore/data.ts +++ b/src/dataStore/data.ts @@ -744,7 +744,7 @@ export class DataStore { if (item.isDue) { if (this.settings.algorithm === algorithmNames.Fsrs) { const data: FsrsData = item.data as FsrsData; - if (data.last_review < new Date(date)) { + if (new Date(data.last_review) < new Date(date)) { rc[date].due++; } } else { diff --git a/src/dataStore/itemToDecks.ts b/src/dataStore/itemToDecks.ts index b72ae9e1..92818c5d 100644 --- a/src/dataStore/itemToDecks.ts +++ b/src/dataStore/itemToDecks.ts @@ -19,6 +19,9 @@ import { algorithmNames } from "src/algorithms/algorithms"; export class ItemToDecks { settings: SRSettings; + static create(settings: SRSettings) { + return new ItemToDecks(settings); + } constructor(settings: SRSettings) { this.settings = settings; } @@ -155,8 +158,14 @@ export class ItemToDecks { latterQue[fileid] = rdeck.deckName; } - if (item.isDue) { - rdeck.scheduledNotes.push({ note, item, dueUnix: item.nextReview }); + if (item.hasDue) { + rdeck.scheduledNotes.push({ + note, + item, + dueUnix: item.nextReview, + interval: item.interval, + ease: item.ease, + }); if (item.nextReview <= now_number) { rdeck.dueNotesCount++; // console.debug(`${rdeck.deckName} isDue dueCnt: ${rdeck.dueNotesCount}`, item); @@ -202,8 +211,8 @@ export class ItemToDecks { carditem.itemIds .map((id: number) => store.getItembyID(id).getSched()) .filter((sched) => { - // ignore new add card - if (sched != null && scheduling.length <= count) { + // ignore new add card sched != null && + if (scheduling.length <= count) { scheduling.push(sched); return true; } diff --git a/src/dataStore/location_switch.ts b/src/dataStore/location_switch.ts index adaa6c0e..b3e27d8b 100644 --- a/src/dataStore/location_switch.ts +++ b/src/dataStore/location_switch.ts @@ -30,7 +30,7 @@ export class LocationSwitch { public afternoteStats: Stats; public beforecardStats: Stats; public aftercardStats: Stats; - revTag: string; + private revTag: string; constructor(plugin: SRPlugin, settings: SRSettings) { this.plugin = plugin; @@ -136,6 +136,15 @@ export class LocationSwitch { topicPath = new TopicPath([deckname]); fileText = delDefaultTag(fileText, this.revTag); fileChanged = true; + } else if ( + topicPath.path.length === 2 && + settings.tagsToReview.includes(topicPath.path[1]) + ) { + deckname = topicPath.path[1]; + topicPath = new TopicPath([deckname]); + const revtag = this.converteTag(deckname); + fileText = delDefaultTag(fileText, revtag); + fileChanged = true; } } @@ -309,7 +318,7 @@ export class LocationSwitch { // const citem = store.getItembyID(id); // if (citem.isTracked) { const sched = citem.getSchedDurAsStr(); - if (citem.isDue && sched != null) { + if (citem.hasDue && sched != null) { scheduling.push(sched); dueIds.push(citem.ID); } @@ -332,7 +341,7 @@ export class LocationSwitch { item?.isTracked && (tkfile.isDefault || Tags.isTagedNoteDeckName(item.deckName, this.settings)) ) { - if (item?.isDue) { + if (item?.hasDue) { // let due: str, ease: number, interval: number; const ret = item.getSchedDurAsStr(); if (ret != null) { @@ -353,7 +362,7 @@ export class LocationSwitch { noteTag == null && this.settings.tagsToReview.includes(item.deckName) ) { - const tag = this.converteTag(item.deckName.substring(1)); + const tag = this.converteTag(item.deckName); fileText = addDefaultTagtoNote(fileText, tag); fileChanged = true; } diff --git a/src/dataStore/repetitionItem.ts b/src/dataStore/repetitionItem.ts index db9791a3..37ee6a69 100644 --- a/src/dataStore/repetitionItem.ts +++ b/src/dataStore/repetitionItem.ts @@ -114,8 +114,7 @@ export class RepetitionItem { let ease: number; let interval: number; - const isFsrs: boolean = Object.prototype.hasOwnProperty.call(this.data, "state"); - if (isFsrs) { + if (this.isFsrs) { const data = this.data as FsrsData; interval = data.scheduled_days; // ease just used for StatsChart, not review scheduling. @@ -131,6 +130,10 @@ export class RepetitionItem { return sched; } + private isFsrs(): boolean { + return Object.prototype.hasOwnProperty.call(this.data, "state"); + } + getSchedDurAsStr() { const sched = this.getSched(); if (sched == null) return null; @@ -163,6 +166,13 @@ export class RepetitionItem { } } + get interval(): number { + return Number(this.getSched()[2]); + } + get ease(): number { + return Number(this.getSched()[3]); + } + /** * check if file id is just new add. * @returns boolean @@ -182,7 +192,33 @@ export class RepetitionItem { } } + /** + * check if item should be reviewed rightnow. + */ get isDue() { + const now_number = Date.now(); + if (this.hasDue) { + if (this.nextReview < now_number) { + return true; + } + if (this.nextReview < DateUtils.EndofToday) { + if (this.isFsrs) { + const data: FsrsData = this.data as FsrsData; + if (data.scheduled_days >= 1) { + return true; + } + } else { + const data: AnkiData = this.data as AnkiData; + if (data.lastInterval >= 1) { + return true; + } + } + } + } + return false; + } + + get hasDue() { try { if (this.nextReview > 0 || this.timesReviewed > 0) { return true; diff --git a/src/gui/flashcard-modal.tsx b/src/gui/flashcard-modal.tsx index 323a4ce0..e52d919a 100644 --- a/src/gui/flashcard-modal.tsx +++ b/src/gui/flashcard-modal.tsx @@ -1,4 +1,4 @@ -import { Modal, App, Notice, Platform, setIcon, MarkdownView } from "obsidian"; +import { Modal, App, Notice, Platform, setIcon, MarkdownView, TFile } from "obsidian"; // eslint-disable-next-line @typescript-eslint/no-unused-vars import h from "vhtml"; @@ -24,6 +24,7 @@ import { DataLocation } from "src/dataStore/dataLocation"; import { SrTFile } from "src/SRFile"; import { RepetitionItem, RPITEMTYPE } from "src/dataStore/repetitionItem"; import { debug } from "src/util/utils_recall"; +import { ItemInfoModal } from "./info"; export enum FlashcardModalMode { DecksList, @@ -369,6 +370,11 @@ export class FlashcardModal extends Modal { notePath: this.currentQuestion.note.filePath, }); new Notice(currentEaseStr + "\n" + currentIntervalStr + "\n" + generatedFromStr); + const srfile = this.currentNote.file as SrTFile; + const store = this.plugin.store; + const id = this.currentCard.Id; + const infoM = new ItemInfoModal(this.settings, srfile.file, store.getItembyID(id)); + infoM.open(); } createBackButton() { diff --git a/src/gui/info.ts b/src/gui/info.ts index 25d324f2..1fb6f9f9 100644 --- a/src/gui/info.ts +++ b/src/gui/info.ts @@ -11,27 +11,33 @@ export class ItemInfoModal extends Modal { store: DataStore; settings: SRSettings; file: TFile; + item: RepetitionItem; nextReview: number; lastInterval: number; - constructor(settings: SRSettings, file: TFile) { + constructor(settings: SRSettings, file: TFile, item: RepetitionItem = null) { super(app); // this.plugin = plugin; this.store = DataStore.getInstance(); this.settings = settings; this.file = file; + if (item == null) { + this.item = this.store.getItemsOfFile(this.file.path)[0]; + } else { + this.item = item; + } } onOpen() { const { contentEl } = this; //TODO: Implement Item info. - const item = this.store.getItemsOfFile(this.file.path)[0]; + // const item = this.store.getItemsOfFile(this.file.path)[0]; // const path = this.store.getFilePath(item); // contentEl.createEl("p").setText("Item info of " + this.file.path); const buttonDivAll = contentEl.createDiv("srs-flex-row"); const contentdiv = contentEl.createEl("div"); - this.displayitem(contentdiv, item); + this.displayitem(contentdiv, this.item); // new ButtonComponent(buttonDivAll).setButtonText("Current").onClick(() => { // this.displayitem(contentdiv, item); @@ -126,7 +132,7 @@ export class ItemInfoModal extends Modal { } submit() { - const item = this.store.getItemsOfFile(this.file.path)[0]; + const item = this.item; console.debug(this); const algo = this.settings.algorithm; if (this.nextReview) { diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index 00ed7ecd..5ac32584 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -81,18 +81,16 @@ export default { RESET_DEFAULT: "重置为默认", CARD_MODAL_WIDTH_PERCENT: "卡片宽度百分比", RANDOMIZE_CARD_ORDER: "复习时随机显示卡片?", - REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review", - REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)", - REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)", - REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)", - REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)", - REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck", - REVIEW_DECK_ORDER: "Order decks are displayed during review", - REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: - "Sequentially (once all cards in previous deck reviewed)", - REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: - "Randomly (once all cards in previous deck reviewed)", - REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck", + REVIEW_CARD_ORDER_WITHIN_DECK: "复习时卡片组内的卡片排序", + REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "卡片组内顺序 (全部新卡片优先)", + REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "卡片组内顺序 (全部到期卡片优先)", + REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "卡片组内乱序 (全部新卡片优先)", + REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "卡片组内乱序 (全部到期卡片优先)", + REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "卡片组及卡片都乱序", + REVIEW_DECK_ORDER: "复习时卡片组的排序", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: "顺序 (在前一卡片组内卡片都复习完后)", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: "乱序 (在前一卡片组内卡片都复习完后)", + REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "卡片组及卡片都乱序", DISABLE_CLOZE_CARDS: "不进行完形填空?", CONVERT_HIGHLIGHTS_TO_CLOZES: "将 ==高亮== 转换为完形填空?", CONVERT_BOLD_TEXT_TO_CLOZES: "将 **粗体** 转换为完形填空?", diff --git a/src/lang/locale/zh-tw.ts b/src/lang/locale/zh-tw.ts index 6bf5114f..3ebc5133 100644 --- a/src/lang/locale/zh-tw.ts +++ b/src/lang/locale/zh-tw.ts @@ -81,18 +81,16 @@ export default { RESET_DEFAULT: "重置為預設值", CARD_MODAL_WIDTH_PERCENT: "卡片寬度百分比", RANDOMIZE_CARD_ORDER: "復習時隨機顯示卡片?", - REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review", - REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)", - REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)", - REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)", - REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)", - REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck", - REVIEW_DECK_ORDER: "Order decks are displayed during review", - REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: - "Sequentially (once all cards in previous deck reviewed)", - REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: - "Randomly (once all cards in previous deck reviewed)", - REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck", + REVIEW_CARD_ORDER_WITHIN_DECK: "復習時牌組內的卡片排序", + REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "牌組內順序 (全部新卡片優先)", + REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "牌組內順序 (全部到期卡片優先)", + REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "牌組內亂序 (全部新卡片優先)", + REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "牌組內亂序 (全部到期卡片優先)", + REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "牌組及卡片都亂序", + REVIEW_DECK_ORDER: "復習時牌組的排序", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL: "順序 (在前一牌組內卡片都復習完後)", + REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM: "亂序 (在前一牌組內卡片都復習完後)", + REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "牌組及卡片都亂序", DISABLE_CLOZE_CARDS: "停用填空克漏字卡片?", CONVERT_HIGHLIGHTS_TO_CLOZES: "將 ==高亮== 轉換為填空克漏字?", CONVERT_BOLD_TEXT_TO_CLOZES: "將 **粗體** 轉換為填空克漏字?", diff --git a/src/main.ts b/src/main.ts index 237c0fbe..bcd14c79 100644 --- a/src/main.ts +++ b/src/main.ts @@ -368,11 +368,7 @@ export default class SRPlugin extends Plugin { async sync(): Promise { // this.clock_start = Date.now(); - if (this.data.settings.dataLocation != DataLocation.SaveOnNoteFile) { - await this.sync_Algo(); - // this.getTimeDuration(this.sync.name); - return; - } + const settings = this.data.settings; if (this.syncLock) { return; @@ -387,8 +383,6 @@ export default class SRPlugin extends Plugin { this.linkRank = new LinkRank(this.data.settings, this.app.metadataCache); this.reviewDecks = {}; - this.noteStats = new Stats(); - // reset flashcards stuff const fullDeckTree = new Deck("root", null); @@ -403,33 +397,71 @@ export default class SRPlugin extends Plugin { await this.savePluginData(); } - const notes: TFile[] = this.app.vault.getMarkdownFiles(); - for (const noteFile of notes) { - if ( - this.data.settings.noteFoldersToIgnore.some((folder) => - // note.path.startsWith(folder) - noteFile.path.includes(folder), - ) - ) { - continue; - } + let notes: TFile[] = this.app.vault.getMarkdownFiles(); + notes = notes.filter( + (noteFile) => !isIgnoredPath(this.data.settings.noteFoldersToIgnore, noteFile.path), + ); + this.linkRank.readLinks(notes); + await Promise.all( + notes.map(async (noteFile) => { + const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(noteFile)); + if (topicPath.hasPath) { + const note: Note = await this.loadNote(noteFile, topicPath); + const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate( + note, + this.data.settings, + ); + note.appendCardsToDeck(fullDeckTree); + + if (flashcardsInNoteAvgEase > 0) { + this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase); + } + } + }), + ); + if (settings.dataLocation != DataLocation.SaveOnNoteFile) { + await this.sync_trackfiles(notes); + // this.getTimeDuration(this.sync.name); + } else { + this.sync_onNote(notes); + } - this.linkRank._readLink(noteFile); + // Reviewable cards are all except those with the "edit later" tag + this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(noteFile)); - if (topicPath.hasPath) { - const note: Note = await this.loadNote(noteFile, topicPath); - const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate( - note, - this.data.settings, - ); - note.appendCardsToDeck(fullDeckTree); + // Reviewable cards are all except those with the "edit later" tag + this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - if (flashcardsInNoteAvgEase > 0) { - this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase); - } - } + // sort the deck names + this.deckTree.sortSubdecksList(); + this.remainingDeckTree = DeckTreeFilter.filterForRemainingCards( + this.questionPostponementList, + this.deckTree, + FlashcardReviewMode.Review, + ); + const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator(); + this.cardStats = calc.calculate(this.deckTree); + + if (this.data.settings.showDebugMessages) { + this.showSyncInfo(); + } + if (this.data.settings.showDebugMessages) { + console.log( + "SR: " + + t("SYNC_TIME_TAKEN", { + t: Date.now() - now.valueOf(), + }), + ); + } + + this.updateAndSortDueNotes(); + + this.syncLock = false; + } + + private sync_onNote(notes: TFile[]) { + notes.map((noteFile) => { const fileCachedData = this.app.metadataCache.getFileCache(noteFile) || {}; const frontmatter: FrontMatterCache | Record = @@ -450,7 +482,7 @@ export default class SRPlugin extends Plugin { } } if (shouldIgnore) { - continue; + return; } // file has no scheduling information @@ -464,66 +496,33 @@ export default class SRPlugin extends Plugin { for (const matchedNoteTag of matchedNoteTags) { this.reviewDecks[matchedNoteTag].newNotes.push({ note: noteFile }); } - this.noteStats.incrementNew(); - continue; + return; } const dueUnix: number = window .moment(frontmatter["sr-due"], ["YYYY-MM-DD", "DD-MM-YYYY", "ddd MMM DD YYYY"]) .valueOf(); - for (const matchedNoteTag of matchedNoteTags) { - this.reviewDecks[matchedNoteTag].scheduledNotes.push({ note: noteFile, dueUnix }); - } - const ease: number = frontmatter["sr-ease"]; this.easeByPath.setEaseForPath(noteFile.path, ease); - const nDays: number = Math.ceil((dueUnix - now.valueOf()) / (24 * 3600 * 1000)); - const interval = Number(frontmatter["sr-interval"]); - this.noteStats.update(nDays, interval, ease); - } - - this.linkRank.calcPageRanks(); - - // Reviewable cards are all except those with the "edit later" tag - this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - - // Reviewable cards are all except those with the "edit later" tag - this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - - // sort the deck names - this.deckTree.sortSubdecksList(); - this.remainingDeckTree = DeckTreeFilter.filterForRemainingCards( - this.questionPostponementList, - this.deckTree, - FlashcardReviewMode.Review, - ); - const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator(); - this.cardStats = calc.calculate(this.deckTree); - - if (this.data.settings.showDebugMessages) { - this.showSyncInfo(); - } - - if (this.data.settings.showDebugMessages) { - console.log( - "SR: " + - t("SYNC_TIME_TAKEN", { - t: Date.now() - now.valueOf(), - }), - ); - } - - this.updateAndSortDueNotes(); - this.syncLock = false; + for (const matchedNoteTag of matchedNoteTags) { + this.reviewDecks[matchedNoteTag].scheduledNotes.push({ + note: noteFile, + dueUnix, + interval, + ease, + }); + } + }); } private updateAndSortDueNotes() { this.dueNotesCount = 0; this.dueDatesNotes = {}; + this.noteStats = new Stats(); const now = window.moment(Date.now()); Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => { @@ -541,11 +540,18 @@ export default class SRPlugin extends Plugin { this.dueDatesNotes[nDays] = 0; } this.dueDatesNotes[nDays]++; + this.noteStats.update(nDays, scheduledNote.interval, scheduledNote.ease); }); + this.noteStats.newCount += reviewDeck.newNotes.length; reviewDeck.sortNotes(this.linkRank.pageranks); }); + this.algorithm.setDueDates( + this.noteStats.delayedDays.dict, + this.cardStats.delayedDays.dict, + ); + this.updateStatusBar(); if (this.data.settings.enableNoteReviewPaneOnStartup) this.reviewQueueView.redraw(); @@ -558,115 +564,19 @@ export default class SRPlugin extends Plugin { } // @logExecutionTime() - async sync_Algo(): Promise { - if (this.syncLock) { - return; - } - this.syncLock = true; - const settings = this.data.settings; + async sync_trackfiles(notes: TFile[]): Promise { + // const settings = this.data.settings; const store = this.store; store.data.queues.buildQueue(); - // reset notes stuff - this.easeByPath = new NoteEaseList(this.data.settings); - this.linkRank = new LinkRank(settings, this.app.metadataCache); - this.reviewDecks = {}; - - this.noteStats = new Stats(); - - // reset flashcards stuff - const fullDeckTree = new Deck("root", null); - this.deckTree = new Deck("root", null); - // this.cardStats = {}; - // check trackfile await store.reLoad(); - let now_number = Date.now(); - const now = window.moment(now_number); - const todayDate: string = now.format("YYYY-MM-DD"); - // clear bury list if we've changed dates - if (todayDate !== this.data.buryDate) { - now_number = null; - this.data.buryDate = todayDate; - this.questionPostponementList.clear(); - - // The following isn't needed for plug-in functionality; but can aid during debugging - await this.savePluginData(); - } - - let notes: TFile[] = this.app.vault.getMarkdownFiles(); - notes = notes.filter( - (noteFile) => !isIgnoredPath(settings.noteFoldersToIgnore, noteFile.path), - ); - this.linkRank.readLinks(notes); - const itdecks = new ItemToDecks(this.data.settings); - await Promise.all( - notes.map(async (noteFile) => { - const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(noteFile)); - if (topicPath.hasPath) { - const note: Note = await this.loadNote(noteFile, topicPath); - const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate( - note, - this.data.settings, - ); - note.appendCardsToDeck(fullDeckTree); - - if (flashcardsInNoteAvgEase > 0) { - this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase); - } - } - }), - ); - itdecks.itemToReviewDecks(this.reviewDecks, notes, this.easeByPath); - Object.values(this.reviewDecks).forEach((deck) => { - deck.newNotes.forEach(() => this.noteStats.incrementNew()); - deck.scheduledNotes.forEach((snote) => this.noteStats.updateStats(snote.item)); - }); - this.dueNotesCount = Object.values(this.reviewDecks) - .map((deck) => { - return deck.scheduledNotes - .map((sNote) => sNote.dueUnix) - .filter((due) => due < now.valueOf()); - }) - .flat().length; - - // Reviewable cards are all except those with the "edit later" tag - this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree); - - // sort the deck names - this.deckTree.sortSubdecksList(); - this.remainingDeckTree = DeckTreeFilter.filterForRemainingCards( - this.questionPostponementList, - this.deckTree, - FlashcardReviewMode.Review, - ); - const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator(); - this.cardStats = calc.calculate(this.deckTree); - // this.noteStats = calc.calculate(this.deckTree); - this.algorithm.setDueDates( - this.noteStats.delayedDays.dict, - this.cardStats.delayedDays.dict, + ItemToDecks.create(this.data.settings).itemToReviewDecks( + this.reviewDecks, + notes, + this.easeByPath, ); - - if (this.data.settings.showDebugMessages) { - this.showSyncInfo(); - } - - for (const deckKey in this.reviewDecks) { - this.reviewDecks[deckKey].sortNotes(this.linkRank.pageranks); - } - - if (this.data.settings.showDebugMessages) { - console.log( - "SR: " + - t("SYNC_TIME_TAKEN", { - t: Date.now() - now.valueOf(), - }), - ); - } - this.updateAndSortDueNotes(); - this.syncLock = false; } async loadNote(noteFile: TFile, topicPath: TopicPath): Promise { @@ -683,8 +593,10 @@ export default class SRPlugin extends Plugin { new Notice(t("NOTE_IN_IGNORED_FOLDER")); return; } + let result: { sNote: SchedNote; buryList?: string[] }; + let ease = this.getLinkedEase(note); - if (this.data.settings.dataLocation !== DataLocation.SaveOnNoteFile) { + if (settings.dataLocation !== DataLocation.SaveOnNoteFile) { let deckName = Tags.getNoteDeckName(note, settings); if (deckName == null && !this.store.getTrackedFile(note.path)?.isTrackedNote) { new Notice(t("PLEASE_TAG_NOTE")); @@ -695,27 +607,28 @@ export default class SRPlugin extends Plugin { } if (deckName == null) return; const opt = this.algorithm.srsOptions()[response]; - const ease = this.getLinkedEase(note); - const store = this.store; - const fileId = store.getTrackedFile(note.path).noteID; - const item = store.getItembyID(fileId); - this.noteStats.decrementStats(item); - const result = ReviewNote.saveReviewResponsebyAlgo( - this.reviewDecks[deckName], + + result = ReviewNote.saveReviewResponse_trackfiles( note, opt, settings.burySiblingCards, ease, ); - this.noteStats.updateStats(item); if (settings.burySiblingCards) { this.data.buryList.push(...result.buryList); await this.savePluginData(); } - - this.postResponse(note, result.dueNum); - return; + } else { + // let ease = this.linkRank.getContribution(note, this.easeByPath).ease; + ease = Math.round(ease); + result = await this.saveReviewResponse_onNote(note, response, ease); + this.easeByPath.setEaseForPath(note.path, ease); } + // Update note's properties to update our due notes. + this.postponeResponse(note, result.sNote); + } + + async saveReviewResponse_onNote(note: TFile, response: ReviewResponse, ease: number) { const fileCachedData = this.app.metadataCache.getFileCache(note) || {}; const frontmatter: FrontMatterCache | Record = fileCachedData.frontmatter || {}; @@ -739,7 +652,7 @@ export default class SRPlugin extends Plugin { } let fileText: string = await this.app.vault.read(note); - let ease: number, interval: number, delayBeforeReview: number; + let interval: number, delayBeforeReview: number; const now: number = Date.now(); // new note if ( @@ -749,7 +662,7 @@ export default class SRPlugin extends Plugin { Object.prototype.hasOwnProperty.call(frontmatter, "sr-ease") ) ) { - ease = this.linkRank.getContribution(note, this.easeByPath).ease; + // ease = this.linkRank.getContribution(note, this.easeByPath).ease; ease = Math.round(ease); interval = 1.0; delayBeforeReview = 0; @@ -800,6 +713,8 @@ export default class SRPlugin extends Plugin { `sr-ease: ${ease}\n---\n\n${fileText}`; } + await this.app.vault.modify(note, fileText); + if (this.data.settings.burySiblingCards) { const topicPath: TopicPath = this.findTopicPath(this.createSrTFile(note)); const noteX: Note = await this.loadNote(note, topicPath); @@ -808,22 +723,21 @@ export default class SRPlugin extends Plugin { } await this.savePluginData(); } - await this.app.vault.modify(note, fileText); - // Update note's properties to update our due notes. - this.easeByPath.setEaseForPath(note.path, ease); - this.postResponse(note, due.valueOf()); + return { sNote: { note, dueUnix: due.valueOf() } }; } - private postResponse(note: TFile, dueNum: number) { + private postponeResponse(note: TFile, sNote: SchedNote) { Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => { let wasDueInDeck = false; - for (const scheduledNote of reviewDeck.scheduledNotes) { - if (scheduledNote.note.path === note.path) { - scheduledNote.dueUnix = dueNum; - wasDueInDeck = true; - break; - } + const result = reviewDeck.scheduledNotes.splice( + reviewDeck.scheduledNotes.findIndex((newNote) => newNote.note.path === note.path), + 1, + sNote, + ); + if (result.length > 0) { + return; + wasDueInDeck = true; } // It was a new note, remove it from the new notes and schedule it. @@ -832,7 +746,7 @@ export default class SRPlugin extends Plugin { reviewDeck.newNotes.findIndex((newNote) => newNote.note.path === note.path), 1, ); - reviewDeck.scheduledNotes.push({ note, dueUnix: dueNum }); + reviewDeck.scheduledNotes.push(sNote); } }); diff --git a/src/reviewNote/review-note.ts b/src/reviewNote/review-note.ts index da89266d..ac0ace12 100644 --- a/src/reviewNote/review-note.ts +++ b/src/reviewNote/review-note.ts @@ -1,5 +1,6 @@ import { Notice, TFile } from "obsidian"; import { DataStore } from "src/dataStore/data"; +import { DataLocation } from "src/dataStore/dataLocation"; import { ItemToDecks } from "src/dataStore/itemToDecks"; import { reviewResponseModal } from "src/gui/reviewresponse-modal"; import { t } from "src/lang/helpers"; @@ -12,6 +13,16 @@ export class ReviewNote { static itemId: number; static minNextView: number; + settings: SRSettings; + + static create(settings: SRSettings, location: DataLocation) { + return new ReviewNote(settings); + } + + constructor(settings: SRSettings) { + this.settings = settings; + } + /** * 231215-not used yet. * after checking ignored folder, get note deckname from review tag and trackedfile. @@ -30,7 +41,10 @@ export class ReviewNote { let deckName = Tags.getNoteDeckName(note, settings); - if (deckName == null && !store.getTrackedFile(note.path)?.isTrackedNote) { + if ( + deckName == null || + (!settings.untrackWithReviewTag && !store.getTrackedFile(note.path)?.isTrackedNote) + ) { new Notice(t("PLEASE_TAG_NOTE")); return; } else if (deckName == null) { @@ -39,8 +53,7 @@ export class ReviewNote { return deckName; } - static saveReviewResponsebyAlgo( - deck: ReviewDeck, + static saveReviewResponse_trackfiles( note: TFile, option: string, burySiblingCards: boolean, @@ -49,8 +62,8 @@ export class ReviewNote { const store = DataStore.getInstance(); const now = Date.now(); - const fileId = store.getTrackedFile(note.path).noteID; - const item = store.getItembyID(fileId); + const itemId = store.getTrackedFile(note.path).noteID; + const item = store.getItembyID(itemId); if (item.isNew && ease != null) { // new note item.updateAlgorithmData("ease", ease); @@ -65,11 +78,20 @@ export class ReviewNote { } } - ReviewNote.recallReviewResponse(fileId, option); + ReviewNote.recallReviewResponse(itemId, option); // preUpdateDeck(deck, note); // ItemToDecks.toRevDeck(deck, note, now); - return { buryList, dueNum: item.nextReview }; + return { + buryList, + sNote: { + note, + item, + dueUnix: item.nextReview, + interval: item.interval, + ease: item.ease, + }, + }; } static recallReviewNote(settings: SRSettings) { diff --git a/src/stats.ts b/src/stats.ts index 814dced0..8e1bb719 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -62,7 +62,7 @@ export class Stats { updateStats(item: RepetitionItem, now?: number) { const scheduling = item?.getSched(); - if (item == null || !item.isDue || scheduling == null) { + if (item == null || !item.hasDue || scheduling == null) { this.incrementNew(); return; } @@ -102,7 +102,7 @@ export class Stats { if (now == undefined) { now = Date.now(); } - if (item.isDue && item.nextReview - now < 0) { + if (item.hasDue && item.nextReview - now < 0) { this.decrementOnDue(); } else if (item.isNew) { this.decrementNew(); diff --git a/src/util/DateProvider.ts b/src/util/DateProvider.ts index b995818d..5df68e9f 100644 --- a/src/util/DateProvider.ts +++ b/src/util/DateProvider.ts @@ -4,11 +4,16 @@ import { ALLOWED_DATE_FORMATS } from "src/constants"; export interface IDateProvider { get today(): Moment; + get endofToday(): Moment; } export class LiveDateProvider implements IDateProvider { get today(): Moment { - return moment().startOf("day"); + // return moment().startOf("day"); + return moment(); + } + get endofToday(): Moment { + return moment().endOf("day"); } } @@ -22,6 +27,9 @@ export class StaticDateProvider implements IDateProvider { get today(): Moment { return this.moment.clone(); } + get endofToday(): Moment { + return this.moment.clone().endOf("day"); + } static fromDateStr(str: string): StaticDateProvider { return new StaticDateProvider(DateUtil.dateStrToMoment(str)); diff --git a/tests/unit/data.test.ts b/tests/unit/data.test.ts index f29dcf69..0dfe7e2e 100644 --- a/tests/unit/data.test.ts +++ b/tests/unit/data.test.ts @@ -27,18 +27,18 @@ export class SampleDataStore { algo.updateSettings(settings.algorithmSettings[algorithmNames.Default]); const opts = algo.srsOptions(); // eslint-disable-next-line prefer-const - arr = Array.from(new Array(50)).map((_v, _idx) => { + arr = Array.from(new Array(30)).map((_v, _idx) => { const type = this.roundInt(1) > 0 ? RPITEMTYPE.CARD : RPITEMTYPE.NOTE; store.trackFile("testPath" + _idx, type, true); if (type === RPITEMTYPE.CARD) { const tkfile = store.data.trackedFiles[_idx]; - Array.from(Array(this.roundInt(20))).map((_v, _idx) => { + Array.from(Array(this.roundInt(10))).map((_v, _idx) => { const carditem = tkfile.trackCard(_idx * 3, "chash" + _idx); store.updateCardItems(tkfile, carditem, this.roundInt(10), "fcard", false); }); } - return this.roundInt(100); + return this.roundInt(50); }); const noteStats = new Stats(); const cardStats = new Stats(); From e54b300560e9edc870c06c12000db44134a4e059 Mon Sep 17 00:00:00 2001 From: Newdea <9208450+Newdea@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:42:55 +0800 Subject: [PATCH 2/3] bump version to v1.11.1.2 --- docs/changelog.md | 9 +++++++++ manifest.json | 2 +- package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a8634cba..a49f77d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + + +## [1.11.1.2] + +- merge master branch; +- fix: 当天新卡片复习后,当天无法再次复习 的问题; +- fix: 卡片统计表中,当天复习数据没有更新 的问题; + + ## [1.11.0.1] - fix #34, untrack error; diff --git a/manifest.json b/manifest.json index 2430a4f5..91e9557c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-spaced-repetition-recall", "name": "Spaced Repetition Recall", - "version": "1.11.0.1", + "version": "1.11.1.2", "minAppVersion": "0.15.4", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "author": "Newdea", diff --git a/package.json b/package.json index d32d6bb7..d56ad3a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-spaced-repetition", - "version": "1.11.0.1", + "version": "1.11.1.2", "description": "Fight the forgetting curve by reviewing flashcards & entire notes.", "main": "main.js", "scripts": { From c2ed1eb8f29bf23ad7933eff7289d7739af8200c Mon Sep 17 00:00:00 2001 From: Newdea Date: Wed, 7 Feb 2024 15:44:53 +0000 Subject: [PATCH 3/3] Prettified Code! --- docs/changelog.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a49f77d4..a972ff91 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,14 +4,11 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - - ## [1.11.1.2] -- merge master branch; -- fix: 当天新卡片复习后,当天无法再次复习 的问题; -- fix: 卡片统计表中,当天复习数据没有更新 的问题; - +- merge master branch; +- fix: 当天新卡片复习后,当天无法再次复习 的问题; +- fix: 卡片统计表中,当天复习数据没有更新 的问题; ## [1.11.0.1]