Re-implement answer buttons

This commit is contained in:
Luc Mcgrady 2025-10-04 20:50:11 +01:00
parent 85ebdefec0
commit 953e6c9360
No known key found for this signature in database
GPG key ID: 4F3D7A0B17CC3D9C
7 changed files with 57 additions and 27 deletions

View file

@ -291,12 +291,18 @@ message NextCardDataRequest {
}
message NextCardDataResponse {
message AnswerButton {
CardAnswer.Rating rating = 1;
string due = 2;
}
message NextCardData {
int64 card_id = 1;
string front = 2;
string back = 3;
SchedulingStates states = 4;
repeated AnswerButton answer_buttons = 5;
}
optional NextCardData next_card = 1;

View file

@ -7,6 +7,7 @@ mod states;
use anki_proto::cards;
use anki_proto::generic;
use anki_proto::scheduler;
use anki_proto::scheduler::next_card_data_response::AnswerButton;
use anki_proto::scheduler::next_card_data_response::NextCardData;
use anki_proto::scheduler::ComputeFsrsParamsResponse;
use anki_proto::scheduler::ComputeMemoryStateResponse;
@ -398,6 +399,16 @@ impl crate::services::SchedulerService for Collection {
let render = self.render_existing_card(cid, false, false)?;
let style = format!("<style>{}</style>", render.css);
let answer_buttons = self
.describe_next_states(&next_card.states)?
.into_iter()
.enumerate()
.map(|(i, due)| AnswerButton {
rating: i as i32,
due,
})
.collect();
Ok(NextCardDataResponse {
next_card: Some(NextCardData {
card_id: cid.0,
@ -405,6 +416,7 @@ impl crate::services::SchedulerService for Collection {
back: [style, render.answer().to_string()].concat(),
states: Some(next_card.states.clone().into()),
answer_buttons,
}),
})
} else {

View file

@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ReviewerBottom from "./reviewer-bottom/ReviewerBottom.svelte";
import Reviewer from "./Reviewer.svelte";
let state = new ReviewerState
const state = new ReviewerState();
</script>
<div>

View file

@ -5,12 +5,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import type { ReviewerState } from "./reviewer";
let iframe: HTMLIFrameElement;
export let state: ReviewerState
$: if (iframe) state.registerIFrame(iframe)
export let state: ReviewerState;
$: if (iframe) {
state.registerIFrame(iframe);
}
</script>
<div id="qa">

View file

@ -3,10 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { bridgeCommand } from "@tslib/bridgecommand";
import type { AnswerButtonInfo } from "./types";
import type { NextCardDataResponse_AnswerButton } from "@generated/anki/scheduler_pb";
import * as tr from "@generated/ftl";
import type { ReviewerState } from "../reviewer";
export let info: NextCardDataResponse_AnswerButton;
export let state: ReviewerState;
const labels = [tr.studyingAgain(), tr.studyingHard(), tr.studyingGood(), tr.studyingEasy()]
$: label = labels[info.rating];
export let info: AnswerButtonInfo;
</script>
<span>
@ -16,8 +22,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
&nbsp;
{/if}
</span>
<button on:click={() => bridgeCommand(`ease${info.i}`)}>
{info.label}
<button on:click={() => state.easeButtonPressed(info.rating)}>
{label}
</button>
<style>

View file

@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import "./index.scss";
import AnswerButton from "./AnswerButton.svelte";
import { bridgeCommand } from "@tslib/bridgecommand";
import * as tr from "@generated/ftl";
@ -10,18 +12,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { ReviewerState } from "../reviewer";
import { writable } from "svelte/store";
export let state: ReviewerState
export let state: ReviewerState;
const answerButtons = state.answerButtons;
const answerShown = state.answerShown;
// Placeholders
let answerButtons = writable([]);
let remaining = writable([0, 0, 0]);
let remainingIndex = writable(0);
const remaining = writable([0, 0, 0]);
const remainingIndex = writable(0);
$: button_count = $answerShown ? $answerButtons.length : 1;
$: answerShown = $answerButtons.length;
</script>
<div id="outer" class="fancy">
<div id="tableinner" style="--answer-button-count: {$answerButtons.length || 1}">
<div id="tableinner" style="--answer-button-count: {button_count}">
<span class="disappearing"></span>
<div class="disappearing edit">
<button
@ -31,9 +35,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{tr.studyingEdit()}
</button>
</div>
{#if answerShown}
{#if $answerShown}
{#each $answerButtons as answerButton}
<AnswerButton info={answerButton}></AnswerButton>
<AnswerButton {state} info={answerButton}></AnswerButton>
{/each}
{:else}
<span class="remaining-count">

View file

@ -1,18 +1,17 @@
// 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 { CardAnswer, NextCardDataResponse_AnswerButton, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb";
import { nextCardData } from "@generated/backend";
import { writable } from "svelte/store";
export class ReviewerState {
answerHtml: string = ""
answerHtml = "";
cardData: NextCardDataResponse_NextCardData | undefined = undefined;
beginAnsweringMs = Date.now();
readonly cardClass = writable("");
readonly answerButtons = writable<NextCardDataResponse_AnswerButton[]>([]);
readonly answerShown = writable(false)
iframe: HTMLIFrameElement | undefined = undefined;
onReady() {
@ -28,25 +27,28 @@ export class ReviewerState {
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
this.cardData = resp.nextCard
this.cardData = resp.nextCard;
this.answerButtons.set(this.cardData?.answerButtons ?? []);
const question = resp.nextCard?.front || "";
this.answerShown.set(false);
this.updateHtml(question);
}
public showAnswer() {
this.answerShown.set(true);
this.updateHtml(this.cardData?.back || "");
}
public easeButtonPressed(rating: number) {
const states = this.cardData!.states!;
let newState = ({
const newState = ({
[1]: states.again!,
[2]: states.hard!,
[3]: states.good!,