// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { CardAnswer, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb"; import { nextCardData, playAudio } from "@generated/backend"; import { derived, get, writable } from "svelte/store"; import type { InnerReviewerRequest } from "../reviewer-inner/innerReviewerRequest"; import type { ReviewerRequest } from "./reviewerRequest"; export function isNightMode() { // https://stackoverflow.com/a/57795518 // This will be true in browsers if darkmode but also false in the reviewer if darkmode // If in the reviewer then this will need to be set by the python instead return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) || document.documentElement.classList.contains("night-mode"); } export function enableNightMode() { document.documentElement.classList.add("night-mode"); document.documentElement.setAttribute("data-bs-theme", "dark"); } export function updateNightMode() { if (isNightMode()) { enableNightMode(); } } export class ReviewerState { answerHtml = ""; _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); readonly cardClass = writable(""); readonly answerShown = writable(false); readonly cardData = writable(undefined); readonly answerButtons = derived(this.cardData, ($cardData) => $cardData?.answerButtons ?? []); iframe: HTMLIFrameElement | undefined = undefined; onReady() { this.showQuestion(null); addEventListener("message", this.onMessage.bind(this)); } playAudio(answerSide: boolean, index?: number) { playAudio({ answerSide, index, cid: this.currentCard!.card!.id }); } onMessage(e: MessageEvent) { switch (e.data.type) { case "audio": { this.playAudio(e.data.answerSide, e.data.index); break; } } } public registerIFrame(iframe: HTMLIFrameElement) { this.iframe = iframe; iframe.addEventListener("load", this.onReady.bind(this)); } onKeyDown(e: KeyboardEvent) { switch (e.key) { case "1": { this.easeButtonPressed(0); break; } case "2": { this.easeButtonPressed(1); break; } case "3": { this.easeButtonPressed(2); break; } case "4": { this.easeButtonPressed(3); break; } case " ": { if (!get(this.answerShown)) { this.showAnswer(); } else { this.easeButtonPressed(2); } break; } } } public registerShortcuts() { document.addEventListener("keydown", this.onKeyDown.bind(this)); } sendInnerRequest(message: InnerReviewerRequest) { this.iframe?.contentWindow?.postMessage(message, "*"); } updateHtml(htmlString: string, css?: string, bodyclass?: string) { this.sendInnerRequest({ type: "html", value: htmlString, css, bodyclass }); } async showQuestion(answer: CardAnswer | null) { const resp = await nextCardData({ answer: answer || undefined, }); // TODO: "Congratulation screen" logic this._cardData = resp.nextCard; this.cardData.set(this._cardData); this.answerShown.set(false); const question = resp.nextCard?.front || ""; this.updateHtml(question, resp?.nextCard?.css, resp?.nextCard?.bodyClass); this.playAudio(false) this.beginAnsweringMs = Date.now(); } get currentCard() { return this._cardData?.queue?.cards[0]; } public showAnswer() { this.answerShown.set(true); this.playAudio(true) this.updateHtml(this._cardData?.back || ""); } public easeButtonPressed(rating: number) { if (!get(this.answerShown)) { return; } const states = this.currentCard!.states!; const newState = [ states.again!, states.hard!, states.good!, states.easy!, ][rating]!; this.showQuestion( new CardAnswer({ rating: rating, currentState: states!.current!, newState, cardId: this.currentCard?.card?.id, answeredAtMillis: BigInt(Date.now()), millisecondsTaken: Date.now() - this.beginAnsweringMs, }), ); } }