implement DailyReviewScreen and add support for "due today only" exercise configuration

This commit is contained in:
jonasgaudian
2026-02-18 01:01:39 +01:00
parent 9db538bf0a
commit c81e0886b8
5 changed files with 190 additions and 3 deletions

View File

@@ -58,6 +58,7 @@ object NavigationRoutes {
const val NEW_WORD_REVIEW = "new_word_review" const val NEW_WORD_REVIEW = "new_word_review"
const val VOCABULARY_DETAIL = "vocabulary_detail" const val VOCABULARY_DETAIL = "vocabulary_detail"
const val START_EXERCISE = "start_exercise" const val START_EXERCISE = "start_exercise"
const val START_EXERCISE_DAILY = "start_exercise_daily"
const val CATEGORY_DETAIL = "category_detail" const val CATEGORY_DETAIL = "category_detail"
const val CATEGORY_LIST = "category_list_screen" const val CATEGORY_LIST = "category_list_screen"
const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap" const val STATS_VOCABULARY_HEATMAP = "stats/vocabulary_heatmap"
@@ -182,7 +183,16 @@ fun AppNavHost(
val categoryId = categoryIdString?.toIntOrNull() val categoryId = categoryIdString?.toIntOrNull()
StartExerciseScreen( StartExerciseScreen(
navController = navController, navController = navController,
preselectedCategoryId = categoryId preselectedCategoryId = categoryId,
dueTodayOnly = false
)
}
composable(NavigationRoutes.START_EXERCISE_DAILY) {
StartExerciseScreen(
navController = navController,
preselectedCategoryId = null,
dueTodayOnly = true
) )
} }

View File

@@ -77,6 +77,7 @@ import kotlinx.coroutines.launch
fun StartExerciseScreen( fun StartExerciseScreen(
navController: NavHostController, navController: NavHostController,
preselectedCategoryId: Int? = null, preselectedCategoryId: Int? = null,
dueTodayOnly: Boolean = false,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity() val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
@@ -84,6 +85,15 @@ fun StartExerciseScreen(
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity) val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
val exerciseViewModel: VocabularyExerciseViewModel = 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 exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
val allCategories by categoryViewModel.categories.collectAsState(initial = emptyList()) val allCategories by categoryViewModel.categories.collectAsState(initial = emptyList())
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) } var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }

View File

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

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -85,7 +84,6 @@ fun VocabularyCardHost(
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopAppBar( AppTopAppBar(
modifier = Modifier.height(56.dp),
title = stringResource(R.string.item_details), title = stringResource(R.string.item_details),
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
actions = { actions = {

View File

@@ -450,6 +450,7 @@
<string name="no_models_configured">No Models Configured</string> <string name="no_models_configured">No Models Configured</string>
<string name="no_models_found">No models found</string> <string name="no_models_found">No models found</string>
<string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string> <string name="no_new_vocabulary_to_sort">No New Vocabulary to Sort</string>
<string name="no_items_due_for_review">No items due for review today. Great job!</string>
<string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string> <string name="no_vocabulary_items_found_perhaps_try_changing_the_filters">No vocabulary items found. Perhaps try changing the filters?</string>
<string name="not_available">Not available</string> <string name="not_available">Not available</string>