diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 34b350642..d027278c8 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -17,6 +17,7 @@ import "anki/deck_config.proto"; service SchedulerService { rpc GetQueuedCards(GetQueuedCardsRequest) returns (QueuedCards); rpc AnswerCard(CardAnswer) returns (collection.OpChanges); + rpc NextCardData(NextCardDataRequest) returns (NextCardDataResponse); rpc SchedTimingToday(generic.Empty) returns (SchedTimingTodayResponse); rpc StudiedToday(generic.Empty) returns (generic.String); rpc StudiedTodayMessage(StudiedTodayMessageRequest) returns (generic.String); @@ -285,6 +286,21 @@ message CardAnswer { uint32 milliseconds_taken = 6; } +message NextCardDataRequest { + optional CardAnswer answer = 1; +} + +message NextCardDataResponse { + message NextCardData { + string front = 1; + string back = 2; + + SchedulingStates states = 3; + } + + optional NextCardData next_card = 1; +} + message CustomStudyRequest { message Cram { enum CramKind { diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 397b238e1..905e8dff9 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -696,6 +696,7 @@ exposed_backend_list = [ "get_optimal_retention_parameters", "simulate_fsrs_review", "simulate_fsrs_workload", + "next_card_data", # DeckConfigService "get_ignored_before_count", "get_retention_workload", @@ -719,7 +720,6 @@ post_handlers = { for handler in exposed_backend_list } - def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound: if not aqt.mw.col: return NotFound(message=f"collection not open, ignore request for {path}") @@ -766,6 +766,8 @@ def _check_dynamic_request_permissions(): "/_anki/setSchedulingStates", "/_anki/i18nResources", "/_anki/congratsInfo", + # TODO: Unsure about this + "/_anki/nextCardData" ): pass else: diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 9f42a79f7..39cfc95f5 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -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::NextCardData; use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeOptimalRetentionResponse; @@ -14,6 +15,8 @@ use anki_proto::scheduler::FsrsBenchmarkResponse; use anki_proto::scheduler::FuzzDeltaRequest; use anki_proto::scheduler::FuzzDeltaResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; +use anki_proto::scheduler::NextCardDataRequest; +use anki_proto::scheduler::NextCardDataResponse; use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; use anki_proto::scheduler::SimulateFsrsWorkloadResponse; @@ -382,6 +385,30 @@ impl crate::services::SchedulerService for Collection { delta_days: self.get_fuzz_delta(input.card_id.into(), input.interval)?, }) } + + fn next_card_data(&mut self, req: NextCardDataRequest) -> Result { + if let Some(answer) = req.answer { + self.answer_card(&mut answer.into())?; + } + let queue = self.get_queued_cards(1, false)?; + let next_card = queue.cards.first(); + if let Some(next_card) = next_card { + let cid = next_card.card.id; + + let render = self.render_existing_card(cid, false, false)?; + + Ok(NextCardDataResponse { + next_card: Some(NextCardData { + front: render.question().to_string(), + back: render.answer().to_string(), + + states: Some(next_card.states.clone().into()), + }), + }) + } else { + Ok(NextCardDataResponse::default()) + } + } } impl crate::services::BackendSchedulerService for Backend { diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index f80c4e081..7a3fe0e7b 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -1,29 +1,38 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { CardAnswer, SchedulingStates } from "@generated/anki/scheduler_pb"; +import { nextCardData } from "@generated/backend"; import { bridgeCommand } from "@tslib/bridgecommand"; import { writable } from "svelte/store"; -import { preloadAnswerImages } from "../../reviewer/images"; export function setupReviewer(iframe: HTMLIFrameElement) { const cardClass = writable(""); + let answer_html = ""; + let states: SchedulingStates | undefined; function updateHtml(htmlString) { iframe.contentWindow?.postMessage({ type: "html", value: htmlString }, "*"); } - function showQuestion(q, a, cc) { - updateHtml(q); - // html.set(q); - cardClass.set(cc); - preloadAnswerImages(a); + async function showQuestion(answer: CardAnswer | null) { + let resp = await nextCardData({ + answer: answer || undefined, + }); + // TODO: "Congratulation screen" logic + const question = resp.nextCard?.front || ""; + answer_html = resp.nextCard?.back || ""; + states = resp.nextCard?.states; + console.log({ resp }); + updateHtml(question); + } + + function showAnswer() { + updateHtml(answer_html); } function onReady() { - // TODO This should probably be a "ready" command now that it is part of the actual reviewer, - // Currently this depends on the reviewer component mounting after the bottom-reviewer which it should but seems hacky. - // Maybe use a counter with a counter.subscribe($counter == 2 then call("ready")) - bridgeCommand("bottomReady"); iframe.contentWindow?.postMessage({ type: "nightMode", value: true }, "*"); + showQuestion(null); } iframe?.addEventListener("load", onReady); @@ -46,8 +55,8 @@ export function setupReviewer(iframe: HTMLIFrameElement) { } }); - globalThis._showAnswer = updateHtml; globalThis._showQuestion = showQuestion; + globalThis._showAnswer = showAnswer; return { cardClass }; }