Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-advance options to deck preset #2765

Merged
merged 16 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions ftl/core/deck-config.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ deck-config-maximum-answer-secs-tooltip =
deck-config-show-answer-timer-tooltip =
In the review screen, show a timer that counts the number of seconds you're
taking to review each card.
deck-config-stop-timer-on-answer = Stop timer on answer
deck-config-stop-timer-on-answer-tooltip =
Whether to stop the timer when the answer is revealed.
This doesn't affect statistics.
deck-config-seconds-to-show-question = Seconds to show question
deck-config-seconds-to-show-question-tooltip = The number of seconds to wait before automatically advancing to the next question. Set to 0 to disable.
deck-config-seconds-to-show-answer = Seconds to show answer
deck-config-seconds-to-show-answer-tooltip = The number of seconds to wait before automatically revealing the answer. Set to 0 to disable.
deck-config-answer-action = Answer action
deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one.
deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question

## Audio section

Expand All @@ -234,11 +245,6 @@ deck-config-skip-question-when-replaying = Skip question when replaying answer
deck-config-always-include-question-audio-tooltip =
Whether the question audio should be included when the Replay action is
used while looking at the answer side of a card.
deck-config-stop-timer-on-answer = Stop timer on answer
deck-config-stop-timer-on-answer-tooltip =
Whether to stop the timer when the answer is revealed.
This doesn't affect statistics.

## Advanced section

deck-config-advanced-title = Advanced
Expand Down
1 change: 1 addition & 0 deletions ftl/core/studying.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ studying-minute =
[one] { $count } minute.
*[other] { $count } minutes.
}
studying-answer-time-elapsed = Answer time elapsed
11 changes: 11 additions & 0 deletions proto/anki/deck_config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ message DeckConfig {
LEECH_ACTION_SUSPEND = 0;
LEECH_ACTION_TAG_ONLY = 1;
}
enum AnswerAction {
ANSWER_ACTION_BURY_CARD = 0;
ANSWER_ACTION_ANSWER_AGAIN = 1;
ANSWER_ACTION_ANSWER_GOOD = 2;
ANSWER_ACTION_ANSWER_HARD = 3;
ANSWER_ACTION_SHOW_REMINDER = 4;
}

