From c81e0886b81481e0f88373588c36c8023599dd36 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:01:39 +0100 Subject: [PATCH] implement `DailyReviewScreen` and add support for "due today only" exercise configuration --- .../eu/gaudian/translator/view/Navigation.kt | 12 +- .../view/exercises/StartExerciseScreen.kt | 10 ++ .../translator/view/home/DailyReviewScreen.kt | 168 ++++++++++++++++++ .../view/vocabulary/VocabularyCardHost.kt | 2 - app/src/main/res/values/strings.xml | 1 + 5 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index aa2c7b8..e008bc0 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -58,6 +58,7 @@ object NavigationRoutes { const val NEW_WORD_REVIEW = "new_word_review" const val VOCABULARY_DETAIL = "vocabulary_detail" const val START_EXERCISE = "start_exercise" + const val START_EXERCISE_DAILY = "start_exercise_daily" const val CATEGORY_DETAIL = "category_detail" const val CATEGORY_LIST = "category_list_screen" const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap" @@ -182,7 +183,16 @@ fun AppNavHost( val categoryId = categoryIdString?.toIntOrNull() StartExerciseScreen( navController = navController, - preselectedCategoryId = categoryId + preselectedCategoryId = categoryId, + dueTodayOnly = false + ) + } + + composable(NavigationRoutes.START_EXERCISE_DAILY) { + StartExerciseScreen( + navController = navController, + preselectedCategoryId = null, + dueTodayOnly = true ) } diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt index 2da92f3..733163e 100644 --- a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt @@ -77,6 +77,7 @@ import kotlinx.coroutines.launch fun StartExerciseScreen( navController: NavHostController, preselectedCategoryId: Int? = null, + dueTodayOnly: Boolean = false, modifier: Modifier = Modifier ) { val activity = androidx.compose.ui.platform.LocalContext.current.findActivity() @@ -84,6 +85,15 @@ fun StartExerciseScreen( val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity) + // Initialize exercise config with dueTodayOnly if specified + androidx.compose.runtime.LaunchedEffect(dueTodayOnly) { + if (dueTodayOnly) { + exerciseViewModel.updatePendingExerciseConfig( + exerciseViewModel.pendingExerciseConfig.value.copy(dueTodayOnly = true) + ) + } + } + val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState() val allCategories by categoryViewModel.categories.collectAsState(initial = emptyList()) var selectedLanguagePairs by remember { mutableStateOf>>(emptyList()) } diff --git a/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt new file mode 100644 index 0000000..7205a5a --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt @@ -0,0 +1,168 @@ +package eu.gaudian.translator.view.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import eu.gaudian.translator.R +import eu.gaudian.translator.utils.findActivity +import eu.gaudian.translator.view.NavigationRoutes +import eu.gaudian.translator.view.composable.AppButton +import eu.gaudian.translator.view.composable.AppScaffold +import eu.gaudian.translator.view.composable.AppTopAppBar +import eu.gaudian.translator.view.library.VocabularyCard +import eu.gaudian.translator.viewmodel.LanguageViewModel +import eu.gaudian.translator.viewmodel.VocabularyViewModel + +@Composable +fun DailyReviewScreen( + navController: NavHostController, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val activity = context.findActivity() + val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity) + val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity) + + val dueTodayItems by vocabularyViewModel.dueTodayItems.collectAsStateWithLifecycle(initialValue = emptyList()) + val allLanguages by languageViewModel.allLanguages.collectAsStateWithLifecycle(initialValue = emptyList()) + val listState = rememberLazyListState() + + AppScaffold( + topBar = { + AppTopAppBar( + title = stringResource(R.string.label_daily_review), + onNavigateBack = { navController.popBackStack() } + ) + }, + modifier = modifier.fillMaxSize() + ) { paddingValues -> + if (dueTodayItems.isEmpty()) { + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(200.dp), + painter = painterResource(id = R.drawable.ic_nothing_found), + contentDescription = null + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.no_items_due_for_review), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } else { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(16.dp) + ) { + items( + items = dueTodayItems, + key = { it.id } + ) { item -> + VocabularyCard( + item = item, + allLanguages = allLanguages, + isSelected = false, + onItemClick = { + vocabularyViewModel.setNavigationContext(dueTodayItems, item.id) + navController.navigate("${NavigationRoutes.VOCABULARY_DETAIL}/${item.id}") + }, + onItemLongClick = { }, + onDeleteClick = { } + ) + } + // Add spacing at the bottom for the button + item { + Spacer(modifier = Modifier.height(80.dp)) + } + } + + // Start Exercise Button (fixed at bottom) + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(paddingValues) + .padding(24.dp) + ) { + AppButton( + onClick = { + navController.navigate(NavigationRoutes.START_EXERCISE_DAILY) + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + enabled = dueTodayItems.isNotEmpty(), + shape = RoundedCornerShape(28.dp) + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.label_start_exercise_2d, dueTodayItems.size), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = stringResource(R.string.cd_play), + modifier = Modifier.size(20.dp) + ) + } + } + } + } + } + } +} 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 1638dbb..e190e9b 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 @@ -7,7 +7,6 @@ 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 @@ -85,7 +84,6 @@ fun VocabularyCardHost( AppScaffold( topBar = { AppTopAppBar( - modifier = Modifier.height(56.dp), title = stringResource(R.string.item_details), onNavigateBack = { navController.popBackStack() }, actions = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46ea281..0beb2a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -450,6 +450,7 @@ No Models Configured No models found No New Vocabulary to Sort + No items due for review today. Great job! No vocabulary items found. Perhaps try changing the filters? Not available