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.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)

View File

@@ -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,6 +92,7 @@ fun VocabularyCardHost(
title = stringResource(R.string.item_details),
onNavigateBack = { navController.popBackStack() },
actions = {
if (!isEditing) {
// Previous button
if (navigationPosition > 0) {
IconButton(onClick = {
@@ -117,6 +132,7 @@ fun VocabularyCardHost(
}
}
}
}
)
}
) { paddingValues ->
@@ -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,9 +207,45 @@ fun VocabularyCardHost(
}
// 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,
exerciseMode = exerciseMode,
switchOrder = switchOrder == true,
isFlipped = isFlipped,
onStatisticsClick = { showStatisticsDialog = true },
@@ -196,8 +253,20 @@ fun VocabularyCardHost(
onMoveToStageClick = { showStageDialog = true },
onDeleteClick = { showDeleteDialog = true },
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
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.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,
)
}

View File

@@ -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 = {},

View File

@@ -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(

View File

@@ -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>

View File

@@ -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" }