diff --git a/qt/aqt/flexible_grading_reviewer/widgets.py b/qt/aqt/flexible_grading_reviewer/widgets.py
index 6be537d32..e1ae933fb 100644
--- a/qt/aqt/flexible_grading_reviewer/widgets.py
+++ b/qt/aqt/flexible_grading_reviewer/widgets.py
@@ -139,3 +139,55 @@ class FlexibleBottomBar(FlexibleHorizontalBar):
self.add_widget(self.middle_bucket)
self.add_stretch()
self.add_widget(self.right_bucket)
+
+
+class FlexibleTimerLabel(QLabel):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._time = 0 # current time (seconds)
+ self._max_time = 0 # maximum time (seconds); 0 means hidden
+ self._qtimer = QTimer(self)
+ self._qtimer.setInterval(1000)
+ qconnect(self._qtimer.timeout, self._on_tick)
+ self.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.setHidden(True)
+
+ def start(self, max_time: int):
+ self._time = 0
+ self._max_time = max_time
+ self._update_display()
+ if self._qtimer.isActive():
+ self._qtimer.stop()
+ self._qtimer.start()
+
+ def stop(self):
+ if self._qtimer.isActive():
+ self._qtimer.stop()
+
+ # Internal tick handler
+ def _on_tick(self):
+ self._time += 1
+ # clamp to max_time if set (mirrors TS: time = Math.min(maxTime, time))
+ if self._time > self._max_time > 0:
+ self._time = self._max_time
+ self._update_display()
+ # if reached max, keep ticking but display in red (TS continues interval)
+
+ def _update_display(self):
+ if self._max_time <= 0:
+ super().setText("") # hide when max_time == 0
+ self.setHidden(True)
+ return
+
+ self.setHidden(False)
+ t = min(self._max_time, self._time) if self._max_time > 0 else self._time
+ m = t // 60
+ s = t % 60
+ s_str = f"{s:02d}"
+ time_string = f"{m}:{s_str}"
+
+ if t >= self._max_time > 0:
+ # display red when time == maxTime (using simple HTML)
+ super().setText(f"{time_string}")
+ else:
+ super().setText(time_string)
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index d6d32a0a6..05c2d4bdf 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -10,7 +10,7 @@ from collections.abc import Generator, Sequence
from dataclasses import dataclass
from enum import Enum, auto
from functools import partial
-from typing import Any, Literal, Match, cast
+from typing import Any, Literal, Match, Optional, cast
import aqt
import aqt.browser
@@ -38,7 +38,7 @@ from aqt.flexible_grading_reviewer.utils import (
ease_to_answer_key_short,
studied_today_count,
)
-from aqt.flexible_grading_reviewer.widgets import FlexiblePushButton
+from aqt.flexible_grading_reviewer.widgets import FlexiblePushButton, FlexibleTimerLabel
from aqt.operations.card import set_card_flag
from aqt.operations.note import remove_notes
from aqt.operations.scheduling import (
@@ -1257,8 +1257,11 @@ class FlexibleReviewer(Reviewer):
QueuedCards.REVIEW: "ForestGreen",
}
+ timer: Optional[FlexibleTimerLabel] = None
+
def __init__(self, mw: AnkiQt) -> None:
super().__init__(mw)
+ self.timer = None
def cleanup(self) -> None:
super().cleanup()
@@ -1282,6 +1285,8 @@ class FlexibleReviewer(Reviewer):
FlexiblePushButton(text=tr.studying_more()),
on_clicked=partial(self.showContextMenu),
)
+ # Right side: add timer
+ self.timer = self.mw.bottomWidget.right_bucket.add_widget(FlexibleTimerLabel())
def browse_queue(self, queue_type: Union[str, Any]) -> None:
if queue_type == QueuedCards.LEARNING:
@@ -1354,11 +1359,24 @@ class FlexibleReviewer(Reviewer):
def _clear_bottom_web(self) -> None:
self.bottom.web.setHtml("")
+ def _max_time(self) -> int:
+ if self.card.should_show_timer():
+ return self.card.time_limit() // 1000
+ else:
+ return 0
+
def _showAnswerButton(self) -> None:
self._add_side_buttons()
self._add_middle_buttons_for_question_side()
self._clear_bottom_web()
+ assert self.timer, "timer should exist."
+ self.timer.start(max_time=self._max_time())
+
+ def _should_stop_timer_on_answer(self) -> bool:
+ conf = self.mw.col.decks.config_dict_for_deck_id(self.card.current_deck_id())
+ return bool(conf["stopTimerOnAnswer"])
+
def _showEaseButtons(self) -> None:
if not self._states_mutated:
self.mw.progress.single_shot(50, self._showEaseButtons)
@@ -1367,6 +1385,10 @@ class FlexibleReviewer(Reviewer):
self._add_middle_buttons_for_answer_side()
self._clear_bottom_web()
+ assert self.timer, "timer should exist."
+ if self._should_stop_timer_on_answer():
+ self.timer.stop()
+
def onEnterKey(self) -> None:
if self.state == "question":
self._getTypedAnswer()