diff --git a/docs/changelog.md b/docs/changelog.md index 97fa8316..d34386f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### [Unreleased] +- Overhauled ui as described in issue [`#872`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/872) +- Fixes key listener bug from issue [`#907`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/907) +- Fixes bug from issue [`#773`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/773) - Unrelated Tags in Card Selection Modal since version 1.12.0 [`#908`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/908) #### [1.12.0](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.11.2...1.12.0) @@ -33,9 +36,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). #### [1.11.1](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.11.0...1.11.1) -> 22 January 2024 - -- Bump version to v1.11.1 [`#854`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/854) - chore: fix README to point to new project board [`#848`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/848) - chore: update pt-br.ts for Brazilian Portuguese translation [`#765`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/765) - chore: update dependencies [`#845`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/845) diff --git a/src/gui/DeckListView.tsx b/src/gui/DeckListView.tsx new file mode 100644 index 00000000..c3141762 --- /dev/null +++ b/src/gui/DeckListView.tsx @@ -0,0 +1,236 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import h from "vhtml"; + +import type SRPlugin from "src/main"; +import { SRSettings } from "src/settings"; +import { COLLAPSE_ICON } from "src/constants"; +import { t } from "src/lang/helpers"; +import { Deck } from "../Deck"; +import { + DeckStats, + IFlashcardReviewSequencer as IFlashcardReviewSequencer, +} from "src/FlashcardReviewSequencer"; +import { TopicPath } from "src/TopicPath"; +import { FlashcardModalMode } from "./FlashcardModal"; + +export class DeckListView { + public plugin: SRPlugin; + public mode: FlashcardModalMode; + public modalContentEl: HTMLElement; + + public view: HTMLDivElement; + public header: HTMLDivElement; + public title: HTMLDivElement; + public stats: HTMLDivElement; + public content: HTMLDivElement; + + private reviewSequencer: IFlashcardReviewSequencer; + private settings: SRSettings; + private startReviewOfDeck: (deck: Deck) => void; + + constructor( + plugin: SRPlugin, + settings: SRSettings, + reviewSequencer: IFlashcardReviewSequencer, + contentEl: HTMLElement, + startReviewOfDeck: (deck: Deck) => void, + ) { + // Init properties + this.plugin = plugin; + this.settings = settings; + this.reviewSequencer = reviewSequencer; + this.modalContentEl = contentEl; + this.startReviewOfDeck = startReviewOfDeck; + + // Build ui + this.init(); + } + + /** + * Initializes all static elements in the DeckListView + */ + init(): void { + this.view = this.modalContentEl.createDiv(); + this.view.addClasses(["sr-deck-list", "sr-is-hidden"]); + + this.header = this.view.createDiv(); + this.header.addClass("sr-header"); + + this.title = this.header.createDiv(); + this.title.addClass("sr-title"); + this.title.setText(t("DECKS")); + + this.stats = this.header.createDiv(); + this.stats.addClass("sr-header-stats-container"); + this._createHeaderStats(); + + this.content = this.view.createDiv(); + this.content.addClass("sr-content"); + } + + /** + * Shows the DeckListView & rerenders dynamic elements + */ + show(): void { + this.mode = FlashcardModalMode.DecksList; + + // Redraw in case the stats have changed + this._createHeaderStats(); + + this.content.empty(); + for (const deck of this.reviewSequencer.originalDeckTree.subdecks) { + this._createTree(deck, this.content); + } + + if (this.view.hasClass("sr-is-hidden")) { + this.view.removeClass("sr-is-hidden"); + } + } + + /** + * Hides the DeckListView + */ + hide() { + if (!this.view.hasClass("sr-is-hidden")) { + this.view.addClass("sr-is-hidden"); + } + } + + /** + * Closes the DeckListView + */ + close() { + this.hide(); + } + + // -> Header + + private _createHeaderStats() { + const statistics: DeckStats = this.reviewSequencer.getDeckStats(TopicPath.emptyPath); + this.stats.empty(); + + this._createHeaderStatsContainer(t("TOTAL_CARDS"), statistics.totalCount, "sr-bg-red"); + this._createHeaderStatsContainer(t("NEW_CARDS"), statistics.newCount, "sr-bg-blue"); + this._createHeaderStatsContainer(t("DUE_CARDS"), statistics.dueCount, "sr-bg-green"); + } + + private _createHeaderStatsContainer( + statsLable: string, + statsNumber: number, + statsClass: string, + ): void { + const statsContainer = this.stats.createDiv(); + statsContainer.ariaLabel = statsLable; + statsContainer.addClasses([ + "tag-pane-tag-count", + "tree-item-flair", + "sr-header-stats-count", + statsClass, + ]); + + const lable = statsContainer.createDiv(); + lable.setText(statsLable + ":"); + + const number = statsContainer.createDiv(); + number.setText(statsNumber.toString()); + } + + // -> Tree content + + private _createTree(deck: Deck, container: HTMLElement): void { + const deckTree: HTMLElement = container.createDiv("tree-item sr-tree-item-container"); + const deckTreeSelf: HTMLElement = deckTree.createDiv( + "tree-item-self tag-pane-tag is-clickable sr-tree-item-row", + ); + + const shouldBeInitiallyExpanded: boolean = this.settings.initiallyExpandAllSubdecksInTree; + let collapsed = !shouldBeInitiallyExpanded; + let collapseIconEl: HTMLElement | null = null; + if (deck.subdecks.length > 0) { + collapseIconEl = deckTreeSelf.createDiv("tree-item-icon collapse-icon"); + collapseIconEl.innerHTML = COLLAPSE_ICON; + (collapseIconEl.childNodes[0] as HTMLElement).style.transform = collapsed + ? "rotate(-90deg)" + : ""; + } + + const deckTreeInner: HTMLElement = deckTreeSelf.createDiv("tree-item-inner"); + const deckTreeInnerText: HTMLElement = deckTreeInner.createDiv("tag-pane-tag-text"); + deckTreeInnerText.innerHTML += {deck.deckName}; + + const deckTreeOuter: HTMLDivElement = deckTreeSelf.createDiv(); + deckTreeOuter.addClasses(["tree-item-flair-outer", "sr-tree-stats-container"]); + + const deckStats = this.reviewSequencer.getDeckStats(deck.getTopicPath()); + this._createStats(deckStats, deckTreeOuter); + + const deckTreeChildren: HTMLElement = deckTree.createDiv("tree-item-children"); + deckTreeChildren.style.display = collapsed ? "none" : "block"; + if (deck.subdecks.length > 0) { + collapseIconEl.addEventListener("click", (e) => { + if (collapsed) { + (collapseIconEl.childNodes[0] as HTMLElement).style.transform = ""; + deckTreeChildren.style.display = "block"; + } else { + (collapseIconEl.childNodes[0] as HTMLElement).style.transform = + "rotate(-90deg)"; + deckTreeChildren.style.display = "none"; + } + + // We stop the propagation of the event so that the click event for deckTreeSelf doesn't get called + // if the user clicks on the collapse icon + e.stopPropagation(); + collapsed = !collapsed; + }); + } + + // Add the click handler to deckTreeSelf instead of deckTreeInner so that it activates + // over the entire rectangle of the tree item, not just the text of the topic name + // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/709 + deckTreeSelf.addEventListener("click", () => { + this.startReviewOfDeck(deck); + }); + + for (const subdeck of deck.subdecks) { + this._createTree(subdeck, deckTreeChildren); + } + } + + private _createStats(statistics: DeckStats, statsWrapper: HTMLDivElement) { + statsWrapper.empty(); + + this._createStatsContainer( + t("TOTAL_CARDS"), + statistics.totalCount, + "sr-bg-red", + statsWrapper, + ); + this._createStatsContainer(t("NEW_CARDS"), statistics.newCount, "sr-bg-blue", statsWrapper); + this._createStatsContainer( + t("DUE_CARDS"), + statistics.dueCount, + "sr-bg-green", + statsWrapper, + ); + } + + private _createStatsContainer( + statsLable: string, + statsNumber: number, + statsClass: string, + statsWrapper: HTMLDivElement, + ): void { + const statsContainer = statsWrapper.createDiv(); + + statsContainer.ariaLabel = statsLable; + + statsContainer.addClasses([ + "tag-pane-tag-count", + "tree-item-flair", + "sr-tree-stats-count", + statsClass, + ]); + + statsContainer.setText(statsNumber.toString()); + } +} diff --git a/src/gui/DecksListView.tsx b/src/gui/DecksListView.tsx deleted file mode 100644 index dee2b0ab..00000000 --- a/src/gui/DecksListView.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Platform } from "obsidian"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import h from "vhtml"; - -import type SRPlugin from "src/main"; -import { SRSettings } from "src/settings"; -import { COLLAPSE_ICON } from "src/constants"; -import { t } from "src/lang/helpers"; -import { Deck } from "../Deck"; -import { - DeckStats, - IFlashcardReviewSequencer as IFlashcardReviewSequencer, -} from "src/FlashcardReviewSequencer"; -import { TopicPath } from "src/TopicPath"; - -export class DecksListView { - public plugin: SRPlugin; - public titleEl: HTMLElement; - public contentEl: HTMLElement; - private reviewSequencer: IFlashcardReviewSequencer; - private settings: SRSettings; - private startReviewOfDeck: (deck: Deck) => void; - - constructor( - plugin: SRPlugin, - settings: SRSettings, - reviewSequencer: IFlashcardReviewSequencer, - titleEl: HTMLElement, - contentEl: HTMLElement, - startReviewOfDeck: (deck: Deck) => void, - ) { - this.plugin = plugin; - this.settings = settings; - this.reviewSequencer = reviewSequencer; - - this.startReviewOfDeck = startReviewOfDeck; - - this.titleEl = titleEl; - this.contentEl = contentEl; - - this.titleEl.addClass("sr-centered"); - - this.contentEl.style.position = "relative"; - this.contentEl.style.height = "92%"; - this.contentEl.addClass("sr-modal-content"); - if (Platform.isMobile) { - this.contentEl.style.display = "block"; - } - } - - /** - * Shows the DeckListView - */ - show(): void { - const stats: DeckStats = this.reviewSequencer.getDeckStats(TopicPath.emptyPath); - - this.titleEl.setText(t("DECKS")); - this.titleEl.innerHTML += ( -

