Refactor VocabularyCard into specialized VocabularyDisplayCard and VocabularyExerciseCard components.
This commit is contained in:
@@ -126,7 +126,8 @@ dependencies {
|
||||
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.core.ktx)
|
||||
ksp(libs.room.compiler) // CHANGED: Use ksp instead of implementation
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// Networking
|
||||
implementation(libs.retrofit)
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package eu.gaudian.translator.view.vocabulary
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -41,7 +50,8 @@ import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.dialogs.CategorySelectionDialog
|
||||
import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog
|
||||
import eu.gaudian.translator.view.dialogs.StageSelectionDialog
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyDisplayCard
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -71,6 +81,10 @@ fun VocabularyCardHost(
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId)
|
||||
}
|
||||
|
||||
var isEditing by remember { mutableStateOf(false) }
|
||||
var onSaveEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
var onCancelEdit by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopAppBar(
|
||||
@@ -78,42 +92,44 @@ fun VocabularyCardHost(
|
||||
title = stringResource(R.string.item_details),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
actions = {
|
||||
// Previous button
|
||||
if (navigationPosition > 0) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToPreviousItem()) {
|
||||
val prevItem = navigationItems[navigationPosition - 1]
|
||||
scope.launch {
|
||||
if (!isEditing) {
|
||||
// Previous button
|
||||
if (navigationPosition > 0) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToPreviousItem()) {
|
||||
val prevItem = navigationItems[navigationPosition - 1]
|
||||
scope.launch {
|
||||
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(prevItem.id)
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(prevItem.id)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowLeft,
|
||||
contentDescription = stringResource(R.string.previous_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowLeft,
|
||||
contentDescription = stringResource(R.string.previous_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (navigationPosition < navigationItems.size - 1) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToNextItem()) {
|
||||
val nextItem = navigationItems[navigationPosition + 1]
|
||||
scope.launch {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(nextItem.id)
|
||||
// Next button
|
||||
if (navigationPosition < navigationItems.size - 1) {
|
||||
IconButton(onClick = {
|
||||
if (vocabularyViewModel.navigateToNextItem()) {
|
||||
val nextItem = navigationItems[navigationPosition + 1]
|
||||
scope.launch {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
vocabularyItem = vocabularyViewModel.getVocabularyItemById(nextItem.id)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowRight,
|
||||
contentDescription = stringResource(R.string.next_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
AppIcons.ArrowRight,
|
||||
contentDescription = stringResource(R.string.next_item),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +151,11 @@ fun VocabularyCardHost(
|
||||
var showStageDialog by remember { mutableStateOf(false) }
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(currentVocabularyItem.id) {
|
||||
isEditing = false
|
||||
onSaveEdit = null
|
||||
onCancelEdit = null
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val stats by vocabularyViewModel
|
||||
@@ -186,18 +207,66 @@ fun VocabularyCardHost(
|
||||
}
|
||||
|
||||
// Main content
|
||||
VocabularyCard(
|
||||
vocabularyItem = currentVocabularyItem,
|
||||
exerciseMode = exerciseMode,
|
||||
switchOrder = switchOrder == true,
|
||||
isFlipped = isFlipped,
|
||||
onStatisticsClick = { showStatisticsDialog = true },
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
onDeleteClick = { showDeleteDialog = true },
|
||||
navController = navController,
|
||||
isUserSpellingCorrect = false,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (!exerciseMode && isEditing) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onCancelEdit?.invoke() },
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.label_cancel))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = { onSaveEdit?.invoke() },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.label_save))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exerciseMode) {
|
||||
VocabularyExerciseCard(
|
||||
vocabularyItem = currentVocabularyItem,
|
||||
switchOrder = switchOrder == true,
|
||||
isFlipped = isFlipped,
|
||||
navController = navController,
|
||||
)
|
||||
} else {
|
||||
VocabularyDisplayCard(
|
||||
vocabularyItem = currentVocabularyItem,
|
||||
switchOrder = switchOrder == true,
|
||||
isFlipped = isFlipped,
|
||||
onStatisticsClick = { showStatisticsDialog = true },
|
||||
onMoveToCategoryClick = { showCategoryDialog = true },
|
||||
onMoveToStageClick = { showStageDialog = true },
|
||||
onDeleteClick = { showDeleteDialog = true },
|
||||
navController = navController,
|
||||
onEditStateChange = { editing ->
|
||||
isEditing = editing
|
||||
if (!editing) {
|
||||
onSaveEdit = null
|
||||
onCancelEdit = null
|
||||
}
|
||||
},
|
||||
onEditActionHandlersReady = { onSave, onCancel ->
|
||||
onSaveEdit = onSave
|
||||
onCancelEdit = onCancel
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs are unaffected by the layout change
|
||||
if (showQuitDialog) {
|
||||
|
||||
@@ -48,7 +48,7 @@ import eu.gaudian.translator.ui.theme.semanticColors
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.ComponentDefaults
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyCard
|
||||
import eu.gaudian.translator.view.vocabulary.card.VocabularyExerciseCard
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
|
||||
/**
|
||||
@@ -141,11 +141,10 @@ fun GuessingExercise(
|
||||
navController: NavController,
|
||||
) {
|
||||
|
||||
VocabularyCard(
|
||||
VocabularyExerciseCard(
|
||||
vocabularyItem = state.item,
|
||||
isFlipped = state.isRevealed,
|
||||
navController = navController,
|
||||
exerciseMode = true,
|
||||
switchOrder = state.isSwitched,
|
||||
)
|
||||
}
|
||||
@@ -158,13 +157,12 @@ fun SpellingExercise(
|
||||
navController: NavController,
|
||||
) {
|
||||
|
||||
VocabularyCard(
|
||||
VocabularyExerciseCard(
|
||||
vocabularyItem = state.item,
|
||||
isFlipped = state.isRevealed,
|
||||
userSpellingAnswer = state.userAnswer,
|
||||
isUserSpellingCorrect = state.isCorrect,
|
||||
navController = navController,
|
||||
exerciseMode = true,
|
||||
switchOrder = state.isSwitched,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,8 +66,6 @@ internal fun DraggableActionPanel(
|
||||
onDismiss: () -> Unit,
|
||||
isEditing: Boolean,
|
||||
onEditClick: () -> Unit,
|
||||
onSaveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onStatisticsClick: () -> Unit,
|
||||
onMoveToCategoryClick: () -> Unit,
|
||||
onMoveToStageClick: () -> Unit,
|
||||
@@ -175,13 +173,8 @@ internal fun DraggableActionPanel(
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
|
||||
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
|
||||
} else {
|
||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||
}
|
||||
if (!isEditing) {
|
||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||
|
||||
if (showAnalyzeGrammarButton) {
|
||||
ActionItem(
|
||||
@@ -252,8 +245,6 @@ fun DraggableActionPanelPreview() {
|
||||
onDismiss = {},
|
||||
isEditing = false,
|
||||
onEditClick = {},
|
||||
onSaveClick = {},
|
||||
onCancelClick = {},
|
||||
|
||||
onStatisticsClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
|
||||
@@ -86,6 +86,54 @@ import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun VocabularyDisplayCard(
|
||||
vocabularyItem: VocabularyItem,
|
||||
navController: NavController,
|
||||
isFlipped: Boolean,
|
||||
switchOrder: Boolean,
|
||||
onStatisticsClick: () -> Unit = {},
|
||||
onMoveToCategoryClick: () -> Unit = {},
|
||||
onMoveToStageClick: () -> Unit = {},
|
||||
onDeleteClick: () -> Unit = {},
|
||||
onEditStateChange: ((Boolean) -> Unit)? = null,
|
||||
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
VocabularyCardContent(
|
||||
vocabularyItem = vocabularyItem,
|
||||
navController = navController,
|
||||
isExerciseMode = false,
|
||||
isFlipped = isFlipped,
|
||||
switchOrder = switchOrder,
|
||||
onStatisticsClick = onStatisticsClick,
|
||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||
onMoveToStageClick = onMoveToStageClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
onEditStateChange = onEditStateChange,
|
||||
onEditActionHandlersReady = onEditActionHandlersReady,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VocabularyExerciseCard(
|
||||
vocabularyItem: VocabularyItem,
|
||||
navController: NavController,
|
||||
isFlipped: Boolean,
|
||||
switchOrder: Boolean,
|
||||
userSpellingAnswer: String? = null,
|
||||
isUserSpellingCorrect: Boolean? = null,
|
||||
) {
|
||||
VocabularyCardContent(
|
||||
vocabularyItem = vocabularyItem,
|
||||
navController = navController,
|
||||
isExerciseMode = true,
|
||||
isFlipped = isFlipped,
|
||||
switchOrder = switchOrder,
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("We need to seperate this into two: one for display and one for exercises")
|
||||
@Composable
|
||||
fun VocabularyCard(
|
||||
@@ -101,6 +149,37 @@ fun VocabularyCard(
|
||||
userSpellingAnswer: String? = null,
|
||||
isUserSpellingCorrect: Boolean? = null,
|
||||
) {
|
||||
VocabularyCardContent(
|
||||
vocabularyItem = vocabularyItem,
|
||||
navController = navController,
|
||||
isExerciseMode = exerciseMode,
|
||||
isFlipped = isFlipped,
|
||||
switchOrder = switchOrder,
|
||||
onStatisticsClick = onStatisticsClick,
|
||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||
onMoveToStageClick = onMoveToStageClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VocabularyCardContent(
|
||||
vocabularyItem: VocabularyItem,
|
||||
navController: NavController,
|
||||
isExerciseMode: Boolean,
|
||||
isFlipped: Boolean,
|
||||
switchOrder: Boolean,
|
||||
onStatisticsClick: () -> Unit = {},
|
||||
onMoveToCategoryClick: () -> Unit = {},
|
||||
onMoveToStageClick: () -> Unit = {},
|
||||
onDeleteClick: () -> Unit = {},
|
||||
userSpellingAnswer: String? = null,
|
||||
isUserSpellingCorrect: Boolean? = null,
|
||||
onEditStateChange: ((Boolean) -> Unit)? = null,
|
||||
onEditActionHandlersReady: ((onSave: () -> Unit, onCancel: () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
@@ -202,6 +281,7 @@ fun VocabularyCard(
|
||||
)
|
||||
vocabularyViewModel.editVocabularyItem(updatedItem)
|
||||
isEditing = false
|
||||
onEditStateChange?.invoke(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +294,7 @@ fun VocabularyCard(
|
||||
editedLangSecondId = item.languageSecondId
|
||||
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
|
||||
isEditing = false
|
||||
onEditStateChange?.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,13 +368,13 @@ fun VocabularyCard(
|
||||
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||
language = if (!switchOrder) languageFirst else languageSecond,
|
||||
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||
isRevealed = isFrontFace || exerciseMode,
|
||||
isRevealed = isFrontFace || isExerciseMode,
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
|
||||
onEditGrammarClick = { showGrammarDialogFor = "first" },
|
||||
isExerciseMode = exerciseMode,
|
||||
isExerciseMode = isExerciseMode,
|
||||
vocabularyItem = item,
|
||||
onMoreClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@@ -318,7 +399,7 @@ fun VocabularyCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
if (!exerciseMode && !isFlipped) {
|
||||
if (!isExerciseMode && !isEditing && !isFlipped) {
|
||||
IconButton(onClick = { showActionPanel = true }) {
|
||||
Icon(
|
||||
imageVector = AppIcons.MoreVert,
|
||||
@@ -340,7 +421,7 @@ fun VocabularyCard(
|
||||
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
|
||||
language = if (switchOrder) languageFirst else languageSecond,
|
||||
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
|
||||
isRevealed = !(!isFlipped && exerciseMode),
|
||||
isRevealed = !(!isFlipped && isExerciseMode),
|
||||
userSpellingAnswer = userSpellingAnswer,
|
||||
isUserSpellingCorrect = isUserSpellingCorrect,
|
||||
correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
|
||||
@@ -349,7 +430,7 @@ fun VocabularyCard(
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
showGrammarDialogFor = "second"
|
||||
},
|
||||
isExerciseMode = exerciseMode,
|
||||
isExerciseMode = isExerciseMode,
|
||||
vocabularyItem = item,
|
||||
onMoreClick = {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@@ -362,7 +443,7 @@ fun VocabularyCard(
|
||||
|
||||
|
||||
!switchOrder
|
||||
if(isFlipped || !exerciseMode)
|
||||
if(isFlipped || !isExerciseMode)
|
||||
DraggableActionPanel(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
@@ -370,9 +451,14 @@ fun VocabularyCard(
|
||||
isOpen = showActionPanel,
|
||||
onDismiss = { showActionPanel = false },
|
||||
isEditing = isEditing,
|
||||
onEditClick = { isEditing = true },
|
||||
onSaveClick = { handleSave() },
|
||||
onCancelClick = handleCancel,
|
||||
onEditClick = {
|
||||
isEditing = true
|
||||
onEditStateChange?.invoke(true)
|
||||
onEditActionHandlersReady?.invoke(
|
||||
{ handleSave() },
|
||||
{ handleCancel() }
|
||||
)
|
||||
},
|
||||
|
||||
onStatisticsClick = onStatisticsClick,
|
||||
onMoveToCategoryClick = onMoveToCategoryClick,
|
||||
@@ -439,18 +525,15 @@ fun VocabularyCardPreview() {
|
||||
languageSecondId = R.string.language_2
|
||||
)
|
||||
val navController = NavController(LocalContext.current)
|
||||
VocabularyCard(
|
||||
VocabularyDisplayCard(
|
||||
vocabularyItem = item,
|
||||
navController = navController,
|
||||
exerciseMode = false,
|
||||
isFlipped = false,
|
||||
switchOrder = false,
|
||||
onStatisticsClick = {},
|
||||
onMoveToCategoryClick = {},
|
||||
onMoveToStageClick = {},
|
||||
onDeleteClick = {},
|
||||
userSpellingAnswer = null,
|
||||
isUserSpellingCorrect = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -476,7 +559,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.width(80.dp),
|
||||
.width(100.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Surface(
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<string-array name="changelog_entries">
|
||||
<item>Version 0.3.0 \n• Enabled CSV Import for Vocabulary\n• Option to use a translation server for translations instead of AI models for some supported langugaes\n• UI bug fixes \n• Show word frequency \n• Performance optimizations \n• Improved translations (German and Portuguese)</item>
|
||||
<item>Version 0.4.0 \n• Added dictionary download (beta) \n• UI enhancements \n• Bugfixes \n• Re-designed vocabulary card with improved UI \n• More pre-configured providers \n• Improved performance</item>
|
||||
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now </item>
|
||||
<item>Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now \n• Exercises are more fun now </item>
|
||||
<item> </item>
|
||||
|
||||
</string-array>
|
||||
|
||||
Reference in New Issue
Block a user