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;
}