Skip to content

Commit

Permalink
Flashcard UI/UX
Browse files Browse the repository at this point in the history
  • Loading branch information
st3v3nmw committed Apr 18, 2021
1 parent a4c2fd6 commit efb8d78
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 53 deletions.
2 changes: 2 additions & 0 deletions TODO.md
@@ -0,0 +1,2 @@
1. Change to /^(.+)::(.+)(?:\n<!--SR:(ID)-->)?/gm (ID points to some stored id for ref. to the cards., generate ID using ms timestamps)
2. Document flashcard review shortcuts (Space/Enter => Show answer, 1 => Hard, 2 => Good, 3 => Easy)
198 changes: 177 additions & 21 deletions src/flashcard-modal.ts
@@ -1,22 +1,32 @@
import { Modal, App } from "obsidian";
import { Modal, App, MarkdownRenderer, Notice } from "obsidian";
import type SRPlugin from "./main";
import { BasicCard, ClozeCard } from "./main";
import { Card } from "./main";

enum UserResponse {
ShowAnswer,
ReviewEasy,
ReviewGood,
ReviewHard,
ReviewGood,
ReviewEasy,
Skip,
}

enum Mode {
Front,
Back,
}

export class FlashcardModal extends Modal {
private plugin: SRPlugin;
private answerBtn: HTMLElement;
private flashcardView: HTMLElement;
private hardBtn: HTMLElement;
private goodBtn: HTMLElement;
private easyBtn: HTMLElement;
private responseDiv: HTMLElement;
private fileLinkView: HTMLElement;
private contextView: HTMLElement;
private currentCard: BasicCard | ClozeCard;
private currentCard: Card;
private mode: Mode;

constructor(app: App, plugin: SRPlugin) {
super(app);
Expand All @@ -30,39 +40,134 @@ export class FlashcardModal extends Modal {
this.contentEl.style.position = "relative";
this.contentEl.style.height = "92%";

this.fileLinkView = createDiv("link");
this.fileLinkView = createDiv("sr-link");
this.fileLinkView.setText("Open file");
this.contentEl.appendChild(this.fileLinkView);

this.contextView = document.createElement("div");
this.contextView.setAttribute("id", "sr-context");
this.contentEl.appendChild(this.contextView);

this.flashcardView = document.createElement("div");
this.contentEl.appendChild(this.flashcardView);

this.responseDiv = createDiv("sr-response");

this.hardBtn = document.createElement("button");
this.hardBtn.setAttribute("id", "sr-hard-btn");
this.hardBtn.setText("Hard");
this.hardBtn.addEventListener("click", (_) => {
this.processResponse(UserResponse.ReviewHard);
});
this.responseDiv.appendChild(this.hardBtn);

this.goodBtn = document.createElement("button");
this.goodBtn.setAttribute("id", "sr-good-btn");
this.goodBtn.setText("Good");
this.goodBtn.addEventListener("click", (_) => {
this.processResponse(UserResponse.ReviewGood);
});
this.responseDiv.appendChild(this.goodBtn);

this.easyBtn = document.createElement("button");
this.easyBtn.setAttribute("id", "sr-easy-btn");
this.easyBtn.setText("Easy");
this.easyBtn.addEventListener("click", (_) => {
this.processResponse(UserResponse.ReviewEasy);
});
this.responseDiv.appendChild(this.easyBtn);
this.responseDiv.style.display = "none";

this.contentEl.appendChild(this.responseDiv);

this.answerBtn = document.createElement("div");
this.answerBtn.setAttribute("id", "show-answer");
this.answerBtn.setAttribute("id", "sr-show-answer");
this.answerBtn.setText("Show Answer");
this.answerBtn.addEventListener("click", (_) => {
this.processResponse(UserResponse.ShowAnswer);
});
this.contentEl.appendChild(this.answerBtn);
}

onOpen() {
document.body.onkeypress = (e) => {
if (e.code === "Space")
if (
this.mode == Mode.Front &&
(e.code == "Space" || e.code == "Enter")
)
this.processResponse(UserResponse.ShowAnswer);
else if (this.mode == Mode.Back) {
if (e.code == "Numpad1" || e.code == "Digit1")
this.processResponse(UserResponse.ReviewHard);
else if (e.code == "Numpad2" || e.code == "Digit2")
this.processResponse(UserResponse.ReviewGood);
else if (e.code == "Numpad3" || e.code == "Digit3")
this.processResponse(UserResponse.ReviewEasy);
}
};
}

this.titleEl.setText(
`Queue - ${
this.plugin.newFlashcards.length +
this.plugin.scheduledFlashcards.length
}`
);
onOpen() {
this.nextCard();
}

onClose() {}

nextCard() {
this.responseDiv.style.display = "none";
let count =
this.plugin.newFlashcards.length + this.plugin.dueFlashcards.length;
this.titleEl.setText(`Queue - ${count}`);

if (count == 0) {
this.fileLinkView.innerHTML = "";
this.contextView.innerHTML = "";
this.flashcardView.innerHTML =
"<h3 style='text-align: center; margin-top: 50%;'>You're done for the day :D.</h3>";
return;
}

this.answerBtn.style.display = "initial";
this.flashcardView.innerHTML = "";
this.mode = Mode.Front;

if (this.plugin.newFlashcards.length > 0) {
this.currentCard = this.plugin.newFlashcards[0];
this.flashcardView.setText(this.currentCard.front);
} else if (this.plugin.scheduledFlashcards.length > 0) {
MarkdownRenderer.renderMarkdown(
this.currentCard.front,
this.flashcardView,
this.currentCard.note.path,
this.plugin
);
this.hardBtn.setText("Hard - 1.0 day(s)");
this.goodBtn.setText("Good - 2.5 day(s)");
this.easyBtn.setText("Easy - 2.7 day(s)");
} else if (this.plugin.dueFlashcards.length > 0) {
this.currentCard = this.plugin.dueFlashcards[0];
MarkdownRenderer.renderMarkdown(
this.currentCard.front,
this.flashcardView,
this.currentCard.note.path,
this.plugin
);

let hardInterval = this.nextState(
UserResponse.ReviewHard,
this.currentCard.interval,
this.currentCard.ease
).interval;
let goodInterval = this.nextState(
UserResponse.ReviewGood,
this.currentCard.interval,
this.currentCard.ease
).interval;
let easyInterval = this.nextState(
UserResponse.ReviewEasy,
this.currentCard.interval,
this.currentCard.ease
).interval;

this.hardBtn.setText(`Hard - ${hardInterval} day(s)`);
this.goodBtn.setText(`Good - ${goodInterval} day(s)`);
this.easyBtn.setText(`Easy - ${easyInterval} day(s)`);
}

this.contextView.setText(this.currentCard.context);
Expand All @@ -74,9 +179,60 @@ export class FlashcardModal extends Modal {
});
}

onClose() {}

processResponse(response: UserResponse) {
this.flashcardView.setText(this.currentCard.back);
if (response == UserResponse.ShowAnswer) {
this.mode = Mode.Back;

this.answerBtn.style.display = "none";
this.responseDiv.style.display = "grid";

let hr = document.createElement("hr");
hr.setAttribute("id", "sr-hr-card-divide");
this.flashcardView.appendChild(hr);
MarkdownRenderer.renderMarkdown(
this.currentCard.back,
this.flashcardView,
this.currentCard.note.path,
this.plugin
);
} else if (
response == UserResponse.ReviewHard ||
response == UserResponse.ReviewGood ||
response == UserResponse.ReviewEasy
) {
let interval, ease;
// scheduled card
if (this.currentCard.dueUnix) {
interval = this.currentCard.interval;
ease = this.currentCard.ease;
this.plugin.dueFlashcards.splice(0, 1);
} else {
interval = 250;
ease = 1;
this.plugin.newFlashcards.splice(0, 1);
}

// TODO: save responses

this.nextCard();
}
}

nextState(response: UserResponse, interval: number, ease: number) {
if (response != UserResponse.ReviewGood) {
ease =
response == UserResponse.ReviewEasy
? ease + 20
: Math.max(130, ease - 20);
}

interval = Math.max(
1,
response != UserResponse.ReviewHard
? (interval * ease) / 100
: interval * this.plugin.data.settings.lapsesIntervalChange
);

return { ease, interval: Math.round(interval * 10) / 10 };
}
}
75 changes: 49 additions & 26 deletions src/main.ts
@@ -1,4 +1,4 @@
import { Notice, Plugin, addIcon, TFile } from "obsidian";
import { Notice, Plugin, addIcon, TFile, HeadingCache } from "obsidian";
import * as graph from "pagerank.js";
import { SRSettings, SRSettingTab, DEFAULT_SETTINGS } from "./settings";
import { FlashcardModal } from "./flashcard-modal";
Expand Down Expand Up @@ -34,21 +34,16 @@ enum ReviewResponse {
Hard,
}

