diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 418920ec7..9b489a037 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -302,6 +302,11 @@ message NextCardDataResponse { string args = 2; } + message TimerPreferences { + uint32 max_time_ms = 1; + bool stop_on_answer = 2; + } + message NextCardData { QueuedCards queue = 1; repeated AnswerButton answer_buttons = 2; @@ -313,7 +318,7 @@ message NextCardDataResponse { bool autoplay = 7; bool marked = 13; optional TypedAnswer typed_answer = 12; - uint32 max_time_ms = 14; + optional TimerPreferences timer = 14; repeated card_rendering.AVTag question_av_tags = 8; repeated card_rendering.AVTag answer_av_tags = 9; diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 74525549a..470e3d332 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -11,6 +11,7 @@ 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::next_card_data_response::TimerPreferences; use anki_proto::scheduler::next_card_data_response::TypedAnswer; use anki_proto::scheduler::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; @@ -403,7 +404,7 @@ impl crate::services::SchedulerService for Collection { let next_card = queue.cards.first(); if let Some(next_card) = next_card { let cid = next_card.card.id; - let deck_config = self.deck_config_for_card(&next_card.card)?; + let deck_config = self.deck_config_for_card(&next_card.card)?.inner; let note = self.get_note(next_card.card.note_id.into())?; let render = self.render_existing_card(cid, false, true)?; @@ -477,6 +478,11 @@ impl crate::services::SchedulerService for Collection { queue.new_count = 0; } + let timer = deck_config.show_timer.then_some(TimerPreferences { + max_time_ms: deck_config.cap_answer_time_to_secs * 1000, + stop_on_answer: deck_config.stop_timer_on_answer, + }); + Ok(NextCardDataResponse { next_card: Some(NextCardData { queue: Some(queue.into()), @@ -486,13 +492,13 @@ impl crate::services::SchedulerService for Collection { partial_back: rendered_nodes_to_proto(render.anodes), answer_buttons, - autoplay: !deck_config.inner.disable_autoplay, + autoplay: !deck_config.disable_autoplay, typed_answer: typed_answer.map(|answer| TypedAnswer { text: answer.1, args: answer.0, }), marked, - max_time_ms: deck_config.inner.cap_answer_time_to_secs * 1000, + timer, // Filled by python front: "".to_string(), diff --git a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte index 50c54fe75..6f8e0a55a 100644 --- a/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte +++ b/ts/routes/reviewer/reviewer-bottom/ReviewerBottom.svelte @@ -52,7 +52,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if}
- + {#if $cardData?.timer} + + {/if}
@@ -82,7 +84,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .more { - // text-align: right; direction: rtl; } diff --git a/ts/routes/reviewer/reviewer-bottom/Timer.svelte b/ts/routes/reviewer/reviewer-bottom/Timer.svelte index f9ea65819..1ca7aae44 100644 --- a/ts/routes/reviewer/reviewer-bottom/Timer.svelte +++ b/ts/routes/reviewer/reviewer-bottom/Timer.svelte @@ -12,14 +12,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let cls = ""; function step() { - let time = Date.now() - state.beginAnsweringMs; - const maxTime = state._cardData?.maxTimeMs ?? 0; + const timerPreferences = state._cardData?.timer; + let time = Date.now(); + if (timerPreferences?.stopOnAnswer && state.answerMs !== undefined) { + time = state.answerMs; + } + time -= state.beginAnsweringMs; + const maxTime = state._cardData?.timer?.maxTimeMs ?? 0; if (time >= maxTime) { time = maxTime; cls = "overtime"; } else { cls = ""; } + text = formatTime(time); } @@ -37,7 +43,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html onDestroy(() => { clearInterval(interval); }); - step(); function formatTime(time: number) { const seconds = time / 1000; diff --git a/ts/routes/reviewer/reviewer.ts b/ts/routes/reviewer/reviewer.ts index eb53e71ca..fcf57662b 100644 --- a/ts/routes/reviewer/reviewer.ts +++ b/ts/routes/reviewer/reviewer.ts @@ -53,6 +53,7 @@ export class ReviewerState { currentTypedAnswer = ""; _cardData: NextCardDataResponse_NextCardData | undefined = undefined; beginAnsweringMs = Date.now(); + answerMs: number | undefined = undefined; readonly cardClass = writable(""); readonly answerShown = writable(false); readonly cardData = writable(undefined); @@ -310,6 +311,7 @@ export class ReviewerState { } this.beginAnsweringMs = Date.now(); + this.answerMs = undefined; } get currentCard() { @@ -338,6 +340,7 @@ export class ReviewerState { if (this._cardData?.autoplay) { playAvtags({ tags: this._cardData!.answerAvTags }); } + this.answerMs = Date.now(); this.updateHtml(await this.showTypedAnswer(this._cardData?.back || "")); }