Skip to content

Commit

Permalink
feat: add times under review buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianWoelki committed Jul 7, 2024
1 parent 9d67671 commit 911d2dc
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 29 deletions.
100 changes: 100 additions & 0 deletions src/spaced-repetition/anki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,106 @@ export class AnkiAlgorithm extends SpacedRepetitionAlgorithm<AnkiParameters> {
};
}

public calculatePotentialNextReviewDate(
item: SpacedRepetitionItem,
performanceResponse: PerformanceResponse,
): Date {
const newItem = { ...item };

const updateStrategies = {
[PerformanceResponse.AGAIN]: () => {
newItem.easeFactor = Math.max(
this.parameters.minEaseFactor,
newItem.easeFactor - this.parameters.easeFactorDecrement,
);
if (newItem.state === CardState.REVIEW) {
newItem.state = CardState.RELEARNING;
newItem.stepIndex = 0;
} else {
newItem.stepIndex = 0;
}
return newItem.interval * this.parameters.lapseInterval;
},
[PerformanceResponse.HARD]: () => {
newItem.easeFactor = Math.max(
this.parameters.minEaseFactor,
newItem.easeFactor - this.parameters.easeFactorIncrement,
);
if (
newItem.state === CardState.LEARNING ||
newItem.state === CardState.RELEARNING
) {
newItem.stepIndex += 1;
}
return Math.max(
newItem.interval * this.parameters.hardIntervalMultiplier,
newItem.interval + 1,
);
},
[PerformanceResponse.GOOD]: () => {
if (
newItem.state === CardState.NEW ||
newItem.state === CardState.LEARNING ||
newItem.state === CardState.RELEARNING
) {
newItem.stepIndex += 1;
const steps =
newItem.state === CardState.LEARNING
? this.parameters.learningSteps
: this.parameters.relearningSteps;
if (newItem.stepIndex >= steps.length) {
newItem.state = CardState.REVIEW;
return this.parameters.graduatingInterval;
}
return 0;
}
return Math.max(
newItem.interval * newItem.easeFactor,
newItem.interval + 1,
);
},
[PerformanceResponse.EASY]: () => {
newItem.easeFactor += this.parameters.easeFactorIncrement;
if (
newItem.state === CardState.NEW ||
newItem.state === CardState.LEARNING ||
newItem.state === CardState.RELEARNING
) {
newItem.state = CardState.REVIEW;
return this.parameters.easyInterval;
}
return (
newItem.interval * newItem.easeFactor * this.parameters.easyBonus
);
},
};

if (newItem.state === CardState.NEW) {
newItem.state = CardState.LEARNING;
newItem.stepIndex = 0;
}

const newInterval = updateStrategies[performanceResponse]();

if (
(newItem.state === CardState.LEARNING ||
newItem.state === CardState.RELEARNING) &&
newItem.stepIndex <
(newItem.state === CardState.LEARNING
? this.parameters.learningSteps
: this.parameters.relearningSteps
).length
) {
const steps =
newItem.state === CardState.LEARNING
? this.parameters.learningSteps
: this.parameters.relearningSteps;
return this.calculateNextReviewDate(steps[newItem.stepIndex], true);
} else {
return this.calculateNextReviewDate(newInterval);
}
}

public scheduleReview(item: SpacedRepetitionItem): void {
item.lastReviewDate = new Date();

Expand Down
12 changes: 10 additions & 2 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@
.better-recall-review-card__answer-buttons-bar button {
display: flex;
flex-direction: column;
height: calc(var(--input-height) * 2.5);
height: calc(var(--input-height) * 3);
width: unset;
max-width: calc(var(--input-height) * 5);
min-width: calc(var(--input-height) * 2.5);
min-width: calc(var(--input-height) * 3);
padding: 0 var(--size-4-4);
}

Expand Down Expand Up @@ -168,3 +168,11 @@
align-items: center;
justify-content: space-between;
}

.better-recall-review-card__time {
font-size: var(--font-smallest);
padding: var(--size-2-1) var(--size-2-2);
margin-top: var(--size-2-2);
border: 1px solid var(--color-base-35);
border-radius: var(--radius-s);
}
61 changes: 34 additions & 27 deletions src/ui/views/review-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RecallSubView } from './sub-view';
import { Deck } from 'src/data/deck';
import { BUTTONS_BAR_CLASS, CENTERED_VIEW } from '../classes';
import { ButtonComponent } from 'obsidian';
import { formatTimeDifference } from 'src/util';

