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 NextCardDataResponse {
message AnswerButton {
CardAnswer.Rating rating = 1;
string due = 2;
}
message NextCardData { message NextCardData {
int64 card_id = 1; int64 card_id = 1;
string front = 2; string front = 2;
string back = 3; string back = 3;
SchedulingStates states = 4; SchedulingStates states = 4;
repeated AnswerButton answer_buttons = 5;
} }
optional NextCardData next_card = 1; optional NextCardData next_card = 1;

View file

@ -7,6 +7,7 @@ mod states;
use anki_proto::cards; use anki_proto::cards;
use anki_proto::generic; use anki_proto::generic;
use anki_proto::scheduler; 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::next_card_data_response::NextCardData;
use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeFsrsParamsResponse;
use anki_proto::scheduler::ComputeMemoryStateResponse; 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 render = self.render_existing_card(cid, false, false)?;
let style = format!("<style>{}</style>", render.css); 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 { Ok(NextCardDataResponse {
next_card: Some(NextCardData { next_card: Some(NextCardData {
card_id: cid.0, card_id: cid.0,
@ -405,6 +416,7 @@ impl crate::services::SchedulerService for Collection {
back: [style, render.answer().to_string()].concat(), back: [style, render.answer().to_string()].concat(),
states: Some(next_card.states.clone().into()), states: Some(next_card.states.clone().into()),
answer_buttons,
}), }),
}) })
} else { } 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 ReviewerBottom from "./reviewer-bottom/ReviewerBottom.svelte";
import Reviewer from "./Reviewer.svelte"; import Reviewer from "./Reviewer.svelte";
let state = new ReviewerState const state = new ReviewerState();
</script> </script>
<div> <div>

View file

@ -5,12 +5,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts"> <script lang="ts">
import type { ReviewerState } from "./reviewer"; import type { ReviewerState } from "./reviewer";
let iframe: HTMLIFrameElement; let iframe: HTMLIFrameElement;
export let state: ReviewerState export let state: ReviewerState;
$: if (iframe) state.registerIFrame(iframe)
$: if (iframe) {
state.registerIFrame(iframe);
}
</script> </script>
<div id="qa"> <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 License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import { bridgeCommand } from "@tslib/bridgecommand"; import type { NextCardDataResponse_AnswerButton } from "@generated/anki/scheduler_pb";
import type { AnswerButtonInfo } from "./types"; 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> </script>
<span> <span>
@ -16,8 +22,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
&nbsp; &nbsp;
{/if} {/if}
</span> </span>
<button on:click={() => bridgeCommand(`ease${info.i}`)}> <button on:click={() => state.easeButtonPressed(info.rating)}>
{info.label} {label}
</button> </button>
<style> <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 License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import "./index.scss";
import AnswerButton from "./AnswerButton.svelte"; import AnswerButton from "./AnswerButton.svelte";
import { bridgeCommand } from "@tslib/bridgecommand"; import { bridgeCommand } from "@tslib/bridgecommand";
import * as tr from "@generated/ftl"; 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 type { ReviewerState } from "../reviewer";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export let state: ReviewerState export let state: ReviewerState;
const answerButtons = state.answerButtons;
const answerShown = state.answerShown;
// Placeholders // Placeholders
let answerButtons = writable([]); const remaining = writable([0, 0, 0]);
let remaining = writable([0, 0, 0]); const remainingIndex = writable(0);
let remainingIndex = writable(0);
$: button_count = $answerShown ? $answerButtons.length : 1;
$: answerShown = $answerButtons.length;
</script> </script>
<div id="outer" class="fancy"> <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> <span class="disappearing"></span>
<div class="disappearing edit"> <div class="disappearing edit">
<button <button
@ -31,9 +35,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{tr.studyingEdit()} {tr.studyingEdit()}
</button> </button>
</div> </div>
{#if answerShown} {#if $answerShown}
{#each $answerButtons as answerButton} {#each $answerButtons as answerButton}
<AnswerButton info={answerButton}></AnswerButton> <AnswerButton {state} info={answerButton}></AnswerButton>
{/each} {/each}
{:else} {:else}
<span class="remaining-count"> <span class="remaining-count">

View file

@ -1,18 +1,17 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { import { CardAnswer, NextCardDataResponse_AnswerButton, type NextCardDataResponse_NextCardData } from "@generated/anki/scheduler_pb";
CardAnswer,
type NextCardDataResponse_NextCardData,
} from "@generated/anki/scheduler_pb";
import { nextCardData } from "@generated/backend"; import { nextCardData } from "@generated/backend";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export class ReviewerState { export class ReviewerState {
answerHtml: string = "" answerHtml = "";
cardData: NextCardDataResponse_NextCardData | undefined = undefined; cardData: NextCardDataResponse_NextCardData | undefined = undefined;
beginAnsweringMs = Date.now(); beginAnsweringMs = Date.now();
readonly cardClass = writable(""); readonly cardClass = writable("");
readonly answerButtons = writable<NextCardDataResponse_AnswerButton[]>([]);
readonly answerShown = writable(false)
iframe: HTMLIFrameElement | undefined = undefined; iframe: HTMLIFrameElement | undefined = undefined;
onReady() { onReady() {
@ -28,25 +27,28 @@ export class ReviewerState {
updateHtml(htmlString: string) { updateHtml(htmlString: string) {
this.iframe?.contentWindow?.postMessage({ type: "html", value: htmlString }, "*"); this.iframe?.contentWindow?.postMessage({ type: "html", value: htmlString }, "*");
} }
async showQuestion(answer: CardAnswer | null) { async showQuestion(answer: CardAnswer | null) {
const resp = await nextCardData({ const resp = await nextCardData({
answer: answer || undefined, answer: answer || undefined,
}); });
// TODO: "Congratulation screen" logic // TODO: "Congratulation screen" logic
this.cardData = resp.nextCard this.cardData = resp.nextCard;
this.answerButtons.set(this.cardData?.answerButtons ?? []);
const question = resp.nextCard?.front || ""; const question = resp.nextCard?.front || "";
this.answerShown.set(false);
this.updateHtml(question); this.updateHtml(question);
} }
public showAnswer() { public showAnswer() {
this.answerShown.set(true);
this.updateHtml(this.cardData?.back || ""); this.updateHtml(this.cardData?.back || "");
} }
public easeButtonPressed(rating: number) { public easeButtonPressed(rating: number) {
const states = this.cardData!.states!; const states = this.cardData!.states!;
let newState = ({ const newState = ({
[1]: states.again!, [1]: states.again!,
[2]: states.hard!, [2]: states.hard!,
[3]: states.good!, [3]: states.good!,