interface Card {
export interface Card {
dueUnix?: number;
ease?: number;
interval?: number;
context: string;
context?: string;
note: TFile;
}

export interface BasicCard extends Card {
front: string;
back: string;
}

export interface ClozeCard extends Card {}

export default class SRPlugin extends Plugin {
private statusBar: HTMLElement;
private reviewQueueView: ReviewQueueListView;
Expand All @@ -61,9 +56,8 @@ export default class SRPlugin extends Plugin {
private pageranks: Record<string, number> = {};
private dueNotesCount: number = 0;

public newFlashcards: (BasicCard | ClozeCard)[] = [];
public scheduledFlashcards: (BasicCard | ClozeCard)[] = [];
public dueFlashcardsCount: number = 0;
public newFlashcards: Card[] = [];
public dueFlashcards: Card[] = [];

async onload() {
await this.loadPluginData();
Expand Down Expand Up @@ -187,8 +181,7 @@ export default class SRPlugin extends Plugin {
this.dueNotesCount = 0;

this.newFlashcards = [];
this.scheduledFlashcards = [];
this.dueFlashcardsCount = 0;
this.dueFlashcards = [];

let now = Date.now();
for (let note of notes) {
Expand Down Expand Up @@ -443,25 +436,55 @@ export default class SRPlugin extends Plugin {

async findFlashcards(note: TFile) {
let fileText = await this.app.vault.read(note);
let fileCachedData = this.app.metadataCache.getFileCache(note) || {};
let headings = fileCachedData.headings || [];

let now = Date.now();
for (let match of fileText.matchAll(REMNOTE_STYLE_REGEX)) {
let cardObj: BasicCard = {
front: match[1],
back: match[2],
context: "write > context > function",
note,
};

// note has scheduling information
let cardObj: Card;
// flashcard has scheduling information
if (match[3]) {
cardObj.dueUnix = Number.parseInt(match[3]);
cardObj.ease = Number.parseInt(match[4]);
cardObj.interval = Number.parseInt(match[5]);

this.scheduledFlashcards.push(cardObj);
// flashcard due for review
if (Number.parseInt(match[3]) <= now) {
cardObj = {
front: match[1],
back: match[2],
note,
dueUnix: Number.parseInt(match[3]),
ease: Number.parseInt(match[4]),
interval: Number.parseInt(match[5]),
};
this.dueFlashcards.push(cardObj);
} else {
continue;
}
} else {
cardObj = {
front: match[1],
back: match[2],
note,
};
this.newFlashcards.push(cardObj);
}

let cardOffset = match.index;
let stack: HeadingCache[] = [];
for (let heading of headings) {
if (heading.position.start.offset > cardOffset) break;

while (
stack.length > 0 &&
stack[stack.length - 1].level >= heading.level
)
stack.pop();

stack.push(heading);
}

cardObj.context = "";
for (let headingObj of stack)
cardObj.context += headingObj.heading + " > ";
cardObj.context = cardObj.context.slice(0, -3);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/settings.ts
Expand Up @@ -170,7 +170,7 @@ export class SRSettingTab extends PluginSettingTab {
})
);

let helpEl = containerEl.createDiv("help-div");
let helpEl = containerEl.createDiv("sr-help-div");
helpEl.innerHTML =
'<a href="https://github.com/st3v3nmw/obsidian-spaced-repetition/blob/master/README.md">For more information, check the README.</a>';
}
Expand Down

0 comments on commit efb8d78

Please sign in to comment.