Skip to content

Commit

Permalink
feat(new reviewer): auto advance
Browse files Browse the repository at this point in the history
This differs from the legacy reviewer in four things:

1. It implements `Show reminder` as an action (it is a tooltip in Anki, so I made it a snackbar here)
2. It doesn't follow the global switch in settings, as Anki desktop doesn't have one and users complained about finding the global setting unexpected
3. There is now a `Question action` setting
4. The legacy reviewer has an issue where the same configuration is carried through all cards in the review session. So if a card with a different deck config shows up, the config won't be respected. This is fixed in the new reviewer
  • Loading branch information
BrayanDSO committed Jun 22, 2024
1 parent 8b196f6 commit d3be563
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ class ReviewerFragment :
anchorView = this@ReviewerFragment.view?.findViewById(R.id.buttons_area)
}

override fun onStop() {
super.onStop()
if (!requireActivity().isChangingConfigurations) {
viewModel.stopAutoAdvance()
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.ichi2.anki.servicelayer.MARKED_TAG
import com.ichi2.anki.servicelayer.NoteService
import com.ichi2.anki.servicelayer.isBuryNoteAvailable
import com.ichi2.anki.servicelayer.isSuspendNoteAvailable
import com.ichi2.anki.ui.windows.reviewer.autoadvance.AutoAdvance
import com.ichi2.libanki.ChangeManager
import com.ichi2.libanki.hasTag
import com.ichi2.libanki.note
Expand Down Expand Up @@ -78,6 +79,8 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :
private val stateMutationKey = TimeManager.time.intTimeMS().toString()
val statesMutationEval = MutableSharedFlow<String>()

private val autoAdvance = AutoAdvance(this)

/**
* A flag that determines if the SchedulingStates in CurrentQueueState are
* safe to persist in the database when answering a card. This is used to
Expand All @@ -98,6 +101,17 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :
launchCatchingIO {
updateUndoAndRedoLabels()
}
cardMediaPlayer.setOnSoundGroupCompletedListener {
launchCatchingIO {
if (!autoAdvance.shouldWaitForAudio()) return@launchCatchingIO

if (showingAnswer.value) {
autoAdvance.onShowAnswer()
} else {
autoAdvance.onShowQuestion()
}
}
}
}

/* *********************************************************************************************
Expand All @@ -124,6 +138,9 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :
}
showAnswerInternal()
loadAndPlaySounds(CardSide.ANSWER)
if (!autoAdvance.shouldWaitForAudio()) {
autoAdvance.onShowAnswer()
} // else wait for onSoundGroupCompleted
}
}

Expand Down Expand Up @@ -266,6 +283,10 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :
}
}

fun stopAutoAdvance() {
autoAdvance.cancelQuestionAndAnswerActionJobs()
}

/* *********************************************************************************************
*************************************** Internal methods ***************************************
********************************************************************************************* */
Expand All @@ -285,6 +306,9 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :
override suspend fun showQuestion() {
super.showQuestion()
runStateMutationHook()
if (!autoAdvance.shouldWaitForAudio()) {
autoAdvance.onShowQuestion()
} // else run in onSoundGroupCompleted
}

private suspend fun runStateMutationHook() {
Expand Down Expand Up @@ -356,6 +380,7 @@ class ReviewerViewModel(cardMediaPlayer: CardMediaPlayer) :

val card = state.topCard
currentCard = CompletableDeferred(card)
autoAdvance.onCardChange(card)
showQuestion()
loadAndPlaySounds(CardSide.QUESTION)
updateMarkedStatus()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.ui.windows.reviewer.autoadvance

import com.ichi2.libanki.DeckConfig

enum class AnswerAction(val configValue: Int) {
BURY_CARD(0),
ANSWER_AGAIN(1),
ANSWER_GOOD(2),
ANSWER_HARD(3),
SHOW_REMINDER(4);

companion object {
fun from(config: DeckConfig): AnswerAction {
val value = config.optInt("answerAction")
return entries.firstOrNull { it.configValue == value } ?: BURY_CARD
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.ui.windows.reviewer.autoadvance

import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.asyncIO
import com.ichi2.anki.launchCatchingIO
import com.ichi2.anki.ui.windows.reviewer.ReviewerViewModel
import com.ichi2.libanki.Card
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay

/**
* Implementation of the `Auto Advance` deck options
*
* A timer (in seconds) can be set to automatically trigger an action after it runs out,
* either in the question side ([QuestionAction]) or in the answer side ([AnswerAction]).
*
* If a timer is set to 0, the corresponding action is not triggered.
*
* @see AutoAdvanceSettings
*/
class AutoAdvance(val viewModel: ReviewerViewModel) {
private var questionActionJob: Job? = null
private var answerActionJob: Job? = null

private var settings = viewModel.asyncIO {
val card = viewModel.currentCard.await()
AutoAdvanceSettings.createInstance(card.currentDeckId().did)
}

private suspend fun durationToShowQuestionFor() = settings.await().durationToShowQuestionFor
private suspend fun durationToShowAnswerFor() = settings.await().durationToShowAnswerFor
private suspend fun questionAction() = settings.await().questionAction
private suspend fun answerAction() = settings.await().answerAction
suspend fun shouldWaitForAudio() = settings.await().waitForAudio

fun cancelQuestionAndAnswerActionJobs() {
questionActionJob?.cancel()
answerActionJob?.cancel()
}

fun onCardChange(card: Card) {
cancelQuestionAndAnswerActionJobs()
settings = viewModel.asyncIO {
AutoAdvanceSettings.createInstance(card.currentDeckId().did)
}
}

suspend fun onShowQuestion() {
answerActionJob?.cancel()
if (!durationToShowQuestionFor().isPositive()) return

questionActionJob = viewModel.launchCatchingIO {
delay(durationToShowQuestionFor())
when (questionAction()) {
QuestionAction.SHOW_ANSWER -> viewModel.showAnswer()
QuestionAction.SHOW_REMINDER -> showReminder(TR.studyingQuestionTimeElapsed())
}
}
}

suspend fun onShowAnswer() {
questionActionJob?.cancel()
if (!durationToShowAnswerFor().isPositive()) return

answerActionJob = viewModel.launchCatchingIO {
delay(durationToShowAnswerFor())
when (answerAction()) {
AnswerAction.BURY_CARD -> viewModel.buryCard()
AnswerAction.ANSWER_AGAIN -> viewModel.answerAgain()
AnswerAction.ANSWER_HARD -> viewModel.answerHard()
AnswerAction.ANSWER_GOOD -> viewModel.answerGood()
AnswerAction.SHOW_REMINDER -> showReminder(TR.studyingAnswerTimeElapsed())
}
}
}

private fun showReminder(message: String) {
viewModel.launchCatchingIO {
viewModel.actionFeedbackFlow.emit(message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.ui.windows.reviewer.autoadvance

import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.utils.ext.secondsToShowAnswer
import com.ichi2.anki.utils.ext.secondsToShowQuestion
import com.ichi2.libanki.DeckId
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

data class AutoAdvanceSettings(
val questionAction: QuestionAction,
val answerAction: AnswerAction,
val durationToShowQuestionFor: Duration,
val durationToShowAnswerFor: Duration,
val waitForAudio: Boolean
) {
companion object {
suspend fun createInstance(deckId: DeckId): AutoAdvanceSettings {
val config = withCol { decks.configDictForDeckId(deckId) }
val questionAction = QuestionAction.from(config)
val answerAction = AnswerAction.from(config)
val waitForAudio = config.optBoolean("waitForAudio", true)

return AutoAdvanceSettings(
questionAction = questionAction,
answerAction = answerAction,
durationToShowQuestionFor = config.secondsToShowQuestion.toDuration(DurationUnit.SECONDS),
durationToShowAnswerFor = config.secondsToShowAnswer.toDuration(DurationUnit.SECONDS),
waitForAudio = waitForAudio
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.ui.windows.reviewer.autoadvance

import com.ichi2.libanki.DeckConfig

enum class QuestionAction(val configValue: Int) {
SHOW_ANSWER(0),
SHOW_REMINDER(1);

companion object {
fun from(config: DeckConfig): QuestionAction {
val value = config.optInt("questionAction")
return entries.firstOrNull { it.configValue == value } ?: SHOW_ANSWER
}
}
}

0 comments on commit d3be563

Please sign in to comment.