enum ReviewState {
ONGOING,
Expand Down Expand Up @@ -124,33 +125,39 @@ export class ReviewView extends RecallSubView {
`${BUTTONS_BAR_CLASS} better-recall-review-card__answer-buttons-bar`,
);

const againButton = new ButtonComponent(this.recallButtonsBarEl);
const againEmojiEl = againButton.buttonEl.createSpan();
const againTextEl = againButton.buttonEl.createSpan();
againEmojiEl.setText('❌');
againTextEl.setText('Again');
againButton.onClick(() => this.handleResponse(PerformanceResponse.AGAIN));

const goodButton = new ButtonComponent(this.recallButtonsBarEl);
const goodEmojiEl = goodButton.buttonEl.createSpan();
const goodTextEl = goodButton.buttonEl.createSpan();
goodEmojiEl.setText('😬');
goodTextEl.setText('Good');
goodButton.onClick(() => this.handleResponse(PerformanceResponse.GOOD));

const hardButton = new ButtonComponent(this.recallButtonsBarEl);
const hardEmojiEl = hardButton.buttonEl.createSpan();
const hardTextEl = hardButton.buttonEl.createSpan();
hardEmojiEl.setText('😰');
hardTextEl.setText('Hard');
hardButton.onClick(() => this.handleResponse(PerformanceResponse.HARD));

const easyButton = new ButtonComponent(this.recallButtonsBarEl);
const easyEmojiEl = easyButton.buttonEl.createSpan();
const easyTextEl = easyButton.buttonEl.createSpan();
easyEmojiEl.setText('👑');
easyTextEl.setText('Easy');
easyButton.onClick(() => this.handleResponse(PerformanceResponse.EASY));
this.renderButton(PerformanceResponse.AGAIN, '❌', 'Again');

this.renderButton(PerformanceResponse.GOOD, '😬', 'Good');

this.renderButton(PerformanceResponse.HARD, '😰', 'Hard');

this.renderButton(PerformanceResponse.EASY, '👑', 'Easy');
}

private renderButton(
performanceResponse: PerformanceResponse,
emoji: string,
text: string,
): void {
if (!this.currentItem) {
return;
}

const button = new ButtonComponent(this.recallButtonsBarEl);
const emojiEl = button.buttonEl.createSpan();
const textEl = button.buttonEl.createSpan();
const timeEl = button.buttonEl.createSpan(
'better-recall-review-card__time',
);
emojiEl.setText(emoji);
textEl.setText(text);

const nextReviewDate = this.ankiAlgorithm.calculatePotentialNextReviewDate(
this.currentItem,
performanceResponse,
);
timeEl.setText(formatTimeDifference(nextReviewDate));
button.onClick(() => this.handleResponse(performanceResponse));
}

private showRecallButtons(): void {
Expand Down
38 changes: 38 additions & 0 deletions src/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { vi, it, beforeEach, afterEach, expect } from 'vitest';
import { formatTimeDifference } from './util';

const mockNow = new Date();

beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(mockNow);
});

afterEach(() => {
vi.useRealTimers();
});

it('should return "1 min" for exactly one minute or less than that', () => {
const exactlyOneDate = new Date(mockNow.getTime() + 60 * 1000);
expect(formatTimeDifference(exactlyOneDate)).toBe('1 min');

const lessThanOneDate = new Date(mockNow.getTime() + 30 * 1000);
expect(formatTimeDifference(lessThanOneDate)).toBe('1 min');
});

it.each([
[5 * 60 * 1000, '5 mins', 'multiple minutes'],
[60 * 60 * 1000, '1 hour', 'exactly one hour'],
[3 * 60 * 60 * 1000, '3 hours', 'multiple hours'],
[25 * 60 * 60 * 1000, '1 day', 'exactly one day'],
[4 * 25 * 60 * 60 * 1000, '4 days', 'multiple days'],
[7 * 25 * 60 * 60 * 1000, '1 week', 'exactly one week'],
[3 * 7 * 25 * 60 * 60 * 1000, '3 weeks', 'multiple weeks'],
[30 * 25 * 60 * 60 * 1000, '1 month', 'approximately one month'],
[3 * 30 * 25 * 60 * 60 * 1000, '3 months', 'multiple months'],
[365 * 25 * 60 * 60 * 1000, '1 year', 'approximately one year'],
[2 * 365 * 25 * 60 * 60 * 1000, '2 years', 'multiple years'],
])('should return "%s" for %s (%i ms)', (milliseconds, expected) => {
const futureDate = new Date(mockNow.getTime() + milliseconds);
expect(formatTimeDifference(futureDate)).toBe(expected);
});
26 changes: 26 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function formatTimeDifference(futureDate: Date): string {
const now = new Date();
const differenceInMillis = futureDate.getTime() - now.getTime();
const seconds = Math.floor(differenceInMillis / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const weeks = Math.floor(days / 7);
const months = Math.floor(days / 30.44);
const years = Math.floor(days / 365.25);

if (years > 0) {
return `${years} ${years === 1 ? 'year' : 'years'}`;
} else if (months > 0) {
return `${months} ${months === 1 ? 'month' : 'months'}`;
} else if (weeks > 0) {
return `${weeks} ${weeks === 1 ? 'week' : 'weeks'}`;
} else if (days > 0) {
return `${days} ${days === 1 ? 'day' : 'days'}`;
} else if (hours > 0) {
return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
} else {
const printedMinutes = Math.max(1, minutes);
return `${printedMinutes} ${printedMinutes === 1 ? 'min' : 'mins'}`;
}
}

0 comments on commit 911d2dc

Please sign in to comment.