repeated float learn_steps = 1;
repeated float relearn_steps = 2;
Expand Down Expand Up @@ -133,6 +140,10 @@ message DeckConfig {
uint32 cap_answer_time_to_secs = 24;
bool show_timer = 25;
bool stop_timer_on_answer = 38;
float seconds_to_show_question = 41;
float seconds_to_show_answer = 42;
AnswerAction answer_action = 43;
bool wait_for_audio = 44;
bool skip_question_when_replaying_answer = 26;

bool bury_new = 27;
Expand Down
9 changes: 9 additions & 0 deletions qt/aqt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ def eventFilter(self, obj, evt):
self.mw.bottomWeb.hide_timer.start()
return True

if evt.type() == QEvent.Type.FocusOut:
self.mw._auto_advance_was_enabled = self.mw.reviewer.auto_advance_enabled
self.mw.reviewer.auto_advance_enabled = False
return True

return False


Expand Down Expand Up @@ -189,6 +194,7 @@ def __init__(
self.app = app
self.pm = profileManager
self.fullscreen = False
self._auto_advance_was_enabled = False
# init rest of app
self.safeMode = (
bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier)
Expand Down Expand Up @@ -822,6 +828,8 @@ def on_focus_did_change(
if new_focus and new_focus.window() == self:
if self.state == "review":
self.reviewer.refresh_if_needed()
self.reviewer.auto_advance_enabled = self._auto_advance_was_enabled
self.reviewer.auto_advance_if_enabled()
elif self.state == "overview":
self.overview.refresh_if_needed()
elif self.state == "deckBrowser":
Expand Down Expand Up @@ -1021,6 +1029,7 @@ def setupReviewer(self) -> None:
from aqt.reviewer import Reviewer

self.reviewer = Reviewer(self)
self._auto_advance_was_enabled = self.reviewer.auto_advance_enabled

# Syncing
##########################################################################
Expand Down
116 changes: 116 additions & 0 deletions qt/aqt/reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ def rating_from_ease(ease: int) -> CardAnswer.Rating.V:
return CardAnswer.EASY


class AnswerAction(Enum):
BURY_CARD = 0
ANSWER_AGAIN = 1
ANSWER_GOOD = 2
ANSWER_HARD = 3
SHOW_REMINDER = 4


class Reviewer:
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
Expand All @@ -147,6 +155,10 @@ def __init__(self, mw: AnkiQt) -> None:
self._previous_card_info = PreviousReviewerCardInfo(self.mw)
self._states_mutated = True
self._reps: int = None
self._show_question_timer: QTimer | None = None
self._show_answer_timer: QTimer | None = None
self.auto_advance_enabled = False
gui_hooks.av_player_did_end_playing.append(self._on_av_player_did_end_playing)

def show(self) -> None:
if self.mw.col.sched_ver() == 1 or not self.mw.col.v3_scheduler():
Expand Down Expand Up @@ -175,6 +187,7 @@ def lastCard(self) -> Card | None:
def cleanup(self) -> None:
gui_hooks.reviewer_will_end()
self.card = None
self.auto_advance_enabled = False

def refresh_if_needed(self) -> None:
if self._refresh_needed is RefreshNeeded.QUEUES:
Expand Down Expand Up @@ -282,6 +295,21 @@ def replayAudio(self) -> None:
replay_audio(self.card, False)
gui_hooks.audio_will_replay(self.web, self.card, self.state == "question")

def _on_av_player_did_end_playing(self, *args) -> None:
def task() -> None:
if av_player.queue_is_empty():
if self._show_question_timer and not sip.isdeleted(
self._show_question_timer
):
self._on_show_question_timeout()
elif self._show_answer_timer and not sip.isdeleted(
self._show_answer_timer
):
self._on_show_answer_timeout()

# Allow time for audio queue to update
self.mw.taskman.run_on_main(lambda: self.mw.progress.single_shot(100, task))

# Initializing the webview
##########################################################################

Expand Down Expand Up @@ -363,6 +391,35 @@ def _showQuestion(self) -> None:
self.mw.web.setFocus()
# user hook
gui_hooks.reviewer_did_show_question(c)
self._auto_advance_to_answer_if_enabled()

def _auto_advance_to_answer_if_enabled(self) -> None:
if self.auto_advance_enabled:
conf = self.mw.col.decks.config_dict_for_deck_id(
self.card.current_deck_id()
)
timer = None
if conf["secondsToShowAnswer"]:
timer = self._show_answer_timer = self.mw.progress.timer(
int(conf["secondsToShowAnswer"] * 1000),
lambda: self._on_show_answer_timeout(timer),
repeat=False,
parent=self.mw,
)

def _on_show_answer_timeout(self, timer: QTimer | None = None) -> None:
if self.card is None:
return
conf = self.mw.col.decks.config_dict_for_deck_id(self.card.current_deck_id())
if (conf["waitForAudio"] and av_player.current_player) or (
timer and self._show_answer_timer != timer
):
return
if self._show_answer_timer is not None:
self._show_answer_timer.deleteLater()
if not self.auto_advance_enabled:
return
self._showAnswer()

def autoplay(self, card: Card) -> bool:
print("use card.autoplay() instead of reviewer.autoplay(card)")
Expand Down Expand Up @@ -404,6 +461,48 @@ def _showAnswer(self) -> None:
self.mw.web.setFocus()
# user hook
gui_hooks.reviewer_did_show_answer(c)
self._auto_advance_to_question_if_enabled()

def _auto_advance_to_question_if_enabled(self) -> None:
if self.auto_advance_enabled:
conf = self.mw.col.decks.config_dict_for_deck_id(
self.card.current_deck_id()
)
timer = None
if conf["secondsToShowQuestion"]:
timer = self._show_question_timer = self.mw.progress.timer(
int(conf["secondsToShowQuestion"] * 1000),
lambda: self._on_show_question_timeout(timer),
repeat=False,
parent=self.mw,
)

def _on_show_question_timeout(self, timer: QTimer | None = None) -> None:
if self.card is None:
return
conf = self.mw.col.decks.config_dict_for_deck_id(self.card.current_deck_id())
if (conf["waitForAudio"] and av_player.current_player) or (
timer and self._show_question_timer != timer
):
return
if self._show_question_timer is not None:
self._show_question_timer.deleteLater()
if not self.auto_advance_enabled:
return
try:
answer_action = list(AnswerAction)[conf["answerAction"]]
except IndexError:
answer_action = AnswerAction.ANSWER_GOOD
if answer_action == AnswerAction.BURY_CARD:
self.bury_current_card()
elif answer_action == AnswerAction.ANSWER_AGAIN:
self._answerCard(1)
elif answer_action == AnswerAction.ANSWER_HARD:
self._answerCard(2)
elif answer_action == AnswerAction.SHOW_REMINDER:
tooltip(tr.studying_answer_time_elapsed())
else:
self._answerCard(3)

# Answering a card
############################################################
Expand Down Expand Up @@ -507,6 +606,7 @@ def _shortcutKeys(
("5", self.on_pause_audio),
("6", self.on_seek_backward),
("7", self.on_seek_forward),
("Shift+A", self.toggle_auto_advance),
*self.korean_shortcuts(),
]

Expand Down Expand Up @@ -883,6 +983,12 @@ def _contextMenu(self) -> list[Any]:
[tr.studying_audio_and5s(), "7", self.on_seek_forward],
[tr.studying_record_own_voice(), "Shift+V", self.onRecordVoice],
[tr.studying_replay_own_voice(), "V", self.onReplayRecorded],
[
tr.actions_auto_advance(),
"Shift+A",
self.toggle_auto_advance,
dict(checked=self.auto_advance_enabled),
],
]
return opts

Expand Down Expand Up @@ -1039,6 +1145,16 @@ def onReplayRecorded(self) -> None:
return
av_player.play_file(self._recordedAudio)

def toggle_auto_advance(self) -> None:
self.auto_advance_enabled = not self.auto_advance_enabled
self.auto_advance_if_enabled()

def auto_advance_if_enabled(self) -> None:
if self.state == "question":
self._auto_advance_to_answer_if_enabled()
elif self.state == "answer":
self._auto_advance_to_question_if_enabled()

# legacy

onBuryCard = bury_current_card
Expand Down
2 changes: 1 addition & 1 deletion qt/aqt/sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def append_tags(self, tags: list[AVTag]) -> None:
self._play_next_if_idle()

def queue_is_empty(self) -> bool:
return bool(self._enqueued)
return not bool(self._enqueued)

def stop_and_clear_queue(self) -> None:
self._enqueued = []
Expand Down
5 changes: 5 additions & 0 deletions rslib/src/deckconfig/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod service;
pub(crate) mod undo;
mod update;

pub use anki_proto::deck_config::deck_config::config::AnswerAction;
pub use anki_proto::deck_config::deck_config::config::LeechAction;
pub use anki_proto::deck_config::deck_config::config::NewCardGatherPriority;
pub use anki_proto::deck_config::deck_config::config::NewCardInsertOrder;
Expand Down Expand Up @@ -63,6 +64,10 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
cap_answer_time_to_secs: 60,
show_timer: false,
stop_timer_on_answer: false,
seconds_to_show_question: 0.0,
seconds_to_show_answer: 0.0,
answer_action: AnswerAction::BuryCard as i32,
wait_for_audio: true,
skip_question_when_replaying_answer: false,
bury_new: false,
bury_reviews: false,
Expand Down
46 changes: 46 additions & 0 deletions rslib/src/deckconfig/schema11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ use crate::serde::default_on_invalid;
use crate::timestamp::TimestampSecs;
use crate::types::Usn;

fn wait_for_audio_default() -> bool {
true
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeckConfSchema11 {
Expand Down Expand Up @@ -72,6 +76,14 @@ pub struct DeckConfSchema11 {
#[serde(default)]
stop_timer_on_answer: bool,
#[serde(default)]
seconds_to_show_question: f32,
#[serde(default)]
seconds_to_show_answer: f32,
#[serde(default)]
answer_action: AnswerAction,
#[serde(default = "wait_for_audio_default")]
wait_for_audio: bool,
#[serde(default)]
reschedule_fsrs_cards: bool,
#[serde(default)]
sm2_retention: f32,
Expand All @@ -80,6 +92,18 @@ pub struct DeckConfSchema11 {
other: HashMap<String, Value>,
}

#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
#[derive(Default)]
pub enum AnswerAction {
#[default]
BuryCard = 0,
AnswerAgain = 1,
AnswerGood = 2,
AnswerHard = 3,
ShowReminder = 4,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NewConfSchema11 {
Expand Down Expand Up @@ -249,6 +273,10 @@ impl Default for DeckConfSchema11 {
autoplay: true,
timer: 0,
stop_timer_on_answer: false,
seconds_to_show_question: 0.0,
seconds_to_show_answer: 0.0,
answer_action: AnswerAction::BuryCard,
wait_for_audio: true,
replayq: true,
dynamic: false,
new: Default::default(),
Expand Down Expand Up @@ -331,6 +359,10 @@ impl From<DeckConfSchema11> for DeckConfig {
cap_answer_time_to_secs: c.max_taken.max(0) as u32,
show_timer: c.timer != 0,
stop_timer_on_answer: c.stop_timer_on_answer,
seconds_to_show_question: c.seconds_to_show_question,
seconds_to_show_answer: c.seconds_to_show_answer,
answer_action: c.answer_action as i32,
wait_for_audio: c.wait_for_audio,
skip_question_when_replaying_answer: !c.replayq,
bury_new: c.new.bury,
bury_reviews: c.rev.bury,
Expand Down Expand Up @@ -385,6 +417,16 @@ impl From<DeckConfig> for DeckConfSchema11 {
autoplay: !i.disable_autoplay,
timer: i.show_timer.into(),
stop_timer_on_answer: i.stop_timer_on_answer,
seconds_to_show_question: i.seconds_to_show_question,
seconds_to_show_answer: i.seconds_to_show_answer,
answer_action: match i.answer_action {
0 => AnswerAction::BuryCard,
1 => AnswerAction::AnswerAgain,
3 => AnswerAction::AnswerHard,
4 => AnswerAction::ShowReminder,
_ => AnswerAction::AnswerGood,
},
wait_for_audio: i.wait_for_audio,
replayq: !i.skip_question_when_replaying_answer,
dynamic: false,
new: NewConfSchema11 {
Expand Down Expand Up @@ -459,6 +501,10 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"fsrsWeights",
"desiredRetention",
"stopTimerOnAnswer",
"secondsToShowQuestion",
"secondsToShowAnswer",
"answerAction",
"waitForAudio",
"rescheduleFsrsCards",
"sm2Retention",
};
Expand Down
Loading