Refactor VocabularyCard into specialized VocabularyDisplayCard and VocabularyExerciseCard components.

This commit is contained in:
jonasgaudian
2026-02-17 12:12:57 +01:00
parent 4855a347b9
commit f779da470f
7 changed files with 217 additions and 73 deletions

View File

@@ -126,7 +126,8 @@ dependencies {
implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime implementation(libs.androidx.room.runtime) // ADDED: Explicitly add runtime
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.core.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 // Networking
implementation(libs.retrofit) implementation(libs.retrofit)

View File

@@ -1,14 +1,23 @@
package eu.gaudian.translator.view.vocabulary package eu.gaudian.translator.view.vocabulary
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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.CategorySelectionDialog
import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog import eu.gaudian.translator.view.dialogs.ImportVocabularyDialog
import eu.gaudian.translator.view.dialogs.StageSelectionDialog 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.LanguageViewModel
import eu.gaudian.translator.viewmodel.VocabularyViewModel import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -71,6 +81,10 @@ fun VocabularyCardHost(
vocabularyItem = vocabularyViewModel.getVocabularyItemById(itemId) 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( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
@@ -78,6 +92,7 @@ fun VocabularyCardHost(
title = stringResource(R.string.item_details), title = stringResource(R.string.item_details),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
actions = { actions = {
if (!isEditing) {
// Previous button // Previous button
if (navigationPosition > 0) { if (navigationPosition > 0) {
IconButton(onClick = { IconButton(onClick = {
@@ -117,6 +132,7 @@ fun VocabularyCardHost(
} }
} }
} }
}
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -135,6 +151,11 @@ fun VocabularyCardHost(
var showStageDialog by remember { mutableStateOf(false) } var showStageDialog by remember { mutableStateOf(false) }
var showImportDialog by remember { mutableStateOf(false) } var showImportDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
LaunchedEffect(currentVocabularyItem.id) {
isEditing = false
onSaveEdit = null
onCancelEdit = null
}
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val stats by vocabularyViewModel val stats by vocabularyViewModel
@@ -186,9 +207,45 @@ fun VocabularyCardHost(
} }
// Main content // Main content
VocabularyCard( 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, vocabularyItem = currentVocabularyItem,
exerciseMode = exerciseMode,
switchOrder = switchOrder == true, switchOrder = switchOrder == true,
isFlipped = isFlipped, isFlipped = isFlipped,
onStatisticsClick = { showStatisticsDialog = true }, onStatisticsClick = { showStatisticsDialog = true },
@@ -196,8 +253,20 @@ fun VocabularyCardHost(
onMoveToStageClick = { showStageDialog = true }, onMoveToStageClick = { showStageDialog = true },
onDeleteClick = { showDeleteDialog = true }, onDeleteClick = { showDeleteDialog = true },
navController = navController, navController = navController,
isUserSpellingCorrect = false, onEditStateChange = { editing ->
isEditing = editing
if (!editing) {
onSaveEdit = null
onCancelEdit = null
}
},
onEditActionHandlersReady = { onSave, onCancel ->
onSaveEdit = onSave
onCancelEdit = onCancel
},
) )
}
}
// Dialogs are unaffected by the layout change // Dialogs are unaffected by the layout change
if (showQuitDialog) { if (showQuitDialog) {

View File

@@ -48,7 +48,7 @@ import eu.gaudian.translator.ui.theme.semanticColors
import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.composable.AppButton import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.ComponentDefaults 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 import eu.gaudian.translator.viewmodel.LanguageViewModel
/** /**
@@ -141,11 +141,10 @@ fun GuessingExercise(
navController: NavController, navController: NavController,
) { ) {
VocabularyCard( VocabularyExerciseCard(
vocabularyItem = state.item, vocabularyItem = state.item,
isFlipped = state.isRevealed, isFlipped = state.isRevealed,
navController = navController, navController = navController,
exerciseMode = true,
switchOrder = state.isSwitched, switchOrder = state.isSwitched,
) )
} }
@@ -158,13 +157,12 @@ fun SpellingExercise(
navController: NavController, navController: NavController,
) { ) {
VocabularyCard( VocabularyExerciseCard(
vocabularyItem = state.item, vocabularyItem = state.item,
isFlipped = state.isRevealed, isFlipped = state.isRevealed,
userSpellingAnswer = state.userAnswer, userSpellingAnswer = state.userAnswer,
isUserSpellingCorrect = state.isCorrect, isUserSpellingCorrect = state.isCorrect,
navController = navController, navController = navController,
exerciseMode = true,
switchOrder = state.isSwitched, switchOrder = state.isSwitched,
) )
} }

View File

@@ -66,8 +66,6 @@ internal fun DraggableActionPanel(
onDismiss: () -> Unit, onDismiss: () -> Unit,
isEditing: Boolean, isEditing: Boolean,
onEditClick: () -> Unit, onEditClick: () -> Unit,
onSaveClick: () -> Unit,
onCancelClick: () -> Unit,
onStatisticsClick: () -> Unit, onStatisticsClick: () -> Unit,
onMoveToCategoryClick: () -> Unit, onMoveToCategoryClick: () -> Unit,
onMoveToStageClick: () -> 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) { if (!isEditing) {
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
if (showAnalyzeGrammarButton) { if (showAnalyzeGrammarButton) {
ActionItem( ActionItem(
@@ -252,8 +245,6 @@ fun DraggableActionPanelPreview() {
onDismiss = {}, onDismiss = {},
isEditing = false, isEditing = false,
onEditClick = {}, onEditClick = {},
onSaveClick = {},
onCancelClick = {},
onStatisticsClick = {}, onStatisticsClick = {},
onMoveToCategoryClick = {}, onMoveToCategoryClick = {},

View File

@@ -86,6 +86,54 @@ import eu.gaudian.translator.viewmodel.VocabularyViewModel
import kotlinx.coroutines.launch 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") @Deprecated("We need to seperate this into two: one for display and one for exercises")
@Composable @Composable
fun VocabularyCard( fun VocabularyCard(
@@ -101,6 +149,37 @@ fun VocabularyCard(
userSpellingAnswer: String? = null, userSpellingAnswer: String? = null,
isUserSpellingCorrect: Boolean? = 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 activity = LocalContext.current.findActivity()
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -202,6 +281,7 @@ fun VocabularyCard(
) )
vocabularyViewModel.editVocabularyItem(updatedItem) vocabularyViewModel.editVocabularyItem(updatedItem)
isEditing = false isEditing = false
onEditStateChange?.invoke(false)
} }
} }
} }
@@ -214,6 +294,7 @@ fun VocabularyCard(
editedLangSecondId = item.languageSecondId editedLangSecondId = item.languageSecondId
editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures() editedFeatures = item.features?.let { jsonParser.decodeFromString<VocabularyFeatures>(it) } ?: VocabularyFeatures()
isEditing = false isEditing = false
onEditStateChange?.invoke(false)
} }
} }
@@ -287,13 +368,13 @@ fun VocabularyCard(
onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it }, onWordChange = { if (!switchOrder) editedWordFirst = it else editedWordSecond = it },
language = if (!switchOrder) languageFirst else languageSecond, language = if (!switchOrder) languageFirst else languageSecond,
onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it }, onLanguageIdChange = { if (!switchOrder) editedLangFirstId = it else editedLangSecondId = it },
isRevealed = isFrontFace || exerciseMode, isRevealed = isFrontFace || isExerciseMode,
userSpellingAnswer = userSpellingAnswer, userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect, isUserSpellingCorrect = isUserSpellingCorrect,
correctWord = if (switchOrder) item.wordFirst else item.wordSecond, correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second, wordDetails = if (!switchOrder) editedFeatures.first else editedFeatures.second,
onEditGrammarClick = { showGrammarDialogFor = "first" }, onEditGrammarClick = { showGrammarDialogFor = "first" },
isExerciseMode = exerciseMode, isExerciseMode = isExerciseMode,
vocabularyItem = item, vocabularyItem = item,
onMoreClick = { onMoreClick = {
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@@ -318,7 +399,7 @@ fun VocabularyCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
) )
if (!exerciseMode && !isFlipped) { if (!isExerciseMode && !isEditing && !isFlipped) {
IconButton(onClick = { showActionPanel = true }) { IconButton(onClick = { showActionPanel = true }) {
Icon( Icon(
imageVector = AppIcons.MoreVert, imageVector = AppIcons.MoreVert,
@@ -340,7 +421,7 @@ fun VocabularyCard(
onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it }, onWordChange = { if (switchOrder) editedWordFirst = it else editedWordSecond = it },
language = if (switchOrder) languageFirst else languageSecond, language = if (switchOrder) languageFirst else languageSecond,
onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it }, onLanguageIdChange = { if (switchOrder) editedLangFirstId = it else editedLangSecondId = it },
isRevealed = !(!isFlipped && exerciseMode), isRevealed = !(!isFlipped && isExerciseMode),
userSpellingAnswer = userSpellingAnswer, userSpellingAnswer = userSpellingAnswer,
isUserSpellingCorrect = isUserSpellingCorrect, isUserSpellingCorrect = isUserSpellingCorrect,
correctWord = if (switchOrder) item.wordFirst else item.wordSecond, correctWord = if (switchOrder) item.wordFirst else item.wordSecond,
@@ -349,7 +430,7 @@ fun VocabularyCard(
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
showGrammarDialogFor = "second" showGrammarDialogFor = "second"
}, },
isExerciseMode = exerciseMode, isExerciseMode = isExerciseMode,
vocabularyItem = item, vocabularyItem = item,
onMoreClick = { onMoreClick = {
@Suppress("HardCodedStringLiteral") @Suppress("HardCodedStringLiteral")
@@ -362,7 +443,7 @@ fun VocabularyCard(
!switchOrder !switchOrder
if(isFlipped || !exerciseMode) if(isFlipped || !isExerciseMode)
DraggableActionPanel( DraggableActionPanel(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
@@ -370,9 +451,14 @@ fun VocabularyCard(
isOpen = showActionPanel, isOpen = showActionPanel,
onDismiss = { showActionPanel = false }, onDismiss = { showActionPanel = false },
isEditing = isEditing, isEditing = isEditing,
onEditClick = { isEditing = true }, onEditClick = {
onSaveClick = { handleSave() }, isEditing = true
onCancelClick = handleCancel, onEditStateChange?.invoke(true)
onEditActionHandlersReady?.invoke(
{ handleSave() },
{ handleCancel() }
)
},
onStatisticsClick = onStatisticsClick, onStatisticsClick = onStatisticsClick,
onMoveToCategoryClick = onMoveToCategoryClick, onMoveToCategoryClick = onMoveToCategoryClick,
@@ -439,18 +525,15 @@ fun VocabularyCardPreview() {
languageSecondId = R.string.language_2 languageSecondId = R.string.language_2
) )
val navController = NavController(LocalContext.current) val navController = NavController(LocalContext.current)
VocabularyCard( VocabularyDisplayCard(
vocabularyItem = item, vocabularyItem = item,
navController = navController, navController = navController,
exerciseMode = false,
isFlipped = false, isFlipped = false,
switchOrder = false, switchOrder = false,
onStatisticsClick = {}, onStatisticsClick = {},
onMoveToCategoryClick = {}, onMoveToCategoryClick = {},
onMoveToStageClick = {}, onMoveToStageClick = {},
onDeleteClick = {}, onDeleteClick = {},
userSpellingAnswer = null,
isUserSpellingCorrect = null
) )
} }
@@ -476,7 +559,7 @@ private fun FrequencyPill(zipfFrequency: Float?) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.width(80.dp), .width(100.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Surface( Surface(

View File

@@ -120,7 +120,7 @@
<string-array name="changelog_entries"> <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.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.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> <item> </item>
</string-array> </string-array>

View File

@@ -43,6 +43,7 @@ truth = "1.4.5"
zstdJni = "1.5.7-7" zstdJni = "1.5.7-7"
composeMarkdown = "0.5.8" composeMarkdown = "0.5.8"
jitpack = "1.0.10" jitpack = "1.0.10"
foundationVersion = "1.10.3"
[libraries] [libraries]
@@ -103,6 +104,7 @@ hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", ve
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" } hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" }
mockk = { module = "io.mockk:mockk", version = "1.14.9" } mockk = { module = "io.mockk:mockk", version = "1.14.9" }
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" } compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }