Implement the StartExerciseScreen with comprehensive filtering and configuration options.
This commit is contained in:
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -24,40 +23,106 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Hearing
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Circle
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppSlider
|
||||
import eu.gaudian.translator.view.composable.OptionItemSwitch
|
||||
import eu.gaudian.translator.view.dialogs.CategoryDropdown
|
||||
import eu.gaudian.translator.view.vocabulary.VocabularyExerciseType
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun StartExerciseScreen(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val exerciseViewModel: VocabularyExerciseViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val exerciseConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||
var selectedLanguagePairs by remember { mutableStateOf<List<Pair<Language, Language>>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
|
||||
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
|
||||
val selectedPairsIds = remember(selectedLanguagePairs) {
|
||||
selectedLanguagePairs.map { it.first.nameResId to it.second.nameResId }
|
||||
}
|
||||
val selectedCategoryIds = remember(selectedCategories) {
|
||||
selectedCategories.map { it.id }
|
||||
}
|
||||
|
||||
val filteredItemsFlow = remember(
|
||||
selectedPairsIds,
|
||||
selectedCategoryIds,
|
||||
selectedStages,
|
||||
exerciseConfig.dueTodayOnly
|
||||
) {
|
||||
vocabularyViewModel.filterVocabularyItemsByPairs(
|
||||
languagePairs = selectedPairsIds.ifEmpty { null },
|
||||
query = null,
|
||||
categoryIds = selectedCategoryIds.ifEmpty { null },
|
||||
stages = selectedStages.ifEmpty { null },
|
||||
sortOrder = VocabularyViewModel.SortOrder.NEWEST_FIRST,
|
||||
dueTodayOnly = exerciseConfig.dueTodayOnly
|
||||
)
|
||||
}
|
||||
val itemsToShow by filteredItemsFlow.collectAsState(initial = emptyList())
|
||||
val totalItemCount = itemsToShow.size
|
||||
|
||||
var amount by remember { mutableStateOf(0) }
|
||||
androidx.compose.runtime.LaunchedEffect(totalItemCount) {
|
||||
amount = totalItemCount
|
||||
}
|
||||
|
||||
val updateConfig: (eu.gaudian.translator.viewmodel.ExerciseConfig) -> Unit = { config ->
|
||||
exerciseViewModel.updatePendingExerciseConfig(config)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
@@ -67,7 +132,15 @@ fun StartExerciseScreen(
|
||||
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
||||
.fillMaxSize()
|
||||
) {
|
||||
TopBarSection(onBackClick = { navController.popBackStack() })
|
||||
TopBarSection(
|
||||
onBackClick = { navController.popBackStack() },
|
||||
shuffleCards = exerciseConfig.shuffleCards,
|
||||
onShuffleCardsChanged = { updateConfig(exerciseConfig.copy(shuffleCards = it)) },
|
||||
shuffleLanguages = exerciseConfig.shuffleLanguages,
|
||||
onShuffleLanguagesChanged = { updateConfig(exerciseConfig.copy(shuffleLanguages = it)) },
|
||||
trainingMode = exerciseConfig.trainingMode,
|
||||
onTrainingModeChanged = { updateConfig(exerciseConfig.copy(trainingMode = it)) },
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
@@ -76,27 +149,107 @@ fun StartExerciseScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
item { LanguagePairSection() }
|
||||
item { CategoriesSection() }
|
||||
item { DifficultySection() }
|
||||
item { NumberOfCardsSection() }
|
||||
item { QuestionTypesSection() }
|
||||
item {
|
||||
LanguagePairSection(
|
||||
selectedPairs = selectedLanguagePairs,
|
||||
onPairsChanged = { selectedLanguagePairs = it },
|
||||
onOriginLanguageSelected = { language ->
|
||||
selectedOriginLanguage = language
|
||||
if (selectedTargetLanguage?.nameResId == language?.nameResId) {
|
||||
selectedTargetLanguage = null
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(originLanguageId = language?.nameResId))
|
||||
},
|
||||
onTargetLanguageSelected = { language ->
|
||||
selectedTargetLanguage = language
|
||||
if (selectedOriginLanguage?.nameResId == language?.nameResId) {
|
||||
selectedOriginLanguage = null
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(targetLanguageId = language?.nameResId))
|
||||
},
|
||||
selectedOriginLanguage = selectedOriginLanguage,
|
||||
selectedTargetLanguage = selectedTargetLanguage
|
||||
)
|
||||
}
|
||||
item {
|
||||
CategoriesSection(
|
||||
selectedCategories = selectedCategories,
|
||||
onCategoriesChanged = { selectedCategories = it }
|
||||
)
|
||||
}
|
||||
item {
|
||||
DifficultySection(
|
||||
selectedStages = selectedStages,
|
||||
onStagesChanged = { selectedStages = it }
|
||||
)
|
||||
}
|
||||
item {
|
||||
NumberOfCardsSection(
|
||||
totalAvailable = totalItemCount,
|
||||
amount = amount,
|
||||
onAmountChanged = { amount = it },
|
||||
dueTodayOnly = exerciseConfig.dueTodayOnly,
|
||||
onDueTodayOnlyChanged = { updateConfig(exerciseConfig.copy(dueTodayOnly = it)) }
|
||||
)
|
||||
}
|
||||
item {
|
||||
QuestionTypesSection(
|
||||
selectedTypes = exerciseConfig.selectedExerciseTypes,
|
||||
onTypeSelected = { type ->
|
||||
val current = exerciseConfig.selectedExerciseTypes.toMutableSet()
|
||||
if (type in current) {
|
||||
if (current.size > 1) current.remove(type)
|
||||
} else {
|
||||
current.add(type)
|
||||
}
|
||||
updateConfig(exerciseConfig.copy(selectedExerciseTypes = current))
|
||||
}
|
||||
)
|
||||
}
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
}
|
||||
|
||||
BottomButtonSection()
|
||||
BottomButtonSection(
|
||||
enabled = totalItemCount > 0 && amount > 0,
|
||||
amount = amount,
|
||||
onStart = {
|
||||
val finalItems = if (exerciseConfig.shuffleCards) {
|
||||
itemsToShow.shuffled().take(amount)
|
||||
} else {
|
||||
itemsToShow.take(amount)
|
||||
}
|
||||
|
||||
exerciseViewModel.startExerciseWithConfig(
|
||||
finalItems,
|
||||
exerciseConfig.copy(
|
||||
exerciseItemCount = finalItems.size,
|
||||
originalExerciseItems = finalItems,
|
||||
originLanguageId = selectedOriginLanguage?.nameResId,
|
||||
targetLanguageId = selectedTargetLanguage?.nameResId
|
||||
)
|
||||
)
|
||||
|
||||
navController.navigate("vocabulary_exercise/false")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO add a settings icon here (top right), this should open a modal bottom sheed with our
|
||||
// OptionItemSwitch from startscreen
|
||||
//change the behavior: shuffle cards, default: on
|
||||
//shuffle languages, default: on
|
||||
//training mode, default: off
|
||||
//due today will be place more prominent, not in the bottom sheet
|
||||
@Composable
|
||||
fun TopBarSection(onBackClick: () -> Unit) {
|
||||
fun TopBarSection(
|
||||
onBackClick: () -> Unit,
|
||||
shuffleCards: Boolean,
|
||||
onShuffleCardsChanged: (Boolean) -> Unit,
|
||||
shuffleLanguages: Boolean,
|
||||
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
||||
trainingMode: Boolean,
|
||||
onTrainingModeChanged: (Boolean) -> Unit
|
||||
) {
|
||||
var showSettings by remember { mutableStateOf(false) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -118,15 +271,45 @@ fun TopBarSection(onBackClick: () -> Unit) {
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Start Exercise",
|
||||
text = stringResource(R.string.label_start_exercise),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Spacer to balance the back button for centering
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
IconButton(
|
||||
onClick = { showSettings = true },
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Settings",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSettings) {
|
||||
StartExerciseSettingsBottomSheet(
|
||||
sheetState = sheetState,
|
||||
shuffleCards = shuffleCards,
|
||||
onShuffleCardsChanged = onShuffleCardsChanged,
|
||||
shuffleLanguages = shuffleLanguages,
|
||||
onShuffleLanguagesChanged = onShuffleLanguagesChanged,
|
||||
trainingMode = trainingMode,
|
||||
onTrainingModeChanged = onTrainingModeChanged,
|
||||
onDismiss = {
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) {
|
||||
showSettings = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,27 +339,129 @@ fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Get our (exisiting) language pairs from the viewmodel, user should be able to select 0,1 , or several, also order by more popular (amount of flashcard in one language)
|
||||
// so that more used/popular language pairs appear first
|
||||
@Composable
|
||||
fun LanguagePairSection() {
|
||||
var selectedPair by remember { mutableStateOf(0) }
|
||||
fun LanguagePairSection(
|
||||
selectedPairs: List<Pair<Language, Language>>,
|
||||
onPairsChanged: (List<Pair<Language, Language>>) -> Unit,
|
||||
onOriginLanguageSelected: (Language?) -> Unit,
|
||||
onTargetLanguageSelected: (Language?) -> Unit,
|
||||
selectedOriginLanguage: Language?,
|
||||
selectedTargetLanguage: Language?
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val languagesPresent by vocabularyViewModel.languagesPresent.collectAsState(initial = emptySet())
|
||||
val allLanguages by languageViewModel.allLanguages.collectAsState(initial = emptyList())
|
||||
|
||||
val availableLanguages = remember(languagesPresent, allLanguages) {
|
||||
val presentIds = languagesPresent.filterNotNull().toSet()
|
||||
allLanguages.filter { it.nameResId in presentIds }
|
||||
}
|
||||
|
||||
val allItems by vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList())
|
||||
val pairCounts = remember(allItems) {
|
||||
allItems.mapNotNull { item ->
|
||||
val first = item.languageFirstId
|
||||
val second = item.languageSecondId
|
||||
if (first != null && second != null) {
|
||||
val pair = if (first < second) first to second else second to first
|
||||
pair
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.groupingBy { it }.eachCount()
|
||||
}
|
||||
|
||||
val availablePairs = remember(pairCounts, allLanguages) {
|
||||
pairCounts.entries
|
||||
.sortedByDescending { it.value }
|
||||
.mapNotNull { (pairIds, _) ->
|
||||
val first = allLanguages.find { it.nameResId == pairIds.first }
|
||||
val second = allLanguages.find { it.nameResId == pairIds.second }
|
||||
if (first != null && second != null) first to second else null
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
SectionHeader(title = "Language Pair", actionText = "Change")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LanguageChip(
|
||||
text = "EN → ES",
|
||||
isSelected = selectedPair == 0,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { selectedPair = 0 }
|
||||
)
|
||||
LanguageChip(
|
||||
text = "EN → FR",
|
||||
isSelected = selectedPair == 1,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { selectedPair = 1 }
|
||||
SectionHeader(title = stringResource(R.string.language_pair))
|
||||
|
||||
if (availablePairs.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.text_no_dictionary_language_pairs_found),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
availablePairs.forEach { pair ->
|
||||
val isSelected = selectedPairs.contains(pair)
|
||||
LanguageChip(
|
||||
text = "${pair.first.name} → ${pair.second.name}",
|
||||
isSelected = isSelected,
|
||||
modifier = Modifier.widthIn(min = 160.dp),
|
||||
onClick = {
|
||||
val updated = if (isSelected) {
|
||||
selectedPairs - pair
|
||||
} else {
|
||||
selectedPairs + pair
|
||||
}
|
||||
onPairsChanged(updated)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_language_direction),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.text_language_direction_explanation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_origin_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedOriginLanguage,
|
||||
onLanguageSelected = onOriginLanguageSelected,
|
||||
showNoneOption = true,
|
||||
alternateLanguages = availableLanguages
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_target_language),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
eu.gaudian.translator.view.composable.SingleLanguageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
languageViewModel = languageViewModel,
|
||||
selectedLanguage = selectedTargetLanguage,
|
||||
onLanguageSelected = onTargetLanguageSelected,
|
||||
showNoneOption = true,
|
||||
alternateLanguages = availableLanguages
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,32 +492,93 @@ fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifie
|
||||
}
|
||||
}
|
||||
|
||||
//TODO uSe our real categories here
|
||||
//If there are too many categories (+15) use CategoryDropDown instead of FlowRow
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CategoriesSection() {
|
||||
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships")
|
||||
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) }
|
||||
fun CategoriesSection(
|
||||
selectedCategories: List<VocabularyCategory>,
|
||||
onCategoriesChanged: (List<VocabularyCategory>) -> Unit
|
||||
) {
|
||||
val activity = androidx.compose.ui.platform.LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
Column {
|
||||
SectionHeader(title = "Categories")
|
||||
SectionHeader(title = stringResource(R.string.label_categories))
|
||||
|
||||
val tagCategories = categories.filterIsInstance<TagCategory>()
|
||||
if (tagCategories.size > 15) {
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { selections ->
|
||||
onCategoriesChanged(selections.filterNotNull())
|
||||
},
|
||||
multipleSelectable = true,
|
||||
onlyLists = false,
|
||||
addCategory = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enableSearch = true
|
||||
)
|
||||
} else {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
tagCategories.forEach { category ->
|
||||
val isSelected = selectedCategories.contains(category)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
onClick = {
|
||||
val updated = if (isSelected) {
|
||||
selectedCategories - category
|
||||
} else {
|
||||
selectedCategories + category
|
||||
}
|
||||
onCategoriesChanged(updated)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = category.name,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Make it a flow row, as all stages in one row does not work
|
||||
@Composable
|
||||
fun DifficultySection(
|
||||
selectedStages: List<VocabularyStage>,
|
||||
onStagesChanged: (List<VocabularyStage>) -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
Column {
|
||||
SectionHeader(title = stringResource(R.string.label_filter_by_stage))
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
categories.forEach { category ->
|
||||
val isSelected = selectedCategories.contains(category)
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
val isSelected = selectedStages.contains(stage)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
onClick = {
|
||||
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
|
||||
val updated = if (isSelected) {
|
||||
selectedStages - stage
|
||||
} else {
|
||||
selectedStages + stage
|
||||
}
|
||||
onStagesChanged(updated)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = category,
|
||||
text = stage.toString(context),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -244,63 +590,46 @@ fun CategoriesSection() {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO use our real stages here
|
||||
//Make it a flow row, as all stages in one row does not work
|
||||
@Composable
|
||||
fun DifficultySection() {
|
||||
val stages = listOf("New", "Stage 1", "Stage 2", "Stage 3", "Stage 4", "Stage 5", "Learned")
|
||||
var selectedDifficulty by remember { mutableStateOf("Medium") }
|
||||
|
||||
fun NumberOfCardsSection(
|
||||
totalAvailable: Int,
|
||||
amount: Int,
|
||||
onAmountChanged: (Int) -> Unit,
|
||||
dueTodayOnly: Boolean,
|
||||
onDueTodayOnlyChanged: (Boolean) -> Unit
|
||||
) {
|
||||
Column {
|
||||
SectionHeader(title = "Difficulty Level")
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
stages.forEach { level ->
|
||||
val isSelected = selectedDifficulty == level
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||
.clickable { selectedDifficulty = level },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = level,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_due_today_only),
|
||||
description = stringResource(R.string.text_due_today_only_description),
|
||||
checked = dueTodayOnly,
|
||||
onCheckedChange = onDueTodayOnlyChanged
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (totalAvailable == 0) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_cards_found_for_the_selected_filters),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
return@Column
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO add optionitemswitch "Die Today Only", make sure that the amount of cards displayed is correct
|
||||
val maxAvailable = totalAvailable.coerceAtLeast(1)
|
||||
val coercedAmount = amount.coerceIn(1, maxAvailable)
|
||||
if (coercedAmount != amount) {
|
||||
onAmountChanged(coercedAmount)
|
||||
}
|
||||
|
||||
//TODO use the slider from ImportVocabulary dialog here, also with the quick selection of cards
|
||||
@Composable
|
||||
fun NumberOfCardsSection() {
|
||||
var sliderPosition by remember { mutableFloatStateOf(25f) }
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "NUMBER OF CARDS",
|
||||
text = stringResource(R.string.text_amount_of_cards).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
@@ -311,7 +640,7 @@ fun NumberOfCardsSection() {
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = sliderPosition.toInt().toString(),
|
||||
text = "$coercedAmount / $totalAvailable",
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -320,59 +649,73 @@ fun NumberOfCardsSection() {
|
||||
}
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = sliderPosition,
|
||||
onValueChange = { sliderPosition = it },
|
||||
valueRange = 5f..50f,
|
||||
steps = 45
|
||||
AppSlider(
|
||||
value = coercedAmount.toFloat(),
|
||||
onValueChange = { onAmountChanged(it.toInt().coerceIn(1, maxAvailable)) },
|
||||
valueRange = 1f..maxAvailable.toFloat(),
|
||||
steps = if (maxAvailable > 1) maxAvailable - 2 else 0,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("5 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text("50 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
val quickSelectValues = listOf(10, 25, 50, 100)
|
||||
val availableQuickSelections = quickSelectValues.filter { it <= maxAvailable }
|
||||
if (availableQuickSelections.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
availableQuickSelections.forEach { value ->
|
||||
AppOutlinedButton(
|
||||
onClick = { onAmountChanged(value) },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO use our four question types here, from StartScreen.kt
|
||||
//TODO use our four question types here, from StartScreen.kt, user must select at least one
|
||||
@Composable
|
||||
fun QuestionTypesSection() {
|
||||
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) }
|
||||
|
||||
fun QuestionTypesSection(
|
||||
selectedTypes: Set<VocabularyExerciseType>,
|
||||
onTypeSelected: (VocabularyExerciseType) -> Unit
|
||||
) {
|
||||
Column {
|
||||
SectionHeader(title = "Question Types")
|
||||
SectionHeader(title = stringResource(R.string.text_question_types))
|
||||
|
||||
QuestionTypeCard(
|
||||
title = "Multiple Choice",
|
||||
subtitle = "Choose the correct meaning",
|
||||
icon = Icons.Default.List,
|
||||
isSelected = selectedTypes.contains("Multiple Choice"),
|
||||
onClick = {
|
||||
selectedTypes = if (selectedTypes.contains("Multiple Choice")) selectedTypes - "Multiple Choice" else selectedTypes + "Multiple Choice"
|
||||
}
|
||||
title = stringResource(R.string.label_guessing_exercise),
|
||||
subtitle = stringResource(R.string.flip_card),
|
||||
icon = AppIcons.Guessing,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.GUESSING),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = "Spelling",
|
||||
subtitle = "Type the translated word",
|
||||
icon = Icons.Default.Edit,
|
||||
isSelected = selectedTypes.contains("Spelling"),
|
||||
onClick = {
|
||||
selectedTypes = if (selectedTypes.contains("Spelling")) selectedTypes - "Spelling" else selectedTypes + "Spelling"
|
||||
}
|
||||
title = stringResource(R.string.label_spelling_exercise),
|
||||
subtitle = stringResource(R.string.type_the_translation),
|
||||
icon = AppIcons.SpellCheck,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.SPELLING),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = "Listening",
|
||||
subtitle = "Recognize spoken words",
|
||||
icon = Icons.Default.Hearing,
|
||||
isSelected = selectedTypes.contains("Listening"),
|
||||
onClick = {
|
||||
selectedTypes = if (selectedTypes.contains("Listening")) selectedTypes - "Listening" else selectedTypes + "Listening"
|
||||
}
|
||||
title = stringResource(R.string.label_multiple_choice_exercise),
|
||||
subtitle = stringResource(R.string.label_choose_exercise_types),
|
||||
icon = AppIcons.CheckList,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
QuestionTypeCard(
|
||||
title = stringResource(R.string.label_word_jumble_exercise),
|
||||
subtitle = stringResource(R.string.text_assemble_the_word_here),
|
||||
icon = AppIcons.Extension,
|
||||
isSelected = selectedTypes.contains(VocabularyExerciseType.WORD_JUMBLE),
|
||||
onClick = { onTypeSelected(VocabularyExerciseType.WORD_JUMBLE) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -413,36 +756,88 @@ fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelec
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun BottomButtonSection() {
|
||||
fun BottomButtonSection(
|
||||
enabled: Boolean,
|
||||
amount: Int,
|
||||
onStart: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = { /* TODO: Start Session */ },
|
||||
AppButton(
|
||||
onClick = onStart,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
enabled = enabled,
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Start Session",
|
||||
text = stringResource(R.string.label_start_exercise_2d, amount),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play",
|
||||
contentDescription = stringResource(R.string.cd_play),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartExerciseSettingsBottomSheet(
|
||||
sheetState: SheetState,
|
||||
shuffleCards: Boolean,
|
||||
onShuffleCardsChanged: (Boolean) -> Unit,
|
||||
shuffleLanguages: Boolean,
|
||||
onShuffleLanguagesChanged: (Boolean) -> Unit,
|
||||
trainingMode: Boolean,
|
||||
onTrainingModeChanged: (Boolean) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.options),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.shuffle_cards),
|
||||
description = stringResource(R.string.text_shuffle_card_order_description),
|
||||
checked = shuffleCards,
|
||||
onCheckedChange = onShuffleCardsChanged
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.text_shuffle_languages),
|
||||
description = stringResource(R.string.text_shuffle_languages_description),
|
||||
checked = shuffleLanguages,
|
||||
onCheckedChange = onShuffleLanguagesChanged
|
||||
)
|
||||
OptionItemSwitch(
|
||||
title = stringResource(R.string.label_training_mode),
|
||||
description = stringResource(R.string.text_training_mode_description),
|
||||
checked = trainingMode,
|
||||
onCheckedChange = onTrainingModeChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -28,10 +27,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.Language
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||
import eu.gaudian.translator.viewmodel.ExerciseConfig
|
||||
import eu.gaudian.translator.viewmodel.ScreenState
|
||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
@@ -57,14 +54,7 @@ fun VocabularyExerciseHostScreen(
|
||||
|
||||
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
||||
val screenState by exerciseViewModel.screenState.collectAsState()
|
||||
|
||||
var shuffleCards by rememberSaveable { mutableStateOf(false) }
|
||||
var shuffleLanguages by remember { mutableStateOf(false) }
|
||||
var trainingMode by remember { mutableStateOf(false) }
|
||||
var dueTodayOnly by remember { mutableStateOf(false) }
|
||||
var selectedExerciseTypes by remember { mutableStateOf(setOf(VocabularyExerciseType.GUESSING)) }
|
||||
var selectedOriginLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
var selectedTargetLanguage by remember { mutableStateOf<Language?>(null) }
|
||||
val pendingConfig by exerciseViewModel.pendingExerciseConfig.collectAsState()
|
||||
|
||||
var finalScore by remember { mutableIntStateOf(0) }
|
||||
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
||||
@@ -76,16 +66,40 @@ fun VocabularyExerciseHostScreen(
|
||||
false
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Reset exercise state when starting fresh
|
||||
exerciseViewModel.resetExercise()
|
||||
|
||||
vocabularyViewModel.prepareExercise(
|
||||
categoryIdsAsJson,
|
||||
stageNamesAsJson,
|
||||
languageIdsAsJson,
|
||||
dailyOnly = dailyOnly,
|
||||
)
|
||||
LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
|
||||
// Only reset and prepare if the host is opened via explicit filters.
|
||||
if (!categoryIdsAsJson.isNullOrBlank() || !stageNamesAsJson.isNullOrBlank() || !languageIdsAsJson.isNullOrBlank() || dailyOnly) {
|
||||
exerciseViewModel.resetExercise()
|
||||
vocabularyViewModel.prepareExercise(
|
||||
categoryIdsAsJson,
|
||||
stageNamesAsJson,
|
||||
languageIdsAsJson,
|
||||
dailyOnly = dailyOnly,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cardSet, screenState, pendingConfig) {
|
||||
if (cardSet != null && screenState == ScreenState.START) {
|
||||
val items = cardSet?.cards.orEmpty()
|
||||
if (items.isNotEmpty()) {
|
||||
val selectedCount = pendingConfig.exerciseItemCount
|
||||
.takeIf { it > 0 }
|
||||
?: items.size
|
||||
val finalItems = if (pendingConfig.shuffleCards) {
|
||||
items.shuffled().take(selectedCount)
|
||||
} else {
|
||||
items.take(selectedCount)
|
||||
}
|
||||
exerciseViewModel.startExerciseWithConfig(
|
||||
finalItems,
|
||||
pendingConfig.copy(
|
||||
exerciseItemCount = finalItems.size,
|
||||
originalExerciseItems = finalItems
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cardSet == null && screenState != ScreenState.START) {
|
||||
@@ -95,68 +109,9 @@ fun VocabularyExerciseHostScreen(
|
||||
} else {
|
||||
when (screenState) {
|
||||
ScreenState.START -> {
|
||||
StartScreen(
|
||||
cardSet = cardSet,
|
||||
onStartClicked = { finalItems ->
|
||||
exerciseViewModel.startExerciseWithConfig(
|
||||
finalItems,
|
||||
ExerciseConfig(
|
||||
shuffleCards = shuffleCards,
|
||||
shuffleLanguages = shuffleLanguages,
|
||||
trainingMode = trainingMode,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
selectedExerciseTypes = selectedExerciseTypes,
|
||||
exerciseItemCount = finalItems.size,
|
||||
originalExerciseItems = finalItems,
|
||||
originLanguageId = selectedOriginLanguage?.nameResId,
|
||||
targetLanguageId = selectedTargetLanguage?.nameResId
|
||||
)
|
||||
)
|
||||
},
|
||||
onClose = onClose,
|
||||
shuffleCards = shuffleCards,
|
||||
onShuffleCardsChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
shuffleCards = it
|
||||
},
|
||||
shuffleLanguages = shuffleLanguages,
|
||||
onShuffleLanguagesChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
shuffleLanguages = it
|
||||
},
|
||||
trainingMode = trainingMode,
|
||||
onTrainingModeChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
trainingMode = it
|
||||
exerciseViewModel.onTrainingModeChanged(it)
|
||||
},
|
||||
hideTodayOnlySwitch = dailyOnly,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
onDueTodayOnlyChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
dueTodayOnly = it
|
||||
},
|
||||
selectedExerciseTypes = selectedExerciseTypes,
|
||||
onExerciseTypeSelected = { type ->
|
||||
val currentTypes = selectedExerciseTypes.toMutableSet()
|
||||
if (type in currentTypes) {
|
||||
if (currentTypes.size > 1) currentTypes.remove(type)
|
||||
} else {
|
||||
currentTypes.add(type)
|
||||
}
|
||||
selectedExerciseTypes = currentTypes
|
||||
},
|
||||
selectedOriginLanguage = selectedOriginLanguage,
|
||||
onOriginLanguageChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedOriginLanguage = it
|
||||
},
|
||||
selectedTargetLanguage = selectedTargetLanguage,
|
||||
onTargetLanguageChanged = {
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedTargetLanguage = it
|
||||
}
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
ScreenState.EXERCISE -> {
|
||||
ExerciseScreen(
|
||||
|
||||
@@ -90,6 +90,9 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
|
||||
// Exercise configuration state
|
||||
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||
private val _pendingExerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||
|
||||
val pendingExerciseConfig: StateFlow<ExerciseConfig> = _pendingExerciseConfig.asStateFlow()
|
||||
|
||||
// Exercise results state
|
||||
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
||||
@@ -399,12 +402,17 @@ class VocabularyExerciseViewModel @Inject constructor(
|
||||
config: ExerciseConfig
|
||||
) {
|
||||
_exerciseConfig.value = config
|
||||
_pendingExerciseConfig.value = config
|
||||
_totalItems.value = items.size
|
||||
_originalItems.value = items
|
||||
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
||||
_screenState.value = ScreenState.EXERCISE
|
||||
}
|
||||
|
||||
fun updatePendingExerciseConfig(config: ExerciseConfig) {
|
||||
_pendingExerciseConfig.value = config
|
||||
}
|
||||
|
||||
fun finishExercise(score: Int, wrongAnswers: Int) {
|
||||
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
||||
_screenState.value = ScreenState.RESULT
|
||||
|
||||
@@ -617,6 +617,56 @@ class VocabularyViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun filterVocabularyItemsByPairs(
|
||||
languagePairs: List<Pair<Int, Int>>?,
|
||||
query: String?,
|
||||
categoryIds: List<Int>?,
|
||||
stages: List<VocabularyStage>?,
|
||||
wordClass: String? = null,
|
||||
dueTodayOnly: Boolean = false,
|
||||
sortOrder: SortOrder
|
||||
): Flow<List<VocabularyItem>> {
|
||||
val baseFlow = filterVocabularyItems(
|
||||
languages = null,
|
||||
query = query,
|
||||
categoryIds = categoryIds,
|
||||
stage = null,
|
||||
wordClass = wordClass,
|
||||
dueTodayOnly = dueTodayOnly,
|
||||
sortOrder = sortOrder
|
||||
)
|
||||
|
||||
val normalizedPairs = languagePairs
|
||||
?.map { pair -> if (pair.first < pair.second) pair else pair.second to pair.first }
|
||||
?.toSet()
|
||||
|
||||
return combine(baseFlow, stageMapping) { items, stageMap ->
|
||||
var filteredItems = items
|
||||
|
||||
if (!normalizedPairs.isNullOrEmpty()) {
|
||||
filteredItems = filteredItems.filter { item ->
|
||||
val firstId = item.languageFirstId
|
||||
val secondId = item.languageSecondId
|
||||
if (firstId == null || secondId == null) {
|
||||
false
|
||||
} else {
|
||||
val normalizedPair = if (firstId < secondId) firstId to secondId else secondId to firstId
|
||||
normalizedPairs.contains(normalizedPair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stages.isNullOrEmpty()) {
|
||||
filteredItems = filteredItems.filter { item ->
|
||||
val stage = stageMap[item.id] ?: VocabularyStage.NEW
|
||||
stage in stages
|
||||
}
|
||||
}
|
||||
|
||||
filteredItems
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun generateVocabularyItems(category: String, amount: Int) {
|
||||
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
||||
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
||||
<string name="number_of_cards">Anzahl der Karten: %1$d / %2$d</string>
|
||||
<string name="no_cards_found_for_the_selected_filters">Keine Karten für die gewählten Filter gefunden.</string>
|
||||
<string name="label_choose_exercise_types">Übungstypen wählen</string>
|
||||
<string name="label_choose_exercise_types">Die richtige Antwort wählen</string>
|
||||
<string name="options">Optionen</string>
|
||||
<string name="shuffle_cards">Karten mischen</string>
|
||||
<string name="quit">Beenden</string>
|
||||
@@ -338,7 +338,7 @@
|
||||
<string name="statistics_are_loading">Statistiken werden geladen…</string>
|
||||
<string name="to_d">nach %1$s</string>
|
||||
<string name="label_translate_from_2d">Übersetze von %1$s</string>
|
||||
<string name="text_assemble_the_word_here">Bilde das Wort hier</string>
|
||||
<string name="text_assemble_the_word_here">Bringe die Buchstaben in Reihenfolge</string>
|
||||
<string name="correct_answer">Richtige Antwort: %1$s</string>
|
||||
<string name="label_quit_exercise_qm">Übung beenden?</string>
|
||||
<string name="text_are_you_sure_you_want_to_quit_your_progress_in_this_session_will_be_lost">Möchtest du wirklich beenden? Dein Fortschritt in dieser Sitzung geht verloren.</string>
|
||||
@@ -680,7 +680,7 @@
|
||||
<string name="label_language_direction">Sprachenrichtung
|
||||
</string>
|
||||
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</string>
|
||||
<string name="label_guessing_exercise">Vermutung</string>
|
||||
<string name="label_guessing_exercise">Raten</string>
|
||||
<string name="label_spelling_exercise">Rechtschreibung</string>
|
||||
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
||||
<string name="label_word_jumble_exercise">Wortwirrwarr</string>
|
||||
|
||||
Reference in New Issue
Block a user