From f779da470fc3137ca1a768be99b13bcfbf4d4bb3 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:12:57 +0100 Subject: [PATCH] Refactor `VocabularyCard` into specialized `VocabularyDisplayCard` and `VocabularyExerciseCard` components. --- app/build.gradle.kts | 3 +- .../view/vocabulary/VocabularyCardHost.kt | 153 +++++++++++++----- .../view/vocabulary/VocabularyExercise.kt | 8 +- .../vocabulary/card/DraggableActionPanel.kt | 11 +- .../view/vocabulary/card/VocabularyCard.kt | 111 +++++++++++-- app/src/main/res/values/arrays.xml | 2 +- gradle/libs.versions.toml | 2 + 7 files changed, 217 insertions(+), 73 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a04f99f..2e73b66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt index 27fdd2b..7551477 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyCardHost.kt @@ -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) { diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExercise.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExercise.kt index caeb696..366435f 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExercise.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyExercise.kt @@ -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, ) } diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/DraggableActionPanel.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/DraggableActionPanel.kt index 3c8d329..72bb597 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/DraggableActionPanel.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/DraggableActionPanel.kt @@ -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 = {}, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt index a905faf..befeb33 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt @@ -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(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( diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f7e6645..70e0b8d 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -120,7 +120,7 @@ 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) 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 - Version 0.5.0 \n• Reworked UI with new focus on Flashcards and Exercises \n• Adding vocabulary is easier and more intuitive now + 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b06cd35..1e75aae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,7 @@ truth = "1.4.5" zstdJni = "1.5.7-7" composeMarkdown = "0.5.8" jitpack = "1.0.10" +foundationVersion = "1.10.3" [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" } mockk = { module = "io.mockk:mockk", version = "1.14.9" } compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "composeMarkdown" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }