Added: SchedulingState class

This commit is contained in:
Luc Mcgrady 2025-10-04 18:28:37 +01:00
parent 44cc5f82e9
commit cadf59f8d6
No known key found for this signature in database
GPG key ID: 4F3D7A0B17CC3D9C
6 changed files with 48 additions and 168 deletions

View file

@ -3,13 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script>
import ReviewerBottomOuter from "./reviewer-bottom/ReviewerBottomOuter.svelte";
import { ReviewerState } from "./reviewer";
import ReviewerBottom from "./reviewer-bottom/ReviewerBottom.svelte";
import Reviewer from "./Reviewer.svelte";
let state = new ReviewerState
</script>
<div>
<Reviewer></Reviewer>
<ReviewerBottomOuter></ReviewerBottomOuter>
<Reviewer {state}></Reviewer>
<ReviewerBottom {state}></ReviewerBottom>
</div>
<style>

View file

@ -3,14 +3,17 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { setupReviewer } from "./reviewer";
import type { ReviewerState } from "./reviewer";
let iframe: HTMLIFrameElement;
export let state: ReviewerState
$: if (iframe) state.registerIFrame(iframe)
$: ({ cardClass } = setupReviewer(iframe));
</script>
<div id="qa" class={$cardClass}>
<div id="qa">
<iframe
src="/_anki/pages/reviewer-inner.html"
bind:this={iframe}

View file

@ -3,18 +3,20 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { Writable } from "svelte/store";
import AnswerButton from "./AnswerButton.svelte";
import { bridgeCommand } from "@tslib/bridgecommand";
import * as tr from "@generated/ftl";
import RemainingNumber from "./RemainingNumber.svelte";
import type { AnswerButtonInfo } from "./types";
import type { ReviewerState } from "../reviewer";
import { writable } from "svelte/store";
export let answerButtons: Writable<AnswerButtonInfo[]>;
export let remaining: Writable<number[]>;
export let remainingIndex: Writable<number>;
export let state: ReviewerState
// Placeholders
let answerButtons = writable([]);
let remaining = writable([0, 0, 0]);
let remainingIndex = writable(0);
$: console.log($remaining);
$: answerShown = $answerButtons.length;
</script>

View file

@ -1,20 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount } from "svelte";
import ReviewerBottom from "./ReviewerBottom.svelte";
import "./index.scss";
import { setupBottomBar } from "./reviewer-bottom";
let reviewerInfo: null | ReturnType<typeof setupBottomBar> = null;
onMount(() => {
reviewerInfo = setupBottomBar();
});
</script>
{#if reviewerInfo}
<ReviewerBottom {...reviewerInfo}></ReviewerBottom>
{/if}

View file

@ -1,86 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { writable } from "svelte/store";
import type { AnswerButtonInfo } from "./types";
export function setupBottomBar() {
/*
let timerStopped = false;
let maxTime = 0;
function updateTime(): void {
const timeNode = document.getElementById("time");
if (maxTime === 0) {
timeNode.textContent = "";
return;
}
globalThis.time = Math.min(maxTime, globalThis.time);
const m = Math.floor(globalThis.time / 60);
const s = globalThis.time % 60;
const sStr = String(s).padStart(2, "0");
const timeString = `${m}:${sStr}`;
if (maxTime === time) {
timeNode.innerHTML = `<font color=red>${timeString}</font>`;
} else {
timeNode.textContent = timeString;
}
}*/
const answerButtons = writable<AnswerButtonInfo[]>([]);
const remaining = writable<[number, number, number]>([0, 0, 0]);
const remainingIndex = writable<number>(-1);
let intervalId: number | undefined;
function _showQuestion(_txt: string, _maxTime_: number): void {
_showAnswer([]);
globalThis.time = 0;
// maxTime = maxTime_;
// updateTime();
if (intervalId !== undefined) {
clearInterval(intervalId);
}
/*
intervalId = setInterval(function() {
if (!timerStopped) {
globalThis.time += 1;
//updateTime();
}
}, 1000);*/
}
function _showAnswer(info: AnswerButtonInfo[], _stopTimer = false): void {
console.log(info);
answerButtons.set(info);
// timerStopped = stopTimer;
}
function _updateRemaining(counts: [number, number, number], idx: number) {
remaining.set(counts);
remainingIndex.set(idx);
}
globalThis.showQuestion = _showQuestion;
globalThis.showAnswer = _showAnswer;
globalThis._updateRemaining = _updateRemaining;
/*
function selectedAnswerButton(): string | undefined {
const node = document.activeElement as HTMLElement;
if (!node) {
return;
}
return node.dataset.ease;
}
*/
return {
answerButtons,
remaining,
remainingIndex,
};
}

View file

@ -5,42 +5,45 @@ import {
type NextCardDataResponse_NextCardData,
} from "@generated/anki/scheduler_pb";
import { nextCardData } from "@generated/backend";
import { bridgeCommand } from "@tslib/bridgecommand";
import { writable } from "svelte/store";
export function setupReviewer(iframe: HTMLIFrameElement) {
const cardClass = writable("");
let answer_html = "";
let cardData: NextCardDataResponse_NextCardData | undefined = undefined;
let startAnswering = Date.now();
export class ReviewerState {
answerHtml: string = ""
cardData: NextCardDataResponse_NextCardData | undefined = undefined;
beginAnsweringMs = Date.now();
readonly cardClass = writable("");
iframe: HTMLIFrameElement | undefined = undefined;
function updateHtml(htmlString) {
iframe.contentWindow?.postMessage({ type: "html", value: htmlString }, "*");
onReady() {
this.iframe?.contentWindow?.postMessage({ type: "nightMode", value: true }, "*");
this.showQuestion(null);
}
async function showQuestion(answer: CardAnswer | null) {
public registerIFrame(iframe: HTMLIFrameElement) {
this.iframe = iframe;
iframe.addEventListener("load", this.onReady.bind(this));
}
updateHtml(htmlString: string) {
this.iframe?.contentWindow?.postMessage({ type: "html", value: htmlString }, "*");
}
async showQuestion(answer: CardAnswer | null) {
const resp = await nextCardData({
answer: answer || undefined,
});
// TODO: "Congratulation screen" logic
const question = resp.nextCard?.front || "";
answer_html = resp.nextCard?.back || "";
cardData = resp.nextCard;
console.log({ resp });
updateHtml(question);
this.updateHtml(question);
}
function showAnswer() {
updateHtml(answer_html);
public showAnswer() {
this.updateHtml(this.cardData?.back || "");
}
function onReady() {
iframe.contentWindow?.postMessage({ type: "nightMode", value: true }, "*");
showQuestion(null);
}
function easeButtonPressed(rating: number) {
const states = cardData!.states!;
public easeButtonPressed(rating: number) {
const states = this.cardData!.states!;
let newState = ({
[1]: states.again!,
@ -49,40 +52,15 @@ export function setupReviewer(iframe: HTMLIFrameElement) {
[4]: states.easy!,
})[rating]!;
showQuestion(
this.showQuestion(
new CardAnswer({
rating: rating,
currentState: states!.current!,
newState,
cardId: cardData!.cardId,
cardId: this.cardData!.cardId,
answeredAtMillis: BigInt(Date.now()),
millisecondsTaken: Date.now() - startAnswering,
millisecondsTaken: Date.now() - this.beginAnsweringMs,
}),
);
}
iframe?.addEventListener("load", onReady);
addEventListener("message", (e) => {
switch (e.data.type) {
case "pycmd": {
const cmd = e.data.value as string;
if (cmd.startsWith("play:")) {
bridgeCommand(e.data.value);
} else {
console.error("pycmd command is either invalid or forbidden:", cmd);
}
break;
}
default: {
console.warn(`Unknown message type: ${e.data.type}`);
break;
}
}
});
globalThis._showQuestion = showQuestion;
globalThis._showAnswer = showAnswer;
return { cardClass, easeButtonPressed };
}