Skip to content
Open
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
8 changes: 6 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.SortOrder
import com.ichi2.anki.libanki.undoAvailable
import com.ichi2.anki.libanki.undoLabel
import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase
import com.ichi2.anki.model.CardStateFilter
import com.ichi2.anki.model.CardsOrNotes
import com.ichi2.anki.model.CardsOrNotes.CARDS
Expand Down Expand Up @@ -866,7 +866,11 @@ open class CardBrowser :
}
actionBarMenu?.findItem(R.id.action_undo)?.run {
isVisible = getColUnsafe.undoAvailable()
title = getColUnsafe.undoLabel()
title = getColUnsafe.undoLabel()?.let { label ->
label.removePrefix("Undo ").let { action ->
undoLabelToSentenceCase(this@CardBrowser, action)
}
}
}

actionBarMenu?.findItem(R.id.action_reschedule_cards)?.title =
Expand Down
10 changes: 8 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.exception.ConfirmModSchemaException
import com.ichi2.anki.libanki.sched.DeckNode
import com.ichi2.anki.libanki.undoAvailable
import com.ichi2.anki.libanki.undoLabel
import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase
import com.ichi2.anki.mediacheck.MediaCheckFragment
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.pages.AnkiPackageImporterFragment
Expand Down Expand Up @@ -702,9 +702,15 @@ open class DeckPicker :
fun onUndoUpdated(a: Unit) {
launchCatchingTask {
withOpenColOrNull {
val rawUndoLabel = undoLabel()
val sentenceCaseLabel = rawUndoLabel?.let { label ->
label.removePrefix("Undo ").let { action ->
undoLabelToSentenceCase(this@DeckPicker, action)
}
}
optionsMenuState =
optionsMenuState?.copy(
undoLabel = undoLabel(),
undoLabel = sentenceCaseLabel,
undoAvailable = undoAvailable(),
)
}
Expand Down
16 changes: 12 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,9 @@ import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.libanki.QueueType
import com.ichi2.anki.libanki.redoAvailable
import com.ichi2.anki.libanki.redoLabel
import com.ichi2.anki.libanki.sched.Counts
import com.ichi2.anki.libanki.sched.CurrentQueueState
import com.ichi2.anki.libanki.undoAvailable
import com.ichi2.anki.libanki.undoLabel
import com.ichi2.anki.multimedia.audio.AudioRecordingController
import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.generateTempAudioFile
import com.ichi2.anki.multimedia.audio.AudioRecordingController.Companion.isAudioRecordingSaved
Expand All @@ -98,6 +96,8 @@ import com.ichi2.anki.reviewer.AnswerButtons.Companion.getTextColors
import com.ichi2.anki.reviewer.AnswerTimer
import com.ichi2.anki.reviewer.AutomaticAnswerAction
import com.ichi2.anki.reviewer.Binding
import com.ichi2.anki.ui.internationalization.redoLabelToSentenceCase
import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase
import com.ichi2.anki.reviewer.BindingMap
import com.ichi2.anki.reviewer.BindingProcessor
import com.ichi2.anki.reviewer.CardMarker
Expand Down Expand Up @@ -897,7 +897,11 @@ open class Reviewer :

if (getColUnsafe.undoAvailable()) {
// set the undo title to a named action ('Undo Answer Card' etc...)
undoIcon.title = getColUnsafe.undoLabel()
undoIcon.title = getColUnsafe.undoLabel()?.let { label ->
label.removePrefix("Undo ").let { action ->
undoLabelToSentenceCase(this@Reviewer, action)
}
}
} else {
// In this case, there is no object word for the verb, "Undo",
// so in some languages such as Japanese, which have pre/post-positional particle with the object,
Expand All @@ -914,7 +918,11 @@ open class Reviewer :

menu.findItem(R.id.action_redo)?.apply {
if (getColUnsafe.redoAvailable()) {
title = getColUnsafe.redoLabel()
title = getColUnsafe.redoLabel()?.let { label ->
label.removePrefix("Redo ").let { action ->
redoLabelToSentenceCase(this@Reviewer, action)
}
}
iconAlpha = Themes.ALPHA_ICON_ENABLED_LIGHT
isEnabled = true
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog.CustomStudyAction.Co
import com.ichi2.anki.libanki.undoAvailable
import com.ichi2.anki.libanki.undoLabel
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase
import com.ichi2.anki.utils.ext.setFragmentResultListener
import com.ichi2.ui.RtlCompliantActionProvider
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -131,9 +132,15 @@ class StudyOptionsActivity :
lifecycleScope.launch {
val newUndoState =
withCol {
val rawLabel = undoLabel()
val sentenceCaseLabel = rawLabel?.let { label ->
label.removePrefix("Undo ").let { action ->
undoLabelToSentenceCase(this@StudyOptionsActivity, action)
}
}
UndoState(
hasAction = undoAvailable(),
label = undoLabel(),
label = sentenceCaseLabel,
)
}
if (undoState != newUndoState) {
Expand Down
18 changes: 12 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/Undo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.ichi2.anki

import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import anki.collection.OpChangesAfterUndo
Expand All @@ -25,8 +26,10 @@ import com.ichi2.anki.libanki.redoAvailable
import com.ichi2.anki.libanki.undoAvailable
import com.ichi2.anki.observability.undoableOp
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.internationalization.undoneMessageToSentenceCase
import com.ichi2.anki.ui.internationalization.redoneMessageToSentenceCase

suspend fun tryUndo(): String {
suspend fun tryUndo(context: Context): String {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context should not be a parameter here, this method should not have a direct Android dependency.

val changes =
undoableOp {
if (undoAvailable()) {
Expand All @@ -38,11 +41,13 @@ suspend fun tryUndo(): String {
return if (changes.operation.isEmpty()) {
TR.actionsNothingToUndo()
} else {
TR.undoActionUndone(changes.operation)
// Convert "{Action} undone" to sentence case (e.g., "Empty cards undone")
val message = TR.undoActionUndone(changes.operation)
undoneMessageToSentenceCase(context, changes.operation)
}
}

suspend fun tryRedo(): String {
suspend fun tryRedo(context: Context): String {
val changes =
undoableOp {
if (redoAvailable()) {
Expand All @@ -54,14 +59,15 @@ suspend fun tryRedo(): String {
return if (changes.operation.isEmpty()) {
TR.actionsNothingToRedo()
} else {
TR.undoRedoAction(changes.operation)
// Convert "Redo {Action}" to sentence case (e.g., "Redo empty cards")
redoneMessageToSentenceCase(context, changes.operation)
}
}

/** If there's an action pending in the review queue, undo it and show a snackbar */
suspend fun FragmentActivity.undoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT) {
withProgress {
val text = tryUndo()
val text = tryUndo(this@undoAndShowSnackbar)
showSnackbar(text, duration)
}
}
Expand All @@ -73,7 +79,7 @@ suspend fun Fragment.undoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT)

suspend fun FragmentActivity.redoAndShowSnackbar(duration: Int = Snackbar.LENGTH_SHORT) {
withProgress {
val text = tryRedo()
val text = tryRedo(this@redoAndShowSnackbar)
showSnackbar(text, duration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,130 @@ fun String.toSentenceCase(
if (this.equals(resString, ignoreCase = true)) return resString
return this
}

/**
* A map of Title Case action names to their sentence case resource IDs.
* Used for undo/redo label conversion.
*/
private val actionToSentenceCaseMap = mapOf(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get these strings from?

Could you add the source to the commit message?

"Empty Cards" to R.string.sentence_empty_cards,
"Custom Study" to R.string.sentence_custom_study,
"Set Due Date" to R.string.sentence_set_due_date,
"Suspend Card" to R.string.sentence_suspend_card,
"Answer Card" to R.string.sentence_answer_card,
"Add Deck" to R.string.sentence_add_deck,
"Add Note" to R.string.sentence_add_note,
"Update Tag" to R.string.sentence_update_tag,
"Update Note" to R.string.sentence_update_note,
"Update Card" to R.string.sentence_update_card,
"Update Deck" to R.string.sentence_update_deck,
"Reset Card" to R.string.sentence_reset_card,
"Build Deck" to R.string.sentence_build_deck,
"Add Note Type" to R.string.sentence_add_notetype,
"Remove Note Type" to R.string.sentence_remove_notetype,
"Update Note Type" to R.string.sentence_update_notetype,
"Update Config" to R.string.sentence_update_config,
"Card Info" to R.string.sentence_card_info,
"Previous Card Info" to R.string.sentence_previous_card_info,
"Set Flag" to R.string.sentence_set_flag,
"Auto Advance" to R.string.sentence_auto_advance,
"Bury Card" to R.string.sentence_bury_card,
"Bury Note" to R.string.sentence_bury_note,
"Unbury/Unsuspend" to R.string.sentence_unbury_unsuspend,
"Rename" to R.string.sentence_rename,
"Reposition" to R.string.sentence_reposition,
"Forget Card" to R.string.sentence_forget_card,
"Toggle Load Balancer" to R.string.sentence_toggle_load_balancer,
)

/**
* Converts an action name from Title Case to sentence case.
*
* @param context The context to access resources
* @param action The action name in Title Case (e.g., "Empty Cards")
* @return The action name in sentence case (e.g., "empty cards")
*/
fun actionToSentenceCase(
context: Context,
action: String,
): String {
val resId = actionToSentenceCaseMap[action]
return if (resId != null) {
context.getString(resId)
} else {
// Fallback: convert to lowercase except first letter
action.replaceFirstChar { it.uppercaseChar() }.let { titleCase ->
titleCase.split(" ").joinToString(" ") { word ->
if (word == titleCase.split(" ").first()) {
word.replaceFirstChar { it.uppercaseChar() }
} else {
word.lowercase()
}
}
}
}
}

/**
* Converts undo label to sentence case.
* Handles patterns like "Undo Empty Cards" -> "Undo empty cards"
*
* @param context The context to access resources
* @param action The action name (e.g., "Empty Cards")
* @return The sentence case version (e.g., "Undo empty cards")
*/
fun undoLabelToSentenceCase(
context: Context,
action: String,
): String {
val actionSentenceCase = actionToSentenceCase(context, action)
return "Undo $actionSentenceCase"
}

/**
* Converts redo label to sentence case.
* Handles patterns like "Redo Empty Cards" -> "Redo empty cards"
*
* @param context The context to access resources
* @param action The action name (e.g., "Empty Cards")
* @return The sentence case version (e.g., "Redo empty cards")
*/
fun redoLabelToSentenceCase(
context: Context,
action: String,
): String {
val actionSentenceCase = actionToSentenceCase(context, action)
return "Redo $actionSentenceCase"
}

/**
* Converts "undone" message to sentence case.
* Handles patterns like "Empty Cards undone" -> "Empty cards undone"
*
* @param context The context to access resources
* @param action The action name (e.g., "Empty Cards")
* @return The sentence case version (e.g., "Empty cards undone")
*/
fun undoneMessageToSentenceCase(
context: Context,
action: String,
): String {
val actionSentenceCase = actionToSentenceCase(context, action)
return "$actionSentenceCase undone"
}

/**
* Converts "redone" message to sentence case.
* Handles patterns like "Empty Cards redone" -> "Empty cards redone"
*
* @param context The context to access resources
* @param action The action name (e.g., "Empty Cards")
* @return The sentence case version (e.g., "Empty cards redone")
*/
fun redoneMessageToSentenceCase(
context: Context,
action: String,
): String {
val actionSentenceCase = actionToSentenceCase(context, action)
return "$actionSentenceCase redone"
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package com.ichi2.anki.ui.windows.reviewer

import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import anki.collection.OpChanges
import anki.frontend.SetSchedulingStatesRequest
import anki.scheduler.CardAnswer.Rating
import com.ichi2.anki.AbstractFlashcardViewer
import com.ichi2.anki.AbstractFlashcardViewer.Companion.RESULT_NO_MORE_CARDS
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.Flag
Expand Down Expand Up @@ -344,12 +346,14 @@ class ReviewerViewModel(

private suspend fun undo() {
Timber.v("ReviewerViewModel::undo")
actionFeedbackFlow.emit(tryUndo())
val context = AnkiDroidApp.instance.applicationContext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context should not exist in a ViewModel, it's an Android concern.

actionFeedbackFlow.emit(tryUndo(context))
}

private suspend fun redo() {
Timber.v("ReviewerViewModel::redo")
actionFeedbackFlow.emit(tryRedo())
val context = AnkiDroidApp.instance.applicationContext
actionFeedbackFlow.emit(tryRedo(context))
}

private suspend fun userAction(
Expand Down Expand Up @@ -568,8 +572,23 @@ class ReviewerViewModel(

private suspend fun updateUndoAndRedoLabels() {
Timber.v("ReviewerViewModel::updateUndoAndRedoLabels")
undoLabelFlow.emit(withCol { undoLabel() })
redoLabelFlow.emit(withCol { redoLabel() })
val context = AnkiDroidApp.instance.applicationContext
undoLabelFlow.emit(
withCol { undoLabel() }?.let { label ->
// Extract action name from "Undo {Action}" format
label.removePrefix("Undo ").let { action ->
com.ichi2.anki.ui.internationalization.undoLabelToSentenceCase(context, action)
}
}
)
redoLabelFlow.emit(
withCol { redoLabel() }?.let { label ->
// Extract action name from "Redo {Action}" format
label.removePrefix("Redo ").let { action ->
com.ichi2.anki.ui.internationalization.redoLabelToSentenceCase(context, action)
}
}
)
}

private suspend fun updateNextTimes() {
Expand Down
Loading