diff --git a/src/flashcard-review-sequencer.ts b/src/flashcard-review-sequencer.ts index 558022c6..180ba7b2 100644 --- a/src/flashcard-review-sequencer.ts +++ b/src/flashcard-review-sequencer.ts @@ -25,21 +25,68 @@ export interface IFlashcardReviewSequencer { setDeckTree(originalDeckTree: Deck, remainingDeckTree: Deck): void; setCurrentDeck(topicPath: TopicPath): void; getDeckStats(topicPath: TopicPath): DeckStats; + getSubDecksWithCardsInQueue(deck: Deck): Deck[]; skipCurrentCard(): void; determineCardSchedule(response: ReviewResponse, card: Card): RepItemScheduleInfo; processReview(response: ReviewResponse): Promise; updateCurrentQuestionText(text: string): Promise; } +/** + * Represents statistics for a deck and its subdecks. + * + * @property {number} totalCount - Total number of cards in this deck and all subdecks. + * @property {number} dueCount - Number of due cards in this deck and all subdecks. + * @property {number} newCount - Number of new cards in this deck and all subdecks. + * @property {number} cardsInQueueCount - Number of cards in the queue of this deck and all subdecks. + * @property {number} dueCardsInQueueOfThisDeckCount - Number of due cards just in this deck. + * @property {number} newCardsInQueueOfThisDeckCount - Number of new cards just in this deck. + * @property {number} cardsInQueueOfThisDeckCount - Total number of cards in queue just in this deck. + * @property {number} subDecksInQueueOfThisDeckCount - Number of subdecks in the queue just in this deck. + * @property {number} decksInQueueOfThisDeckCount - Total number of decks in the queue including this deck and its subdecks. + * + * @constructor + * @param {number} totalCount - Initializes the total count of cards. + * @param {number} dueCount - Initializes the due count of cards. + * @param {number} newCount - Initializes the new count of cards. + * @param {number} cardsInQueueCount - Initializes the count of cards in the queue. + * @param {number} dueCardsInQueueOfThisDeckCount - Initializes the count of due cards just in this deck. + * @param {number} newCardsInQueueOfThisDeckCount - Initializes the count of new cards just in this deck. + * @param {number} cardsInQueueOfThisDeckCount - Initializes the count of all cards in the queue just in this deck. + * @param {number} subDecksInQueueOfThisDeckCount - Initializes the count of subdecks in the queue just in this deck. + * @param {number} decksInQueueOfThisDeckCount - Initializes the count of all decks in the queue including this deck and its subdecks. + */ export class DeckStats { + totalCount: number; dueCount: number; newCount: number; - totalCount: number; + cardsInQueueCount: number; + dueCardsInQueueOfThisDeckCount: number; + newCardsInQueueOfThisDeckCount: number; + cardsInQueueOfThisDeckCount: number; + subDecksInQueueOfThisDeckCount: number; + decksInQueueOfThisDeckCount: number; - constructor(dueCount: number, newCount: number, totalCount: number) { + constructor( + totalCount: number, + dueCount: number, + newCount: number, + cardsInQueueCount: number, + dueCardsInQueueOfThisDeckCount: number, + newCardsInQueueOfThisDeckCount: number, + cardsInQueueOfThisDeckCount: number, + subDecksInQueueOfThisDeckCount: number, + decksInQueueOfThisDeckCount: number, + ) { this.dueCount = dueCount; this.newCount = newCount; this.totalCount = totalCount; + this.cardsInQueueCount = cardsInQueueCount; + this.dueCardsInQueueOfThisDeckCount = dueCardsInQueueOfThisDeckCount; + this.newCardsInQueueOfThisDeckCount = newCardsInQueueOfThisDeckCount; + this.cardsInQueueOfThisDeckCount = cardsInQueueOfThisDeckCount; + this.subDecksInQueueOfThisDeckCount = subDecksInQueueOfThisDeckCount; + this.decksInQueueOfThisDeckCount = decksInQueueOfThisDeckCount; } } @@ -123,7 +170,53 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer { const remainingDeck: Deck = this.remainingDeckTree.getDeck(topicPath); const newCount: number = remainingDeck.getDistinctCardCount(CardListType.NewCard, true); const dueCount: number = remainingDeck.getDistinctCardCount(CardListType.DueCard, true); - return new DeckStats(dueCount, newCount, totalCount); + + // Sry for the long variable names, but I needed all these distinct counts in the UI + const newCardsInQueueOfThisDeckCount = remainingDeck.getDistinctCardCount( + CardListType.NewCard, + false, + ); + const dueCardsInQueueOfThisDeckCount = remainingDeck.getDistinctCardCount( + CardListType.DueCard, + false, + ); + const cardsInQueueOfThisDeckCount = + newCardsInQueueOfThisDeckCount + dueCardsInQueueOfThisDeckCount; + + const subDecksInQueueOfThisDeckCount = + this.getSubDecksWithCardsInQueue(remainingDeck).length; + const decksInQueueOfThisDeckCount = + cardsInQueueOfThisDeckCount > 0 + ? subDecksInQueueOfThisDeckCount + 1 + : subDecksInQueueOfThisDeckCount; + + return new DeckStats( + totalCount, + dueCount, + newCount, + dueCount + newCount, + dueCardsInQueueOfThisDeckCount, + newCardsInQueueOfThisDeckCount, + cardsInQueueOfThisDeckCount, + subDecksInQueueOfThisDeckCount, + decksInQueueOfThisDeckCount, + ); + } + + getSubDecksWithCardsInQueue(deck: Deck): Deck[] { + let subDecksWithCardsInQueue: Deck[] = []; + + deck.subdecks.forEach((subDeck) => { + subDecksWithCardsInQueue = subDecksWithCardsInQueue.concat( + this.getSubDecksWithCardsInQueue(subDeck), + ); + + const newCount: number = subDeck.getDistinctCardCount(CardListType.NewCard, false); + const dueCount: number = subDeck.getDistinctCardCount(CardListType.DueCard, false); + if (newCount + dueCount > 0) subDecksWithCardsInQueue.push(subDeck); + }); + + return subDecksWithCardsInQueue; } skipCurrentCard(): void { diff --git a/src/gui/card-ui.tsx b/src/gui/card-ui.tsx index 82a5c25c..e22efb6a 100644 --- a/src/gui/card-ui.tsx +++ b/src/gui/card-ui.tsx @@ -5,7 +5,7 @@ import { RepItemScheduleInfo } from "src/algorithms/base/rep-item-schedule-info" import { ReviewResponse } from "src/algorithms/base/repetition-item"; import { textInterval } from "src/algorithms/osr/note-scheduling"; import { Card } from "src/card"; -import { CardListType, Deck } from "src/deck"; +import { Deck } from "src/deck"; import { FlashcardReviewMode, IFlashcardReviewSequencer as IFlashcardReviewSequencer, @@ -21,35 +21,36 @@ import { RenderMarkdownWrapper } from "src/utils/renderers"; export class CardUI { public app: App; public plugin: SRPlugin; - public contentEl: HTMLElement; - public parentEl: HTMLElement; public mode: FlashcardMode; public view: HTMLDivElement; - public header: HTMLDivElement; - public chosenDeckLabel: HTMLDivElement; - public chosenDeckText: HTMLDivElement; - public chosenDeckSubDeckNumText: HTMLDivElement; - public chosenDeckCardNumIcon: HTMLDivElement; - public chosenDeckDeckNumIcon: HTMLDivElement; + public infoBar: HTMLDivElement; - public deckDivider: HTMLDivElement; + public chosenDeckInfo: HTMLDivElement; + public chosenDeckName: HTMLDivElement; + public chosenDeckCardCounter: HTMLDivElement; + public chosenDeckCardCounterIcon: HTMLDivElement; + public chosenDeckSubDeckCounter: HTMLDivElement; + public chosenDeckSubDeckCounterIcon: HTMLDivElement; - public currentDeckLabel: HTMLDivElement; - public currentDeckText: HTMLDivElement; - public currentDeckCardNumIcon: HTMLDivElement; + public deckInfoDivider: HTMLDivElement; + + public currentDeckInfo: HTMLDivElement; + public currentDeckName: HTMLDivElement; + public currentDeckCardCounter: HTMLDivElement; + public currentDeckCardCounterIcon: HTMLDivElement; + + public cardContext: HTMLElement; + + public content: HTMLDivElement; public controls: HTMLDivElement; public editButton: HTMLButtonElement; public resetButton: HTMLButtonElement; public infoButton: HTMLButtonElement; public skipButton: HTMLButtonElement; - public headerDivider: HTMLHRElement; - - public content: HTMLDivElement; - public context: HTMLElement; - public contextLandscape: HTMLElement; + public controlsDivider: HTMLHRElement; public response: HTMLDivElement; public hardButton: HTMLButtonElement; @@ -59,6 +60,13 @@ export class CardUI { public lastPressed: number; private chosenDeck: Deck | null; + private totalCardsInSession: number = 0; + private totalDecksInSession: number = 0; + + private currentDeck: Deck | null; + private previousDeck: Deck | null; + private currentDeckTotalCardsInQueue: number = 0; + private reviewSequencer: IFlashcardReviewSequencer; private settings: SRSettings; private reviewMode: FlashcardReviewMode; @@ -71,8 +79,7 @@ export class CardUI { settings: SRSettings, reviewSequencer: IFlashcardReviewSequencer, reviewMode: FlashcardReviewMode, - contentEl: HTMLElement, - parentEl: HTMLElement, + view: HTMLDivElement, backToDeck: () => void, editClickHandler: () => void, ) { @@ -84,60 +91,60 @@ export class CardUI { this.reviewMode = reviewMode; this.backToDeck = backToDeck; this.editClickHandler = editClickHandler; - this.contentEl = contentEl; - this.parentEl = parentEl; + this.view = view; this.chosenDeck = null; // Build ui this.init(); } + // #region -> public methods + /** * Initializes all static elements in the FlashcardView */ init() { - this.view = this.contentEl.createDiv(); this.view.addClasses(["sr-flashcard", "sr-is-hidden"]); - this.header = this.view.createDiv(); - this.header.addClass("sr-header"); + this.controls = this.view.createDiv(); + this.controls.addClass("sr-controls"); - this.chosenDeckLabel = this.header.createDiv(); - this.chosenDeckLabel.addClass("sr-chosen-deck-label"); - this.chosenDeckText = this.chosenDeckLabel.createDiv(); + this._createCardControls(); - this.chosenDeckCardNumIcon = this.chosenDeckLabel.createDiv(); - setIcon(this.chosenDeckCardNumIcon, "credit-card"); + this.infoBar = this.view.createDiv(); + this.infoBar.addClass("sr-info-bar"); - this.chosenDeckSubDeckNumText = this.chosenDeckLabel.createDiv(); - this.chosenDeckSubDeckNumText.addClass("sr-chosen-deck-deck-num"); + this.chosenDeckInfo = this.infoBar.createDiv(); + this.chosenDeckInfo.addClass("sr-chosen-deck-info"); + this.chosenDeckName = this.chosenDeckInfo.createDiv(); + this.chosenDeckCardCounter = this.chosenDeckInfo.createDiv(); - this.chosenDeckDeckNumIcon = this.chosenDeckLabel.createDiv(); - setIcon(this.chosenDeckDeckNumIcon, "layers"); + this.chosenDeckCardCounterIcon = this.chosenDeckInfo.createDiv(); + setIcon(this.chosenDeckCardCounterIcon, "credit-card"); - this.deckDivider = this.header.createDiv(); - this.deckDivider.addClass("sr-deck-divider"); - this.deckDivider.addClass("sr-is-hidden"); - this.deckDivider.setText(">>"); + this.chosenDeckSubDeckCounter = this.chosenDeckInfo.createDiv(); + this.chosenDeckSubDeckCounter.addClass("sr-chosen-deck-deck-num"); - this.currentDeckLabel = this.header.createDiv(); - this.currentDeckLabel.addClass("sr-is-hidden"); - this.currentDeckLabel.addClass("sr-current-deck-label"); + this.chosenDeckSubDeckCounterIcon = this.chosenDeckInfo.createDiv(); + setIcon(this.chosenDeckSubDeckCounterIcon, "layers"); - this.currentDeckText = this.currentDeckLabel.createDiv(); - this.currentDeckCardNumIcon = this.currentDeckLabel.createDiv(); - setIcon(this.currentDeckCardNumIcon, "credit-card"); + this.deckInfoDivider = this.infoBar.createDiv(); + this.deckInfoDivider.addClass("sr-deck-divider"); + this.deckInfoDivider.addClass("sr-is-hidden"); + this.deckInfoDivider.setText("|"); - this.controls = this.view.createDiv(); - this.controls.addClass("sr-controls"); + this.currentDeckInfo = this.infoBar.createDiv(); + this.currentDeckInfo.addClass("sr-is-hidden"); + this.currentDeckInfo.addClass("sr-current-deck-info"); - this._createCardControls(); - - this.headerDivider = this.view.createEl("hr"); + this.currentDeckName = this.currentDeckInfo.createDiv(); + this.currentDeckCardCounter = this.currentDeckInfo.createDiv(); + this.currentDeckCardCounterIcon = this.currentDeckInfo.createDiv(); + setIcon(this.currentDeckCardCounterIcon, "credit-card"); if (this.settings.showContextInCards) { - this.context = this.view.createDiv(); - this.context.addClass("sr-context"); + this.cardContext = this.infoBar.createDiv(); + this.cardContext.addClass("sr-context"); } this.content = this.view.createDiv(); @@ -153,16 +160,19 @@ export class CardUI { * Shows the FlashcardView if it is hidden */ async show(chosenDeck: Deck) { + // Prevents rest of code, from running if this was executed multiple times after one another if (!this.view.hasClass("sr-is-hidden")) { return; } + this.chosenDeck = chosenDeck; + const deckStats = this.reviewSequencer.getDeckStats(chosenDeck.getTopicPath()); + this.totalCardsInSession = deckStats.cardsInQueueCount; + this.totalDecksInSession = deckStats.decksInQueueOfThisDeckCount; await this._drawContent(); - // Prevents the following code, from running if this show is just a redraw and not an unhide this.view.removeClass("sr-is-hidden"); - // this.backButton.removeClass("sr-is-hidden"); document.addEventListener("keydown", this._keydownHandler); } @@ -177,14 +187,13 @@ export class CardUI { * Hides the FlashcardView if it is visible */ hide() { - // Prevents the following code, from running if this was executed multiple times after one another + // Prevents the rest of code, from running if this was executed multiple times after one another if (this.view.hasClass("sr-is-hidden")) { return; } document.removeEventListener("keydown", this._keydownHandler); this.view.addClass("sr-is-hidden"); - // this.backButton.addClass("sr-is-hidden"); } /** @@ -198,28 +207,29 @@ export class CardUI { // #region -> Functions & helpers private async _drawContent() { - this.mode = FlashcardMode.Front; - const currentDeck: Deck = this.reviewSequencer.currentDeck; - - // Setup title - this._setChosenDeckLabel(this.chosenDeck); - this._setCurrentDeckLabel(this.chosenDeck, currentDeck); this.resetButton.disabled = true; - // Setup context - if (this.settings.showContextInCards) { - this.context.setText( - this._formatQuestionContextText(this._currentQuestion.questionContext), + // Update current deck info + this.mode = FlashcardMode.Front; + this.previousDeck = this.currentDeck; + this.currentDeck = this.reviewSequencer.currentDeck; + if (this.previousDeck !== this.currentDeck) { + const currentDeckStats = this.reviewSequencer.getDeckStats( + this.currentDeck.getTopicPath(), ); + this.currentDeckTotalCardsInQueue = currentDeckStats.cardsInQueueOfThisDeckCount; } - // Setup card content + this._updateInfoBar(this.chosenDeck, this.currentDeck); + + // Update card content this.content.empty(); const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( this.app, this.plugin, this._currentNote.filePath, ); + await wrapper.renderMarkdownWrapper( this._currentCard.front.trimStart(), this.content, @@ -228,97 +238,10 @@ export class CardUI { // Set scroll position back to top this.content.scrollTop = 0; - // Setup response buttons + // Update response buttons this._resetResponseButtons(); } - private _keydownHandler = (e: KeyboardEvent) => { - // Prevents any input, if the edit modal is open or if the view is not in focus - if ( - document.activeElement.nodeName === "TEXTAREA" || - this.mode === FlashcardMode.Closed || - !this.plugin.getSRInFocusState() - ) { - return; - } - - const consumeKeyEvent = () => { - e.preventDefault(); - e.stopPropagation(); - }; - - switch (e.code) { - case "KeyS": - this._skipCurrentCard(); - consumeKeyEvent(); - break; - case "Space": - if (this.mode === FlashcardMode.Front) { - this._showAnswer(); - consumeKeyEvent(); - } else if (this.mode === FlashcardMode.Back) { - this._processReview(ReviewResponse.Good); - consumeKeyEvent(); - } - break; - case "Enter": - case "NumpadEnter": - if (this.mode !== FlashcardMode.Front) { - break; - } - this._showAnswer(); - consumeKeyEvent(); - break; - case "Numpad1": - case "Digit1": - if (this.mode !== FlashcardMode.Back) { - break; - } - this._processReview(ReviewResponse.Hard); - consumeKeyEvent(); - break; - case "Numpad2": - case "Digit2": - if (this.mode !== FlashcardMode.Back) { - break; - } - this._processReview(ReviewResponse.Good); - consumeKeyEvent(); - break; - case "Numpad3": - case "Digit3": - if (this.mode !== FlashcardMode.Back) { - break; - } - this._processReview(ReviewResponse.Easy); - consumeKeyEvent(); - break; - case "Numpad0": - case "Digit0": - if (this.mode !== FlashcardMode.Back) { - break; - } - this._processReview(ReviewResponse.Reset); - consumeKeyEvent(); - break; - default: - break; - } - }; - - private _displayCurrentCardInfoNotice() { - const schedule = this._currentCard.scheduleInfo; - - const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.latestEase ?? t("NEW")); - const currentIntervalStr = - t("CURRENT_INTERVAL_HELP_TEXT") + textInterval(schedule?.interval, false); - const generatedFromStr = t("CARD_GENERATED_FROM", { - notePath: this._currentQuestion.note.filePath, - }); - - new Notice(currentEaseStr + "\n" + currentIntervalStr + "\n" + generatedFromStr); - } - private get _currentCard(): Card { return this.reviewSequencer.currentCard; } @@ -331,68 +254,6 @@ export class CardUI { return this.reviewSequencer.currentNote; } - private _showAnswer(): void { - const timeNow = now(); - if ( - this.lastPressed && - timeNow - this.lastPressed < this.plugin.data.settings.reviewButtonDelay - ) { - return; - } - this.lastPressed = timeNow; - - this.mode = FlashcardMode.Back; - - this.resetButton.disabled = false; - - // Show answer text - if (this._currentQuestion.questionType !== CardType.Cloze) { - const hr: HTMLElement = document.createElement("hr"); - this.content.appendChild(hr); - } else { - this.content.empty(); - } - - const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( - this.app, - this.plugin, - this._currentNote.filePath, - ); - wrapper.renderMarkdownWrapper( - this._currentCard.back, - this.content, - this._currentQuestion.questionText.textDirection, - ); - - // Show response buttons - this.answerButton.addClass("sr-is-hidden"); - this.hardButton.removeClass("sr-is-hidden"); - this.easyButton.removeClass("sr-is-hidden"); - - if (this.reviewMode === FlashcardReviewMode.Cram) { - this.response.addClass("is-cram"); - this.hardButton.setText(`${this.settings.flashcardHardText}`); - this.easyButton.setText(`${this.settings.flashcardEasyText}`); - } else { - this.goodButton.removeClass("sr-is-hidden"); - this._setupEaseButton( - this.hardButton, - this.settings.flashcardHardText, - ReviewResponse.Hard, - ); - this._setupEaseButton( - this.goodButton, - this.settings.flashcardGoodText, - ReviewResponse.Good, - ); - this._setupEaseButton( - this.easyButton, - this.settings.flashcardEasyText, - ReviewResponse.Easy, - ); - } - } - private async _processReview(response: ReviewResponse): Promise { const timeNow = now(); if ( @@ -404,83 +265,14 @@ export class CardUI { this.lastPressed = timeNow; await this.reviewSequencer.processReview(response); - await this._handleSkipCard(); + await this._showNextCard(); } - private async _skipCurrentCard(): Promise { - this.reviewSequencer.skipCurrentCard(); - await this._handleSkipCard(); - } - - private async _handleSkipCard(): Promise { + private async _showNextCard(): Promise { if (this._currentCard != null) await this.refresh(); else this.backToDeck(); } - private _formatQuestionContextText(questionContext: string[]): string { - const separator: string = " > "; - let result = this._currentNote.file.basename; - questionContext.forEach((context) => { - // Check for links trim [[ ]] - if (context.startsWith("[[") && context.endsWith("]]")) { - context = context.replace("[[", "").replace("]]", ""); - // Use replacement text if any - if (context.contains("|")) { - context = context.split("|")[1]; - } - } - result += separator + context; - }); - return result; - } - - // #region -> Header - - private _setChosenDeckLabel(deck: Deck) { - let text = deck.deckName; - - const deckStats = this.reviewSequencer.getDeckStats(deck.getTopicPath()); - const cardsInQueue = deckStats.dueCount + deckStats.newCount; - text += ` - ${cardsInQueue} x `; - - const subDecksWithCardsInQueue = deck.subdecks.filter((subDeck) => { - const deckStats = this.reviewSequencer.getDeckStats(subDeck.getTopicPath()); - return deckStats.dueCount + deckStats.newCount > 0; - }); - - this.chosenDeckText.setText(text); - this.chosenDeckSubDeckNumText.setText(subDecksWithCardsInQueue.length + " x "); - } - - private _setCurrentDeckLabel(chosenDeck: Deck, currentDeck: Deck) { - if (chosenDeck.subdecks.length === 0) { - if (!this.deckDivider.hasClass("sr-is-hidden")) { - this.deckDivider.addClass("sr-is-hidden"); - } - if (!this.currentDeckLabel.hasClass("sr-is-hidden")) { - this.currentDeckLabel.addClass("sr-is-hidden"); - } - return; - } - - if (this.deckDivider.hasClass("sr-is-hidden")) { - this.deckDivider.removeClass("sr-is-hidden"); - } - - if (this.currentDeckLabel.hasClass("sr-is-hidden")) { - this.currentDeckLabel.removeClass("sr-is-hidden"); - } - - let text = `${currentDeck.deckName} - `; - - const isRandomMode = this.settings.flashcardCardOrder === "EveryCardRandomDeckAndCard"; - if (!isRandomMode) { - text = `${currentDeck.deckName} - ${currentDeck.getCardCount(CardListType.All, false)} x `; - } - - this.currentDeckText.setText(text); - } - // #region -> Controls private _createCardControls() { @@ -530,6 +322,101 @@ export class CardUI { }); } + private async _skipCurrentCard(): Promise { + this.reviewSequencer.skipCurrentCard(); + await this._showNextCard(); + } + + private _displayCurrentCardInfoNotice() { + const schedule = this._currentCard.scheduleInfo; + + const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.latestEase ?? t("NEW")); + const currentIntervalStr = + t("CURRENT_INTERVAL_HELP_TEXT") + textInterval(schedule?.interval, false); + const generatedFromStr = t("CARD_GENERATED_FROM", { + notePath: this._currentQuestion.note.filePath, + }); + + new Notice(currentEaseStr + "\n" + currentIntervalStr + "\n" + generatedFromStr); + } + + // #region -> Deck Info + + private _updateInfoBar(chosenDeck: Deck, currentDeck: Deck) { + this._updateChosenDeckInfo(chosenDeck); + this._updateCurrentDeckInfo(chosenDeck, currentDeck); + this._updateCardContext(); + } + + private _updateChosenDeckInfo(chosenDeck: Deck) { + const chosenDeckStats = this.reviewSequencer.getDeckStats(chosenDeck.getTopicPath()); + + this.chosenDeckName.setText(`${chosenDeck.deckName}`); + this.chosenDeckCardCounter.setText( + `${this.totalCardsInSession - chosenDeckStats.cardsInQueueCount} / ${this.totalCardsInSession}`, + ); + this.chosenDeckSubDeckCounter.setText( + `${this.totalDecksInSession - chosenDeckStats.decksInQueueOfThisDeckCount} / ${this.totalDecksInSession}`, + ); + } + + private _updateCurrentDeckInfo(chosenDeck: Deck, currentDeck: Deck) { + if (chosenDeck.subdecks.length === 0) { + if (!this.deckInfoDivider.hasClass("sr-is-hidden")) { + this.deckInfoDivider.addClass("sr-is-hidden"); + } + if (!this.currentDeckInfo.hasClass("sr-is-hidden")) { + this.currentDeckInfo.addClass("sr-is-hidden"); + } + return; + } + + if (this.deckInfoDivider.hasClass("sr-is-hidden")) { + this.deckInfoDivider.removeClass("sr-is-hidden"); + } + + if (this.currentDeckInfo.hasClass("sr-is-hidden")) { + this.currentDeckInfo.removeClass("sr-is-hidden"); + } + + this.currentDeckName.setText(`${currentDeck.deckName}`); + + const isRandomMode = this.settings.flashcardCardOrder === "EveryCardRandomDeckAndCard"; + if (!isRandomMode) { + const currentDeckStats = this.reviewSequencer.getDeckStats(currentDeck.getTopicPath()); + this.currentDeckCardCounter.setText( + `${this.currentDeckTotalCardsInQueue - currentDeckStats.cardsInQueueOfThisDeckCount} / ${this.currentDeckTotalCardsInQueue}`, + ); + } + } + + private _updateCardContext() { + if (!this.settings.showContextInCards) { + this.cardContext.setText(""); + return; + } + this.cardContext.setText( + ` > ${this._formatQuestionContextText(this._currentQuestion.questionContext)}`, + ); + } + + private _formatQuestionContextText(questionContext: string[]): string { + const separator: string = " > "; + let result = this._currentNote.file.basename; + questionContext.forEach((context) => { + // Check for links trim [[ ]] + if (context.startsWith("[[") && context.endsWith("]]")) { + context = context.replace("[[", "").replace("]]", ""); + // Use replacement text if any + if (context.contains("|")) { + context = context.split("|")[1]; + } + } + result += separator + context; + }); + return result; + } + // #region -> Response private _createResponseButtons() { @@ -619,4 +506,140 @@ export class CardUI { button.setText(buttonName); } } + + private _showAnswer(): void { + const timeNow = now(); + if ( + this.lastPressed && + timeNow - this.lastPressed < this.plugin.data.settings.reviewButtonDelay + ) { + return; + } + this.lastPressed = timeNow; + + this.mode = FlashcardMode.Back; + + this.resetButton.disabled = false; + + // Show answer text + if (this._currentQuestion.questionType !== CardType.Cloze) { + const hr: HTMLElement = document.createElement("hr"); + this.content.appendChild(hr); + } else { + this.content.empty(); + } + + const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( + this.app, + this.plugin, + this._currentNote.filePath, + ); + wrapper.renderMarkdownWrapper( + this._currentCard.back, + this.content, + this._currentQuestion.questionText.textDirection, + ); + + // Show response buttons + this.answerButton.addClass("sr-is-hidden"); + this.hardButton.removeClass("sr-is-hidden"); + this.easyButton.removeClass("sr-is-hidden"); + + if (this.reviewMode === FlashcardReviewMode.Cram) { + this.response.addClass("is-cram"); + this.hardButton.setText(`${this.settings.flashcardHardText}`); + this.easyButton.setText(`${this.settings.flashcardEasyText}`); + } else { + this.goodButton.removeClass("sr-is-hidden"); + this._setupEaseButton( + this.hardButton, + this.settings.flashcardHardText, + ReviewResponse.Hard, + ); + this._setupEaseButton( + this.goodButton, + this.settings.flashcardGoodText, + ReviewResponse.Good, + ); + this._setupEaseButton( + this.easyButton, + this.settings.flashcardEasyText, + ReviewResponse.Easy, + ); + } + } + + private _keydownHandler = (e: KeyboardEvent) => { + // Prevents any input, if the edit modal is open or if the view is not in focus + if ( + document.activeElement.nodeName === "TEXTAREA" || + this.mode === FlashcardMode.Closed || + !this.plugin.getSRInFocusState() + ) { + return; + } + + const consumeKeyEvent = () => { + e.preventDefault(); + e.stopPropagation(); + }; + + switch (e.code) { + case "KeyS": + this._skipCurrentCard(); + consumeKeyEvent(); + break; + case "Space": + if (this.mode === FlashcardMode.Front) { + this._showAnswer(); + consumeKeyEvent(); + } else if (this.mode === FlashcardMode.Back) { + this._processReview(ReviewResponse.Good); + consumeKeyEvent(); + } + break; + case "Enter": + case "NumpadEnter": + if (this.mode !== FlashcardMode.Front) { + break; + } + this._showAnswer(); + consumeKeyEvent(); + break; + case "Numpad1": + case "Digit1": + if (this.mode !== FlashcardMode.Back) { + break; + } + this._processReview(ReviewResponse.Hard); + consumeKeyEvent(); + break; + case "Numpad2": + case "Digit2": + if (this.mode !== FlashcardMode.Back) { + break; + } + this._processReview(ReviewResponse.Good); + consumeKeyEvent(); + break; + case "Numpad3": + case "Digit3": + if (this.mode !== FlashcardMode.Back) { + break; + } + this._processReview(ReviewResponse.Easy); + consumeKeyEvent(); + break; + case "Numpad0": + case "Digit0": + if (this.mode !== FlashcardMode.Back) { + break; + } + this._processReview(ReviewResponse.Reset); + consumeKeyEvent(); + break; + default: + break; + } + }; } diff --git a/src/gui/deck-ui.tsx b/src/gui/deck-ui.tsx index 6b414b23..c0f4bba6 100644 --- a/src/gui/deck-ui.tsx +++ b/src/gui/deck-ui.tsx @@ -33,14 +33,14 @@ export class DeckUI { plugin: SRPlugin, settings: SRSettings, reviewSequencer: IFlashcardReviewSequencer, - contentEl: HTMLElement, + view: HTMLDivElement, startReviewOfDeck: (deck: Deck) => void, ) { // Init properties this.plugin = plugin; this.settings = settings; this.reviewSequencer = reviewSequencer; - this.contentEl = contentEl; + this.view = view; this.startReviewOfDeck = startReviewOfDeck; // Build ui @@ -51,7 +51,6 @@ export class DeckUI { * Initializes all static elements in the DeckListView */ init(): void { - this.view = this.contentEl.createDiv(); this.view.addClasses(["sr-deck-list", "sr-is-hidden"]); this.header = this.view.createDiv(); diff --git a/src/gui/sr-modal.tsx b/src/gui/sr-modal.tsx index d6e304a4..47f47509 100644 --- a/src/gui/sr-modal.tsx +++ b/src/gui/sr-modal.tsx @@ -66,7 +66,7 @@ export class FlashcardModal extends Modal { this.plugin, this.settings, this.reviewSequencer, - this.contentEl, + this.contentEl.createDiv(), this._startReviewOfDeck.bind(this), ); @@ -76,8 +76,7 @@ export class FlashcardModal extends Modal { this.settings, this.reviewSequencer, this.reviewMode, - this.contentEl, - this.modalEl, + this.contentEl.createDiv(), this._showDecksList.bind(this), this._doEditQuestionText.bind(this), ); diff --git a/src/gui/sr-tab-view.tsx b/src/gui/sr-tab-view.tsx index 3e1710f5..ef305af6 100644 --- a/src/gui/sr-tab-view.tsx +++ b/src/gui/sr-tab-view.tsx @@ -118,7 +118,7 @@ export class SRTabView extends ItemView { this.plugin, this.settings, this.reviewSequencer, - this.viewContentEl, + this.viewContentEl.createDiv(), this._startReviewOfDeck.bind(this), ); } @@ -130,8 +130,7 @@ export class SRTabView extends ItemView { this.settings, this.reviewSequencer, this.reviewMode, - this.viewContentEl, - this.viewContainerEl, + this.viewContentEl.createDiv(), this._showDecksList.bind(this), this._doEditQuestionText.bind(this), ); diff --git a/styles.css b/styles.css index 146af1db..74ecdf28 100644 --- a/styles.css +++ b/styles.css @@ -79,11 +79,6 @@ margin-bottom: 8px; } - .is-mobile:not(.is-tablet) .sr-flashcard .sr-chosen-deck-label > *, - .is-mobile:not(.is-tablet) .sr-flashcard .sr-current-deck-label > * { - font-size: medium; - } - .is-mobile:not(.is-tablet) .sr-flashcard .sr-controls { width: unset !important; flex-direction: column; @@ -94,31 +89,69 @@ padding-left: 8px; } - .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard > hr { - margin: 0 0 0 calc(var(--side-button-clearance)); + .is-mobile:not(.is-tablet) .sr-flashcard .sr-chosen-deck-info > *, + .is-mobile:not(.is-tablet) .sr-flashcard .sr-current-deck-info > * { + font-size: medium; } - /* .is-mobile:not(.is-tablet) .sr-flashcard > hr { - margin: 0 var(--side-button-clearance); - } */ - - .is-mobile:not(.is-tablet) .sr-flashcard .sr-context { + .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard .sr-info-bar { margin: 0 0 0 calc(var(--side-button-clearance)); } + .is-mobile:not(.is-tablet) #sr-modal .sr-flashcard .sr-info-bar { + margin: 0 calc(var(--side-button-clearance)); + } + .is-mobile:not(.is-tablet) .sr-flashcard .sr-content { margin: 0 0 0 calc(var(--side-button-clearance)); } + .is-mobile:not(.is-tablet) .sr-flashcard .sr-content hr { + margin: 2px 0; + } + .is-mobile:not(.is-tablet) .sr-response-button { height: 48px !important; } +} + +@media only screen and (orientation: landscape) and (max-height: 430px) { + .is-mobile:not(.is-tablet) .sr-button { + height: 35px; + width: 35px; + } + + .is-mobile:not(.is-tablet) .sr-response { + margin: 0 0 0 calc(var(--side-button-clearance)); + } - .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard .sr-header { - margin: 0 0 0 var(--side-button-clearance); + .is-mobile:not(.is-tablet) .sr-response-button { + height: 32px !important; + } + + .is-mobile:not(.is-tablet) .sr-flashcard .sr-content p { + margin-block-start: 0.5rem; + margin-block-end: 0.5rem; } } +@media only screen and (orientation: landscape) and (max-height: 400px) { + .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard .sr-info-bar { + --side-button-clearance: calc(var(--side-button-width) * 4) !important; + } + + .is-mobile:not(.is-tablet) .sr-tab-view .sr-back-button { + left: calc(var(--side-button-width) * 3) !important; + top: 0px !important; + } + + .is-mobile:not(.is-tablet) .sr-flashcard { + gap: 4px; + } +} + +/* MARK: Mobile portrait mode */ + @media only screen and (orientation: portrait) { .is-mobile:not(.is-tablet) .sr-flashcard, .is-mobile:not(.is-tablet) .sr-deck-list { @@ -131,26 +164,12 @@ } @media only screen and (orientation: portrait) and (max-width: 500px) { - .is-mobile .sr-deck-divider { - display: none; - } - - .is-mobile .sr-flashcard .sr-header { - flex-direction: column !important; - gap: 0px !important; - } - .is-mobile .sr-tab-view { padding: 0 8px; } } @media only screen and (orientation: portrait) and (max-width: 340px) { - .is-mobile .sr-flashcard .sr-header .sr-chosen-deck-label > *, - .is-mobile .sr-flashcard .sr-current-deck-label > * { - font-size: medium; - } - .is-mobile .sr-response-button { height: 32px !important; } @@ -165,38 +184,6 @@ } } -@media only screen and (orientation: landscape) and (max-height: 430px) { - .is-mobile:not(.is-tablet) .sr-response { - margin: 0 0 0 calc(var(--side-button-clearance)); - } - - .is-mobile:not(.is-tablet) .sr-response-button { - height: 32px !important; - } - - .is-mobile:not(.is-tablet) .sr-button { - height: 35px; - width: 35px; - } -} - -@media only screen and (orientation: landscape) and (max-height: 400px) { - .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard .sr-header, - .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard > hr { - --side-button-clearance: calc(var(--side-button-width) * 4) !important; - } - - .is-mobile:not(.is-tablet) .sr-tab-view .sr-back-button { - left: calc(var(--side-button-width) * 3) !important; - top: 0px !important; - } - - .is-mobile:not(.is-tablet) .sr-tab-view .sr-flashcard > hr { - margin: 0 0 0 - calc(var(--side-button-clearance) - var(--side-button-width) - var(--side-button-width)); - } -} - /* MARK: Modal */ #sr-modal { @@ -288,14 +275,6 @@ body:not(.native-scrollbars) #sr-modal .modal-close-button { margin: 0; } -#sr-modal .sr-flashcard .sr-header { - margin: 0 var(--side-button-clearance); -} - -.sr-tab-view .sr-flashcard .sr-header { - margin: 0 var(--side-button-clearance); -} - .sr-title { font-size: var(--font-ui-large); font-weight: var(--font-semibold); @@ -303,13 +282,6 @@ body:not(.native-scrollbars) #sr-modal .modal-close-button { line-height: var(--line-height-tight); } -.sr-sub-title { - font-size: var(--font-ui-medium); - text-align: center; - line-height: var(--line-height-tight); - color: var(--text-muted); -} - .sr-content { overflow-y: auto; } @@ -369,63 +341,57 @@ body:not(.native-scrollbars) #sr-modal .modal-close-button { /* MARK: FlashcardReviewView */ -/* TODO: Enable textwrap for card content */ -.sr-flashcard .sr-header { - position: relative; - gap: 16px; - flex-direction: row; +.sr-flashcard .sr-button:disabled { + cursor: not-allowed; } -.sr-flashcard .sr-chosen-deck-label svg, -.sr-flashcard .sr-current-deck-label svg { +.sr-flashcard .sr-controls { + display: flex; + width: 100%; + gap: var(--size-4-4); + justify-content: center; + align-items: center; +} + +.sr-flashcard .sr-info-bar { + display: flex; + margin-top: 8px; + gap: 8px; + flex-wrap: wrap; +} + +.sr-flashcard .sr-chosen-deck-info svg, +.sr-flashcard .sr-current-deck-info svg { --size: 16px; height: var(--size); width: var(--size); margin-bottom: -3px; } -.sr-flashcard .sr-chosen-deck-label, -.sr-flashcard .sr-current-deck-label { +.sr-flashcard .sr-chosen-deck-info, +.sr-flashcard .sr-current-deck-info { display: flex; flex-direction: row; justify-content: center; align-items: center; gap: 5px; + flex-wrap: wrap; } -.sr-flashcard .sr-chosen-deck-deck-num { - padding-left: 4px; -} - -.sr-flashcard .sr-chosen-deck-label > *, -.sr-flashcard .sr-current-deck-label > * { +.sr-flashcard .sr-chosen-deck-info > *, +.sr-flashcard .sr-current-deck-info > * { display: flex; flex-direction: row; justify-content: center; align-items: center; - font-size: large; + color: var(--text-muted); + text-wrap: nowrap; } -.sr-flashcard .sr-chosen-deck-label > * { +.sr-flashcard .sr-chosen-deck-info > * { font-weight: 500; } -.sr-flashcard .sr-button:disabled { - cursor: not-allowed; -} - -.sr-flashcard .sr-controls { - display: flex; - width: 100%; - gap: var(--size-4-4); - justify-content: center; - align-items: center; -} - -.sr-flashcard > hr { - margin: 0; -} - .sr-flashcard .sr-context { font-style: italic; color: var(--text-faint); diff --git a/tests/unit/flashcard-review-sequencer.test.ts b/tests/unit/flashcard-review-sequencer.test.ts index 5eb215a5..7bbf7198 100644 --- a/tests/unit/flashcard-review-sequencer.test.ts +++ b/tests/unit/flashcard-review-sequencer.test.ts @@ -989,7 +989,7 @@ Q4::A4 text, ); await c.setSequencerDeckTreeFromOriginalText(); - expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4)); + expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(4, 1, 3, 4, 1, 3, 4, 0, 1)); }); test("Reduction in due count after skipping card", async () => { @@ -1008,10 +1008,10 @@ Q4::A4 await c.setSequencerDeckTreeFromOriginalText(); expect(c.reviewSequencer.currentCard.front).toEqual("Q4"); // This is the first card as we are using orderDueFirstSequential - expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(1, 3, 4)); + expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(4, 1, 3, 4, 1, 3, 4, 0, 1)); c.reviewSequencer.skipCurrentCard(); // One less due card - expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(0, 3, 4)); + expect(c.getDeckStats("#flashcards")).toEqual(new DeckStats(4, 0, 3, 3, 0, 3, 3, 0, 1)); }); test("Change in stats after reviewing each card", async () => { @@ -1030,9 +1030,9 @@ Q4::A4 await c.setSequencerDeckTreeFromOriginalText(); await checkStats(c, "#flashcards", [ - [new DeckStats(1, 3, 4), "Q4", ReviewResponse.Easy], // This is the first card as we are using orderDueFirstSequential - [new DeckStats(0, 3, 4), "Q1", ReviewResponse.Easy], // Iterated through all the due cards, now the new ones - [new DeckStats(0, 2, 4), "Q2", ReviewResponse.Easy], + [new DeckStats(4, 1, 3, 4, 1, 3, 4, 0, 1), "Q4", ReviewResponse.Easy], // This is the first card as we are using orderDueFirstSequential + [new DeckStats(4, 0, 3, 3, 0, 3, 3, 0, 1), "Q1", ReviewResponse.Easy], // Iterated through all the due cards, now the new ones + [new DeckStats(4, 0, 2, 2, 0, 2, 2, 0, 1), "Q2", ReviewResponse.Easy], ]); }); });