implement DailyReviewScreen and add support for "due today only" exercise configuration
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<List<Pair<Language, Language>>>(emptyList()) }
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -450,6 +450,7 @@
|
||||
<string name="no_models_configured">No Models Configured</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_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="not_available">Not available</string>
|
||||
|
||||
Reference in New Issue
Block a user