- - {stats.dueCount.toString()} - - - {stats.newCount.toString()} - - - {stats.totalCount.toString()} - -

- ); - this.contentEl.empty(); - this.contentEl.setAttribute("id", "sr-flashcard-view"); - - for (const deck of this.reviewSequencer.originalDeckTree.subdecks) { - this.renderDeck(deck, this.contentEl); - } - } - - renderDeck(deck: Deck, containerEl: HTMLElement): void { - const deckView: HTMLElement = containerEl.createDiv("tree-item"); - - const deckViewSelf: HTMLElement = deckView.createDiv( - "tree-item-self tag-pane-tag is-clickable", - ); - const shouldBeInitiallyExpanded: boolean = this.settings.initiallyExpandAllSubdecksInTree; - let collapsed = !shouldBeInitiallyExpanded; - let collapseIconEl: HTMLElement | null = null; - if (deck.subdecks.length > 0) { - collapseIconEl = deckViewSelf.createDiv("tree-item-icon collapse-icon"); - collapseIconEl.innerHTML = COLLAPSE_ICON; - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = collapsed - ? "rotate(-90deg)" - : ""; - } - - const deckViewInner: HTMLElement = deckViewSelf.createDiv("tree-item-inner"); - const deckViewInnerText: HTMLElement = deckViewInner.createDiv("tag-pane-tag-text"); - deckViewInnerText.innerHTML += {deck.deckName}; - const deckViewOuter: HTMLElement = deckViewSelf.createDiv("tree-item-flair-outer"); - const deckStats = this.reviewSequencer.getDeckStats(deck.getTopicPath()); - deckViewOuter.innerHTML += ( - - - {deckStats.dueCount.toString()} - - - {deckStats.newCount.toString()} - - - {deckStats.totalCount.toString()} - - - ); - - const deckViewChildren: HTMLElement = deckView.createDiv("tree-item-children"); - deckViewChildren.style.display = collapsed ? "none" : "block"; - if (deck.subdecks.length > 0) { - collapseIconEl.addEventListener("click", (e) => { - if (collapsed) { - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = ""; - deckViewChildren.style.display = "block"; - } else { - (collapseIconEl.childNodes[0] as HTMLElement).style.transform = - "rotate(-90deg)"; - deckViewChildren.style.display = "none"; - } - - // We stop the propagation of the event so that the click event for deckViewSelf doesn't get called - // if the user clicks on the collapse icon - e.stopPropagation(); - collapsed = !collapsed; - }); - } - - // Add the click handler to deckViewSelf instead of deckViewInner so that it activates - // over the entire rectangle of the tree item, not just the text of the topic name - // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/709 - deckViewSelf.addEventListener("click", () => { - this.startReviewOfDeck(deck); - }); - - for (const subdeck of deck.subdecks) { - this.renderDeck(subdeck, deckViewChildren); - } - } -} diff --git a/src/gui/EditModal.tsx b/src/gui/EditModal.tsx new file mode 100644 index 00000000..1feb19d5 --- /dev/null +++ b/src/gui/EditModal.tsx @@ -0,0 +1,134 @@ +import { App, Modal } from "obsidian"; +import { t } from "src/lang/helpers"; + +// from https://github.com/chhoumann/quickadd/blob/bce0b4cdac44b867854d6233796e3406dfd163c6/src/gui/GenericInputPrompt/GenericInputPrompt.ts#L5 +export class FlashcardEditModal extends Modal { + public changedText: string; + public waitForClose: Promise; + + public title: HTMLDivElement; + public textArea: HTMLTextAreaElement; + public response: HTMLDivElement; + public saveButton: HTMLButtonElement; + public cancelButton: HTMLButtonElement; + + private resolvePromise: (input: string) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private rejectPromise: (reason?: any) => void; + private didSaveChanges = false; + private readonly modalText: string; + + public static Prompt(app: App, placeholder: string): Promise { + const newPromptModal = new FlashcardEditModal(app, placeholder); + return newPromptModal.waitForClose; + } + + constructor(app: App, existingText: string) { + super(app); + + this.modalText = existingText; + this.changedText = existingText; + + this.waitForClose = new Promise((resolve, reject) => { + this.resolvePromise = resolve; + this.rejectPromise = reject; + }); + + // Init static elements in ui + this.modalEl.addClasses(["sr-modal", "sr-edit-modal"]); + this.init(); + + this.open(); + } + + /** + * Initializes all components of the EditModal + */ + init() { + this.contentEl.empty(); + this.contentEl.addClass("sr-edit-view"); + + this.title = this.contentEl.createDiv(); + this.title.setText(t("EDIT_CARD")); + this.title.addClass("sr-title"); + + this.textArea = this.contentEl.createEl("textarea"); + this.textArea.addClass("sr-input"); + this.textArea.setText(this.modalText ?? ""); + this.textArea.addEventListener("keydown", this.saveOnEnterCallback); + + this._createResponse(this.contentEl); + } + + /** + * Opens the EditModal + */ + onOpen() { + super.onOpen(); + + this.textArea.focus(); + } + + /** + * Closes the EditModal + */ + onClose() { + super.onClose(); + this.resolveInput(); + this.removeInputListener(); + } + + // -> Functions & helpers + + private saveClickCallback = (_: MouseEvent) => this.save(); + + private cancelClickCallback = (_: MouseEvent) => this.cancel(); + + private saveOnEnterCallback = (evt: KeyboardEvent) => { + if ((evt.ctrlKey || evt.metaKey) && evt.key === "Enter") { + evt.preventDefault(); + this.save(); + } + }; + + private save() { + this.didSaveChanges = true; + this.changedText = this.textArea.value; + this.close(); + } + + private cancel() { + this.close(); + } + + private resolveInput() { + if (!this.didSaveChanges) this.rejectPromise(t("NO_INPUT")); + else this.resolvePromise(this.changedText); + } + + private removeInputListener() { + this.textArea.removeEventListener("keydown", this.saveOnEnterCallback); + } + + // -> Response section + + private _createResponseButton( + container: HTMLElement, + text: string, + colorClass: string, + callback: (evt: MouseEvent) => void, + ) { + const button = container.createEl("button"); + button.addClasses(["sr-response-button", colorClass]); + button.setText(text); + button.addEventListener("click", callback); + } + + private _createResponse(mainContentContainer: HTMLElement) { + const response: HTMLDivElement = mainContentContainer.createDiv(); + response.addClass("sr-response"); + this._createResponseButton(response, t("CANCEL"), "sr-bg-red", this.cancelClickCallback); + this._createResponseButton(response, "", "sr-spacer", () => {}); + this._createResponseButton(response, t("SAVE"), "sr-bg-green", this.saveClickCallback); + } +} diff --git a/src/gui/flashcard-modal.tsx b/src/gui/FlashcardModal.tsx similarity index 60% rename from src/gui/flashcard-modal.tsx rename to src/gui/FlashcardModal.tsx index 37571719..9ff4b372 100644 --- a/src/gui/flashcard-modal.tsx +++ b/src/gui/FlashcardModal.tsx @@ -4,14 +4,15 @@ import h from "vhtml"; import type SRPlugin from "src/main"; import { SRSettings } from "src/settings"; + import { Deck } from "../Deck"; import { Question } from "../Question"; import { FlashcardReviewMode, IFlashcardReviewSequencer as IFlashcardReviewSequencer, } from "src/FlashcardReviewSequencer"; -import { FlashcardEditModal } from "./flashcards-edit-modal"; -import { DecksListView } from "./DecksListView"; +import { FlashcardEditModal } from "./EditModal"; +import { DeckListView } from "./DeckListView"; import { FlashcardReviewView } from "./FlashcardReviewView"; export enum FlashcardModalMode { @@ -27,6 +28,8 @@ export class FlashcardModal extends Modal { private reviewSequencer: IFlashcardReviewSequencer; private settings: SRSettings; private reviewMode: FlashcardReviewMode; + private deckView: DeckListView; + private flashcardView: FlashcardReviewView; constructor( app: App, @@ -37,55 +40,79 @@ export class FlashcardModal extends Modal { ) { super(app); + // Init properties this.plugin = plugin; this.settings = settings; this.reviewSequencer = reviewSequencer; this.reviewMode = reviewMode; + // Setup base containers this.modalEl.style.height = this.settings.flashcardHeightPercentage + "%"; this.modalEl.style.width = this.settings.flashcardWidthPercentage + "%"; - this.modalEl.addClass("sr-modal"); + this.modalEl.setAttribute("id", "sr-modal"); + + this.contentEl.addClass("sr-modal-content"); + + // Init static elements in views + this.deckView = new DeckListView( + this.plugin, + this.settings, + this.reviewSequencer, + this.contentEl, + this._startReviewOfDeck.bind(this), + ); + + this.flashcardView = new FlashcardReviewView( + this.app, + this.plugin, + this.settings, + this.reviewSequencer, + this.reviewMode, + this.contentEl, + this.modalEl, + this._showDecksList.bind(this), + this._doEditQuestionText.bind(this), + ); } onOpen(): void { - this.showDecksList(); + this._showDecksList(); } onClose(): void { + this.deckView.close(); + this.flashcardView.close(); this.mode = FlashcardModalMode.Closed; } - showDecksList(): void { - new DecksListView( - this.plugin, - this.settings, - this.reviewSequencer, - this.titleEl, - this.contentEl, - this.startReviewOfDeck.bind(this), - ).show(); + private _showDecksList(): void { + this._hideFlashcard(); + this.deckView.show(); + } + + private _hideDecksList(): void { + this.deckView.hide(); + } + + private _showFlashcard(): void { + this._hideDecksList(); + this.flashcardView.show(); + } + + private _hideFlashcard(): void { + this.flashcardView.hide(); } - startReviewOfDeck(deck: Deck) { + private _startReviewOfDeck(deck: Deck) { this.reviewSequencer.setCurrentDeck(deck.getTopicPath()); if (this.reviewSequencer.hasCurrentCard) { - new FlashcardReviewView( - this.app, - this.plugin, - this.settings, - this.reviewSequencer, - this.reviewMode, - this.titleEl, - this.contentEl, - this.showDecksList.bind(this), - this.doEditQuestionText.bind(this), - ).showCurrentCard(); + this._showFlashcard(); } else { - this.showDecksList(); + this._showDecksList(); } } - async doEditQuestionText(): Promise { + private async _doEditQuestionText(): Promise { const currentQ: Question = this.reviewSequencer.currentQuestion; // Just the question/answer text; without any preceding topic tag diff --git a/src/gui/FlashcardReviewView.tsx b/src/gui/FlashcardReviewView.tsx index 5d8ad443..cb6024b4 100644 --- a/src/gui/FlashcardReviewView.tsx +++ b/src/gui/FlashcardReviewView.tsx @@ -15,54 +15,54 @@ import { import { Note } from "src/Note"; import { RenderMarkdownWrapper } from "src/util/RenderMarkdownWrapper"; import { CardScheduleInfo } from "src/CardSchedule"; -import { FlashcardModalMode } from "./flashcard-modal"; +import { FlashcardModalMode } from "./FlashcardModal"; export class FlashcardReviewView { public app: App; public plugin: SRPlugin; - public answerBtn: HTMLElement; - public titleEl: HTMLElement; - public contentEl: HTMLElement; - public flashcardView: HTMLElement; - private flashCardMenu: HTMLDivElement; - public hardBtn: HTMLElement; - public goodBtn: HTMLElement; - public easyBtn: HTMLElement; - public nextBtn: HTMLElement; - public responseDiv: HTMLElement; - public resetButton: HTMLButtonElement; - public editButton: HTMLElement; - public contextView: HTMLElement; + public modalContentEl: HTMLElement; + public modalEl: HTMLElement; public mode: FlashcardModalMode; + + public view: HTMLDivElement; + + public header: HTMLDivElement; + public title: HTMLDivElement; + public backButton: HTMLDivElement; + + public controls: HTMLDivElement; + public editButton: HTMLButtonElement; + public resetButton: HTMLButtonElement; + public infoButton: HTMLButtonElement; + public skipButton: HTMLButtonElement; + + public content: HTMLDivElement; + public context: HTMLElement; + + public response: HTMLDivElement; + public hardButton: HTMLButtonElement; + public goodButton: HTMLButtonElement; + public easyButton: HTMLButtonElement; + public answerButton: HTMLButtonElement; + private reviewSequencer: IFlashcardReviewSequencer; private settings: SRSettings; private reviewMode: FlashcardReviewMode; private backClickHandler: () => void; private editClickHandler: () => void; - private get currentCard(): Card { - return this.reviewSequencer.currentCard; - } - - private get currentQuestion(): Question { - return this.reviewSequencer.currentQuestion; - } - - private get currentNote(): Note { - return this.reviewSequencer.currentNote; - } - constructor( app: App, plugin: SRPlugin, settings: SRSettings, reviewSequencer: IFlashcardReviewSequencer, reviewMode: FlashcardReviewMode, - titleEl: HTMLElement, contentEl: HTMLElement, + modalEl: HTMLElement, backClickHandler: () => void, editClickHandler: () => void, ) { + // Init properties this.app = app; this.plugin = plugin; this.settings = settings; @@ -70,27 +70,113 @@ export class FlashcardReviewView { this.reviewMode = reviewMode; this.backClickHandler = backClickHandler; this.editClickHandler = editClickHandler; + this.modalContentEl = contentEl; + this.modalEl = modalEl; + + // Build ui + this.init(); + } - this.titleEl = titleEl; - this.contentEl = contentEl; + /** + * Initializes all static elements in the FlashcardView + */ + init() { + this.view = this.modalContentEl.createDiv(); + this.view.addClasses(["sr-flashcard", "sr-is-hidden"]); - this.titleEl.addClass("sr-centered"); + this.header = this.view.createDiv(); + this.header.addClass("sr-header"); - this.contentEl.style.position = "relative"; - this.contentEl.style.height = "92%"; - this.contentEl.addClass("sr-modal-content"); - if (Platform.isMobile) { - this.contentEl.style.display = "block"; + this._createBackButton(); + + this.title = this.header.createDiv(); + this.title.addClass("sr-title"); + + this.controls = this.header.createDiv(); + this.controls.addClass("sr-controls"); + + this._createCardControls(); + + if (this.settings.showContextInCards) { + this.context = this.view.createDiv(); + this.context.addClass("sr-context"); } - document.addEventListener("keydown", this.keydownHandler.bind(this)); + this.content = this.view.createDiv(); + this.content.addClass("sr-content"); + + this.response = this.view.createDiv(); + this.response.addClass("sr-response"); + + this._createResponseButtons(); } - keydownHandler(e: KeyboardEvent): void { - // Checks if the input textbox is in focus before processing keyboard shortcuts. + /** + * Shows the FlashcardView & rerenders all dynamic elements + */ + async show() { + this.mode = FlashcardModalMode.Front; + const deck: Deck = this.reviewSequencer.currentDeck; + + // Setup title + this._setTitle(deck); + this.resetButton.disabled = true; + + // Setup context + if (this.settings.showContextInCards) { + this.context.setText( + this._formatQuestionContextText(this._currentQuestion.questionContext), + ); + } + + // Setup card content + this.content.empty(); + const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( + this.app, + this.plugin, + this._currentNote.filePath, + ); + await wrapper.renderMarkdownWrapper(this._currentCard.front, this.content); + + // Setup response buttons + this._resetResponseButtons(); + + // Prevents the following code, from running if this show is just a redraw and not an unhide + if (!this.view.hasClass("sr-is-hidden")) { + return; + } + this.view.removeClass("sr-is-hidden"); + this.backButton.removeClass("sr-is-hidden"); + document.addEventListener("keydown", this._keydownHandler); + } + + /** + * Hides the FlashcardView + */ + hide() { + // Prevents the following code, from running if this was executed multiple times after one another + if (this.view.hasClass("sr-is-hidden")) { + return; + } + this.view.addClass("sr-is-hidden"); + this.backButton.addClass("sr-is-hidden"); + document.removeEventListener("keydown", this._keydownHandler); + } + + /** + * Closes the FlashcardView + */ + close() { + document.removeEventListener("keydown", this._keydownHandler); + this.hide(); + } + + // -> Functions & helpers + + private _keydownHandler = (e: KeyboardEvent) => { + // Prevents any input, if the edit modal is open if ( document.activeElement.nodeName === "TEXTAREA" || - this.mode === FlashcardModalMode.DecksList || this.mode === FlashcardModalMode.Closed ) { return; @@ -103,15 +189,15 @@ export class FlashcardReviewView { switch (e.code) { case "KeyS": - this.skipCurrentCard(); + this._skipCurrentCard(); consumeKeyEvent(); break; case "Space": if (this.mode === FlashcardModalMode.Front) { - this.showAnswer(); + this._showAnswer(); consumeKeyEvent(); } else if (this.mode === FlashcardModalMode.Back) { - this.processReview(ReviewResponse.Good); + this._processReview(ReviewResponse.Good); consumeKeyEvent(); } break; @@ -120,7 +206,7 @@ export class FlashcardReviewView { if (this.mode !== FlashcardModalMode.Front) { break; } - this.showAnswer(); + this._showAnswer(); consumeKeyEvent(); break; case "Numpad1": @@ -128,7 +214,7 @@ export class FlashcardReviewView { if (this.mode !== FlashcardModalMode.Back) { break; } - this.processReview(ReviewResponse.Hard); + this._processReview(ReviewResponse.Hard); consumeKeyEvent(); break; case "Numpad2": @@ -136,7 +222,7 @@ export class FlashcardReviewView { if (this.mode !== FlashcardModalMode.Back) { break; } - this.processReview(ReviewResponse.Good); + this._processReview(ReviewResponse.Good); consumeKeyEvent(); break; case "Numpad3": @@ -144,7 +230,7 @@ export class FlashcardReviewView { if (this.mode !== FlashcardModalMode.Back) { break; } - this.processReview(ReviewResponse.Easy); + this._processReview(ReviewResponse.Easy); consumeKeyEvent(); break; case "Numpad0": @@ -152,246 +238,255 @@ export class FlashcardReviewView { if (this.mode !== FlashcardModalMode.Back) { break; } - this.processReview(ReviewResponse.Reset); + this._processReview(ReviewResponse.Reset); consumeKeyEvent(); break; default: break; } + }; + + private _displayCurrentCardInfoNotice() { + const schedule = this._currentCard.scheduleInfo; + + const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.ease ?? 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); } - async showCurrentCard(): Promise { - this.setupView(); + private get _currentCard(): Card { + return this.reviewSequencer.currentCard; + } - const deck: Deck = this.reviewSequencer.currentDeck; + private get _currentQuestion(): Question { + return this.reviewSequencer.currentQuestion; + } - this.responseDiv.style.display = "none"; - this.resetButton.disabled = true; - this.titleEl.setText(`${deck.deckName}: ${deck.getCardCount(CardListType.All, true)}`); + private get _currentNote(): Note { + return this.reviewSequencer.currentNote; + } - this.answerBtn.style.display = "initial"; - this.flashcardView.empty(); - this.mode = FlashcardModalMode.Front; + private _showAnswer(): void { + this.mode = FlashcardModalMode.Back; + + this.resetButton.disabled = false; + + // Show answer text + if (this._currentQuestion.questionType !== CardType.Cloze) { + const hr: HTMLElement = document.createElement("hr"); + hr.addClass("sr-card-divide"); + this.content.appendChild(hr); + } else { + this.content.empty(); + } const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( this.app, this.plugin, - this.currentNote.filePath, + this._currentNote.filePath, ); - await wrapper.renderMarkdownWrapper(this.currentCard.front, this.flashcardView); + wrapper.renderMarkdownWrapper(this._currentCard.back, this.content); - if (this.reviewMode == FlashcardReviewMode.Cram) { - // Same for mobile/desktop - this.hardBtn.setText(`${this.settings.flashcardHardText}`); - this.easyBtn.setText(`${this.settings.flashcardEasyText}`); + // 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.setupEaseButton( - this.hardBtn, + this.goodButton.removeClass("sr-is-hidden"); + this._setupEaseButton( + this.hardButton, this.settings.flashcardHardText, ReviewResponse.Hard, ); - this.setupEaseButton( - this.goodBtn, + this._setupEaseButton( + this.goodButton, this.settings.flashcardGoodText, ReviewResponse.Good, ); - this.setupEaseButton( - this.easyBtn, + this._setupEaseButton( + this.easyButton, this.settings.flashcardEasyText, ReviewResponse.Easy, ); } + } - if (this.settings.showContextInCards) - this.contextView.setText( - this.formatQuestionContextText(this.currentQuestion.questionContext), - ); + private async _processReview(response: ReviewResponse): Promise { + await this.reviewSequencer.processReview(response); + await this._handleSkipCard(); } - createShowAnswerButton() { - this.answerBtn = this.contentEl.createDiv(); - this.answerBtn.setAttribute("id", "sr-show-answer"); - this.answerBtn.setText(t("SHOW_ANSWER")); - this.answerBtn.addEventListener("click", () => { - this.showAnswer(); - }); + private async _skipCurrentCard(): Promise { + this.reviewSequencer.skipCurrentCard(); + await this._handleSkipCard(); } - createResponseButtons() { - this.responseDiv = this.contentEl.createDiv("sr-flashcard-response"); + private async _handleSkipCard(): Promise { + if (this._currentCard != null) await this.show(); + else this.backClickHandler(); + } - this.hardBtn = document.createElement("button"); - this.hardBtn.setAttribute("id", "sr-hard-btn"); - this.hardBtn.setText(this.settings.flashcardHardText); - this.hardBtn.addEventListener("click", () => { - this.processReview(ReviewResponse.Hard); - }); - this.responseDiv.appendChild(this.hardBtn); + private _formatQuestionContextText(questionContext: string[]): string { + const separator: string = " > "; + let result = this._currentNote.file.basename; + if (questionContext.length > 0) { + result += separator + questionContext.join(separator); + } + return result + separator + "..."; + } - this.goodBtn = document.createElement("button"); - this.goodBtn.setAttribute("id", "sr-good-btn"); - this.goodBtn.setText(this.settings.flashcardGoodText); - this.goodBtn.addEventListener("click", () => { - this.processReview(ReviewResponse.Good); - }); - this.responseDiv.appendChild(this.goodBtn); + // -> Header - this.easyBtn = document.createElement("button"); - this.easyBtn.setAttribute("id", "sr-easy-btn"); - this.easyBtn.setText(this.settings.flashcardEasyText); - this.easyBtn.addEventListener("click", () => { - this.processReview(ReviewResponse.Easy); + private _createBackButton() { + this.backButton = this.modalEl.createDiv(); + this.backButton.addClasses(["sr-back-button", "sr-is-hidden"]); + setIcon(this.backButton, "arrow-left"); + this.backButton.setAttribute("aria-label", t("BACK")); + this.backButton.addEventListener("click", () => { + /* this.plugin.data.historyDeck = ""; */ + this.backClickHandler(); }); - this.responseDiv.appendChild(this.easyBtn); - this.responseDiv.style.display = "none"; } - createSkipButton() { - const skipButton = this.flashCardMenu.createEl("button"); - skipButton.addClass("sr-flashcard-menu-item"); - setIcon(skipButton, "chevrons-right"); - skipButton.setAttribute("aria-label", t("SKIP")); - skipButton.addEventListener("click", () => { - this.skipCurrentCard(); - }); + private _setTitle(deck: Deck) { + this.title.setText(`${deck.deckName}: ${deck.getCardCount(CardListType.All, true)}`); } - createCardInfoButton() { - const cardInfo = this.flashCardMenu.createEl("button"); - cardInfo.addClass("sr-flashcard-menu-item"); - setIcon(cardInfo, "info"); - cardInfo.setAttribute("aria-label", "View Card Info"); - cardInfo.addEventListener("click", async () => { - this.displayCurrentCardInfoNotice(); - }); - } + // -> Controls - displayCurrentCardInfoNotice() { - const schedule = this.currentCard.scheduleInfo; - const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.ease ?? 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 _createCardControls() { + this._createEditButton(); + this._createResetButton(); + this._createCardInfoButton(); + this._createSkipButton(); } - createBackButton() { - const backButton = this.flashCardMenu.createEl("button"); - backButton.addClass("sr-flashcard-menu-item"); - setIcon(backButton, "arrow-left"); - backButton.setAttribute("aria-label", t("BACK")); - backButton.addEventListener("click", () => { - /* this.plugin.data.historyDeck = ""; */ - this.backClickHandler(); + private _createEditButton() { + this.editButton = this.controls.createEl("button"); + this.editButton.addClasses(["sr-button", "sr-edit-button"]); + setIcon(this.editButton, "edit"); + this.editButton.setAttribute("aria-label", t("EDIT_CARD")); + this.editButton.addEventListener("click", async () => { + this.editClickHandler(); }); } - createResetButton() { - this.resetButton = this.flashCardMenu.createEl("button"); - this.resetButton.addClass("sr-flashcard-menu-item"); + private _createResetButton() { + this.resetButton = this.controls.createEl("button"); + this.resetButton.addClasses(["sr-button", "sr-reset-button"]); setIcon(this.resetButton, "refresh-cw"); this.resetButton.setAttribute("aria-label", t("RESET_CARD_PROGRESS")); this.resetButton.addEventListener("click", () => { - this.processReview(ReviewResponse.Reset); + this._processReview(ReviewResponse.Reset); }); } - createEditButton() { - this.editButton = this.flashCardMenu.createEl("button"); - this.editButton.addClass("sr-flashcard-menu-item"); - setIcon(this.editButton, "edit"); - this.editButton.setAttribute("aria-label", t("EDIT_CARD")); - this.editButton.addEventListener("click", async () => { - this.editClickHandler(); + private _createCardInfoButton() { + this.infoButton = this.controls.createEl("button"); + this.infoButton.addClasses(["sr-button", "sr-info-button"]); + setIcon(this.infoButton, "info"); + this.infoButton.setAttribute("aria-label", "View Card Info"); + this.infoButton.addEventListener("click", async () => { + this._displayCurrentCardInfoNotice(); }); } - private setupView(): void { - this.contentEl.empty(); - - this.flashCardMenu = this.contentEl.createDiv("sr-flashcard-menu"); - - this.createBackButton(); - this.createEditButton(); - this.createResetButton(); - this.createCardInfoButton(); - this.createSkipButton(); - - if (this.settings.showContextInCards) { - this.contextView = this.contentEl.createDiv(); - this.contextView.setAttribute("id", "sr-context"); - } - - this.flashcardView = this.contentEl.createDiv("div"); - this.flashcardView.setAttribute("id", "sr-flashcard-view"); - - this.createResponseButtons(); - - this.createShowAnswerButton(); - - if (this.reviewMode == FlashcardReviewMode.Cram) { - this.goodBtn.style.display = "none"; - - this.responseDiv.addClass("sr-ignorestats-response"); - this.easyBtn.addClass("sr-ignorestats-btn"); - this.hardBtn.addClass("sr-ignorestats-btn"); - } + private _createSkipButton() { + this.skipButton = this.controls.createEl("button"); + this.skipButton.addClasses(["sr-button", "sr-skip-button"]); + setIcon(this.skipButton, "chevrons-right"); + this.skipButton.setAttribute("aria-label", t("SKIP")); + this.skipButton.addEventListener("click", () => { + this._skipCurrentCard(); + }); } - private showAnswer(): void { - this.mode = FlashcardModalMode.Back; - - this.answerBtn.style.display = "none"; - this.responseDiv.style.display = "grid"; + // -> Response - this.resetButton.disabled = false; - - if (this.currentQuestion.questionType !== CardType.Cloze) { - const hr: HTMLElement = document.createElement("hr"); - hr.setAttribute("id", "sr-hr-card-divide"); - this.flashcardView.appendChild(hr); - } else { - this.flashcardView.empty(); - } + private _createResponseButtons() { + this._createShowAnswerButton(); + this._createHardButton(); + this._createGoodButton(); + this._createEasyButton(); + } - const wrapper: RenderMarkdownWrapper = new RenderMarkdownWrapper( - this.app, - this.plugin, - this.currentNote.filePath, - ); - wrapper.renderMarkdownWrapper(this.currentCard.back, this.flashcardView); + private _resetResponseButtons() { + // Sets all buttons in to their default state + this.answerButton.removeClass("sr-is-hidden"); + this.hardButton.addClass("sr-is-hidden"); + this.goodButton.addClass("sr-is-hidden"); + this.easyButton.addClass("sr-is-hidden"); } - private async processReview(response: ReviewResponse): Promise { - await this.reviewSequencer.processReview(response); - await this.handleNextCard(); + private _createShowAnswerButton() { + this.answerButton = this.response.createEl("button"); + this.answerButton.addClasses(["sr-response-button", "sr-show-answer-button", "sr-bg-blue"]); + this.answerButton.setText(t("SHOW_ANSWER")); + this.answerButton.addEventListener("click", () => { + this._showAnswer(); + }); } - private async skipCurrentCard(): Promise { - this.reviewSequencer.skipCurrentCard(); - await this.handleNextCard(); + private _createHardButton() { + this.hardButton = this.response.createEl("button"); + this.hardButton.addClasses([ + "sr-response-button", + "sr-hard-button", + "sr-bg-red", + "sr-is-hidden", + ]); + this.hardButton.setText(this.settings.flashcardHardText); + this.hardButton.addEventListener("click", () => { + this._processReview(ReviewResponse.Hard); + }); } - private async handleNextCard(): Promise { - if (this.currentCard != null) await this.showCurrentCard(); - else this.backClickHandler(); + private _createGoodButton() { + this.goodButton = this.response.createEl("button"); + this.goodButton.addClasses([ + "sr-response-button", + "sr-good-button", + "sr-bg-blue", + "sr-is-hidden", + ]); + this.goodButton.setText(this.settings.flashcardGoodText); + this.goodButton.addEventListener("click", () => { + this._processReview(ReviewResponse.Good); + }); } - private formatQuestionContextText(questionContext: string[]): string { - const result = `${this.currentNote.file.basename} > ${questionContext.join(" > ")}`; - return result; + private _createEasyButton() { + this.easyButton = this.response.createEl("button"); + this.easyButton.addClasses([ + "sr-response-button", + "sr-hard-button", + "sr-bg-green", + "sr-is-hidden", + ]); + this.easyButton.setText(this.settings.flashcardEasyText); + this.easyButton.addEventListener("click", () => { + this._processReview(ReviewResponse.Easy); + }); } - private setupEaseButton( + private _setupEaseButton( button: HTMLElement, buttonName: string, reviewResponse: ReviewResponse, ) { const schedule: CardScheduleInfo = this.reviewSequencer.determineCardSchedule( reviewResponse, - this.currentCard, + this._currentCard, ); const interval: number = schedule.interval; diff --git a/src/gui/sidebar.ts b/src/gui/Sidebar.tsx similarity index 100% rename from src/gui/sidebar.ts rename to src/gui/Sidebar.tsx diff --git a/src/gui/stats-modal.tsx b/src/gui/StatsModal.tsx similarity index 100% rename from src/gui/stats-modal.tsx rename to src/gui/StatsModal.tsx diff --git a/src/gui/flashcards-edit-modal.ts b/src/gui/flashcards-edit-modal.ts deleted file mode 100644 index a6c0c1e4..00000000 --- a/src/gui/flashcards-edit-modal.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { App, ButtonComponent, Modal, TextAreaComponent } from "obsidian"; -import { t } from "src/lang/helpers"; - -// from https://github.com/chhoumann/quickadd/blob/bce0b4cdac44b867854d6233796e3406dfd163c6/src/gui/GenericInputPrompt/GenericInputPrompt.ts#L5 -export class FlashcardEditModal extends Modal { - public input: string; - public waitForClose: Promise; - - private resolvePromise: (input: string) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private rejectPromise: (reason?: any) => void; - private didSubmit = false; - private inputComponent: TextAreaComponent; - private readonly modalText: string; - - public static Prompt(app: App, placeholder: string): Promise { - const newPromptModal = new FlashcardEditModal(app, placeholder); - return newPromptModal.waitForClose; - } - constructor(app: App, existingText: string) { - super(app); - this.titleEl.setText(t("EDIT_CARD")); - this.titleEl.addClass("sr-centered"); - this.modalText = existingText; - this.input = existingText; - - this.waitForClose = new Promise((resolve, reject) => { - this.resolvePromise = resolve; - this.rejectPromise = reject; - }); - this.display(); - this.open(); - } - - private display() { - this.contentEl.empty(); - this.modalEl.addClass("sr-flashcard-input-modal"); - - const mainContentContainer: HTMLDivElement = this.contentEl.createDiv(); - mainContentContainer.addClass("sr-flashcard-input-area"); - this.inputComponent = this.createInputField(mainContentContainer, this.modalText); - this.createButtonBar(mainContentContainer); - } - - private createButton( - container: HTMLElement, - text: string, - callback: (evt: MouseEvent) => void, - ) { - const btn = new ButtonComponent(container); - btn.setButtonText(text).onClick(callback); - return btn; - } - - private createButtonBar(mainContentContainer: HTMLDivElement) { - const buttonBarContainer: HTMLDivElement = mainContentContainer.createDiv(); - buttonBarContainer.addClass("sr-flashcard-edit-button-bar"); - this.createButton( - buttonBarContainer, - t("SAVE"), - this.submitClickCallback, - ).setCta().buttonEl.style.marginRight = "0"; - this.createButton(buttonBarContainer, t("CANCEL"), this.cancelClickCallback); - } - - protected createInputField(container: HTMLElement, value: string) { - const textComponent = new TextAreaComponent(container); - - textComponent.inputEl.style.width = "100%"; - textComponent - .setValue(value ?? "") - .inputEl.addEventListener("keydown", this.submitEnterCallback); - - return textComponent; - } - - private submitClickCallback = (_: MouseEvent) => this.submit(); - private cancelClickCallback = (_: MouseEvent) => this.cancel(); - - private submitEnterCallback = (evt: KeyboardEvent) => { - if ((evt.ctrlKey || evt.metaKey) && evt.key === "Enter") { - evt.preventDefault(); - this.submit(); - } - }; - - private submit() { - this.didSubmit = true; - this.input = this.inputComponent.getValue(); - this.close(); - } - - private cancel() { - this.close(); - } - - onOpen() { - super.onOpen(); - - this.inputComponent.inputEl.focus(); - } - - onClose() { - super.onClose(); - this.resolveInput(); - this.removeInputListener(); - } - - private resolveInput() { - if (!this.didSubmit) this.rejectPromise(t("NO_INPUT")); - else this.resolvePromise(this.input); - } - - private removeInputListener() { - this.inputComponent.inputEl.removeEventListener("keydown", this.submitEnterCallback); - } -} diff --git a/src/lang/locale/pl.ts b/src/lang/locale/pl.ts index d6dee999..3d3cf47d 100644 --- a/src/lang/locale/pl.ts +++ b/src/lang/locale/pl.ts @@ -1,4 +1,4 @@ -//język polski +// język polski export default { // flashcard-modal.tsx diff --git a/src/main.ts b/src/main.ts index daade40d..a8c3c278 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,9 +10,9 @@ import { import * as graph from "pagerank.js"; import { SRSettingTab, SRSettings, DEFAULT_SETTINGS, upgradeSettings } from "src/settings"; -import { FlashcardModal } from "src/gui/flashcard-modal"; -import { StatsModal } from "src/gui/stats-modal"; -import { ReviewQueueListView, REVIEW_QUEUE_VIEW_TYPE } from "src/gui/sidebar"; +import { FlashcardModal } from "src/gui/FlashcardModal"; +import { StatsModal } from "src/gui/StatsModal"; +import { ReviewQueueListView, REVIEW_QUEUE_VIEW_TYPE } from "src/gui/Sidebar"; import { ReviewResponse, schedule } from "src/scheduling"; import { YAML_FRONT_MATTER_REGEX, SCHEDULING_INFO_REGEX } from "src/constants"; import { ReviewDeck, ReviewDeckSelectionModal } from "src/ReviewDeck"; diff --git a/styles.css b/styles.css index b569b986..af8e7a1d 100644 --- a/styles.css +++ b/styles.css @@ -1,162 +1,284 @@ -.is-mobile .sr-modal { +.is-mobile #sr-modal { --top-space: calc(var(--safe-area-inset-top) + var(--header-height) + var(--size-4-2)); width: 100vw !important; height: calc(100vh - var(--top-space)) !important; margin-top: var(--top-space); } -.sr-flashcard-menu { - width: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - gap: 1rem; +#sr-modal .modal-title { + display: none; } -.sr-flashcard-menu-item { - box-shadow: none !important; - cursor: pointer; +#sr-modal .modal-close-button { + z-index: 21; } -.sr-flashcard-menu-item:disabled { - cursor: not-allowed; +body:not(.native-scrollbars) #sr-modal .modal-close-button { + top: 12px; } -.sr-flashcard-input-modal { - height: 80%; +.sr-modal-content { + position: relative; + overflow: hidden; } -.sr-flashcard-input-area { - height: 80%; +.sr-is-hidden { + display: none !important; } -.sr-flashcard-input-area > textarea { - height: 100%; +.sr-centered { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; } -.sr-flashcard-edit-button-bar { +.sr-deck-list, +.sr-flashcard, +.sr-edit-view { display: flex; - flex-direction: row-reverse; - justify-content: space-between; + flex-direction: column; width: 100%; - margin-top: 1rem; + height: 100%; } -.sr-flashcard-response { - display: inline-grid; +.sr-header { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; width: 100%; - grid-template-columns: auto auto auto; - position: absolute; - bottom: 0; + border-bottom: 1px solid var(--hr-color); } -.sr-ignorestats-btn { - width: 100%; +.sr-title { + font-size: var(--font-ui-large); + font-weight: var(--font-semibold); + text-align: center; + line-height: var(--line-height-tight); } -.sr-ignorestats-response { - grid-template-columns: auto auto; - gap: var(--size-4-4); +.sr-content { + overflow-y: auto; } -.sr-centered { +.sr-button { + box-shadow: none !important; + cursor: pointer; +} + +.sr-back-button { + z-index: 21; + cursor: var(--cursor); + position: absolute; + top: 12px; + left: 12px; + font-size: 26px; + line-height: 22px; + height: 26px; + width: 26px; + padding: 0 var(--size-2-2); + border-radius: var(--radius-s); + color: var(--text-muted); display: flex; justify-content: center; align-items: center; - flex-direction: column; } -.sr-deck-counts { - color: #ffffff; - margin-left: 4px; - padding: 4px; +.sr-back-button:hover, +.sr-button:hover { + background-color: var(--background-modifier-hover); } -#sr-show-answer { +.sr-response { + display: flex; + width: 100%; + margin-top: var(--size-4-4); + gap: var(--size-4-4); +} + +.sr-response-button { height: 48px; + flex-grow: 1; + margin: auto; line-height: 48px; - width: 100%; text-align: center; - position: absolute; - bottom: 0; cursor: pointer; - background-color: #2196f3; - color: #ffffff; border-radius: 4px; user-select: text; } -#sr-hr-card-divide { - backdrop-filter: invert(40%); - border: none; - height: 2px; +.sr-bg-blue, +.sr-bg-green, +.sr-bg-red { + color: #ffffff !important; } -#sr-hard-btn, -#sr-good-btn, -#sr-easy-btn { - height: 48px; - margin: auto; +.sr-bg-green { + background-color: #4caf50 !important; +} + +.sr-bg-blue { + background-color: #2094f3 !important; +} + +.sr-bg-red { + background-color: #ff7043 !important; +} + +.sr-deck-list .sr-tree-item-row:hover .sr-bg-green, +.sr-response-button.sr-bg-green:hover { + background-color: hsl(122, 39%, 44%) !important; +} + +.sr-deck-list .sr-tree-item-row:hover .sr-bg-blue, +.sr-response-button.sr-bg-blue:hover { + background-color: hsl(207, 90%, 49%) !important; +} + +.sr-deck-list .sr-tree-item-row:hover .sr-bg-red, +.sr-response-button.sr-bg-red:hover { + background-color: hsl(14, 100%, 58%) !important; +} + +/* -> DeckListView */ + +.sr-deck-list .sr-header { + gap: 8px; + padding-bottom: 14px; + margin-bottom: 24px; +} + +.sr-deck-list .sr-header-stats-container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sr-deck-list .sr-header-stats-count { + display: flex; color: #ffffff; - cursor: pointer; + padding: 4px; + gap: 4px; +} + +.sr-deck-list .sr-header-stats-count > *:first-child { + min-width: 10ch; +} + +.sr-deck-list .sr-header-stats-count > *:last-child { + min-width: 3ch; + text-align: right; +} + +.sr-deck-list .sr-tree-item-row { + padding-top: 2px; + padding-bottom: 2px; + margin-bottom: 0; } -#sr-hard-btn { - background-color: #f44336; +.sr-deck-list .sr-tree-stats-container { + display: flex; + gap: 4px; +} + +.sr-deck-list .sr-tree-stats-count { + min-width: 3ch; + padding: 4px; + box-sizing: content-box; + text-align: center; + color: #ffffff !important; } -#sr-good-btn { - background-color: #2196f3; +/* -> FlashcardReviewView */ + +.sr-flashcard .sr-header { + position: relative; + gap: 8px; + padding-bottom: 8px; } -#sr-easy-btn { - background-color: #4caf50; +.sr-flashcard .sr-button:disabled { + cursor: not-allowed; } -#sr-context { +.sr-flashcard .sr-back-button { + position: absolute; + left: 0; + top: 0; + z-index: 21; +} + +.sr-flashcard .sr-controls { + display: flex; + gap: var(--size-4-4); +} + +.sr-flashcard .sr-context { font-style: italic; - font-weight: bold; - margin-top: 16px; + color: var(--text-faint); display: block; width: 100%; + margin-top: 12px; + margin-bottom: 4px; + margin-left: 8px; } -#sr-flashcard-view { +.sr-flashcard .sr-content { font-size: var(--font-text-size); overflow-y: auto; - height: 80%; user-select: text; + padding-inline: 8px; + width: 100%; + flex-grow: 1; } -#sr-chart-period { - appearance: menulist; - border-right: 8px solid transparent; +.sr-flashcard .sr-card-divide { + backdrop-filter: invert(40%); + border-top-style: dashed; +} + +/* -> EditModal */ + +.sr-edit-modal { + height: 80%; +} + +.sr-edit-view { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--size-4-4); } -@media only screen and (max-width: 600px) { - .sr-back-btn { - width: initial !important; - } +.sr-edit-view .sr-input { + flex-grow: 1; + width: 100%; + resize: none; +} - .sr-modal-content { - width: 98% !important; - } +.sr-edit-view .sr-response { + display: grid; + grid-template-columns: auto auto auto; + width: 100%; + margin-top: 0; +} - .sr-modal-content::-webkit-scrollbar, - #sr-flashcard-view::-webkit-scrollbar { - display: none; - } +.sr-edit-view .sr-response-button { + width: 100%; +} + +.sr-edit-view .sr-response-button.sr-spacer { + opacity: 0; + cursor: default; +} - .sr-flashcard-response, - #sr-show-answer { - width: 93.5% !important; - line-height: 60px; - } +/* -> Statistics */ - #sr-hard-btn, - #sr-good-btn, - #sr-easy-btn { - width: 100px; - } +#sr-chart-period { + appearance: menulist; + border-right: 8px solid transparent; }