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.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
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.height
|
||||||
@@ -24,40 +23,106 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
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.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.outlined.Circle
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
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
|
@Composable
|
||||||
fun StartExerciseScreen(
|
fun StartExerciseScreen(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier
|
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(
|
Box(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
@@ -67,7 +132,15 @@ fun StartExerciseScreen(
|
|||||||
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
|
||||||
.fillMaxSize()
|
.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(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -76,27 +149,107 @@ fun StartExerciseScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
) {
|
) {
|
||||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||||
item { LanguagePairSection() }
|
item {
|
||||||
item { CategoriesSection() }
|
LanguagePairSection(
|
||||||
item { DifficultySection() }
|
selectedPairs = selectedLanguagePairs,
|
||||||
item { NumberOfCardsSection() }
|
onPairsChanged = { selectedLanguagePairs = it },
|
||||||
item { QuestionTypesSection() }
|
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)) }
|
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
|
@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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -118,15 +271,45 @@ fun TopBarSection(onBackClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Start Exercise",
|
text = stringResource(R.string.label_start_exercise),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
// Spacer to balance the back button for centering
|
IconButton(
|
||||||
Spacer(modifier = Modifier.size(48.dp))
|
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,31 +339,133 @@ 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
|
@Composable
|
||||||
fun LanguagePairSection() {
|
fun LanguagePairSection(
|
||||||
var selectedPair by remember { mutableStateOf(0) }
|
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 {
|
Column {
|
||||||
SectionHeader(title = "Language Pair", actionText = "Change")
|
SectionHeader(title = stringResource(R.string.language_pair))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
||||||
LanguageChip(
|
if (availablePairs.isEmpty()) {
|
||||||
text = "EN → ES",
|
Text(
|
||||||
isSelected = selectedPair == 0,
|
text = stringResource(R.string.text_no_dictionary_language_pairs_found),
|
||||||
modifier = Modifier.weight(1f),
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
onClick = { selectedPair = 0 }
|
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(
|
LanguageChip(
|
||||||
text = "EN → FR",
|
text = "${pair.first.name} → ${pair.second.name}",
|
||||||
isSelected = selectedPair == 1,
|
isSelected = isSelected,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.widthIn(min = 160.dp),
|
||||||
onClick = { selectedPair = 1 }
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
@@ -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)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoriesSection() {
|
fun CategoriesSection(
|
||||||
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships")
|
selectedCategories: List<VocabularyCategory>,
|
||||||
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) }
|
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 {
|
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(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
categories.forEach { category ->
|
tagCategories.forEach { category ->
|
||||||
val isSelected = selectedCategories.contains(category)
|
val isSelected = selectedCategories.contains(category)
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
|
val updated = if (isSelected) {
|
||||||
|
selectedCategories - category
|
||||||
|
} else {
|
||||||
|
selectedCategories + category
|
||||||
|
}
|
||||||
|
onCategoriesChanged(updated)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = category,
|
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)
|
||||||
|
) {
|
||||||
|
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 = {
|
||||||
|
val updated = if (isSelected) {
|
||||||
|
selectedStages - stage
|
||||||
|
} else {
|
||||||
|
selectedStages + stage
|
||||||
|
}
|
||||||
|
onStagesChanged(updated)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stage.toString(context),
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
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
|
@Composable
|
||||||
fun DifficultySection() {
|
fun NumberOfCardsSection(
|
||||||
val stages = listOf("New", "Stage 1", "Stage 2", "Stage 3", "Stage 4", "Stage 5", "Learned")
|
totalAvailable: Int,
|
||||||
var selectedDifficulty by remember { mutableStateOf("Medium") }
|
amount: Int,
|
||||||
|
onAmountChanged: (Int) -> Unit,
|
||||||
|
dueTodayOnly: Boolean,
|
||||||
|
onDueTodayOnlyChanged: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
Column {
|
Column {
|
||||||
SectionHeader(title = "Difficulty Level")
|
OptionItemSwitch(
|
||||||
Surface(
|
title = stringResource(R.string.text_due_today_only),
|
||||||
shape = RoundedCornerShape(50),
|
description = stringResource(R.string.text_due_today_only_description),
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
checked = dueTodayOnly,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp)
|
onCheckedChange = onDueTodayOnlyChanged
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "NUMBER OF CARDS",
|
text = stringResource(R.string.text_amount_of_cards).uppercase(),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
letterSpacing = 1.sp,
|
letterSpacing = 1.sp,
|
||||||
@@ -311,7 +640,7 @@ fun NumberOfCardsSection() {
|
|||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = sliderPosition.toInt().toString(),
|
text = "$coercedAmount / $totalAvailable",
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -320,59 +649,73 @@ fun NumberOfCardsSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider(
|
AppSlider(
|
||||||
value = sliderPosition,
|
value = coercedAmount.toFloat(),
|
||||||
onValueChange = { sliderPosition = it },
|
onValueChange = { onAmountChanged(it.toInt().coerceIn(1, maxAvailable)) },
|
||||||
valueRange = 5f..50f,
|
valueRange = 1f..maxAvailable.toFloat(),
|
||||||
steps = 45
|
steps = if (maxAvailable > 1) maxAvailable - 2 else 0,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val quickSelectValues = listOf(10, 25, 50, 100)
|
||||||
|
val availableQuickSelections = quickSelectValues.filter { it <= maxAvailable }
|
||||||
|
if (availableQuickSelections.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text("5 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
availableQuickSelections.forEach { value ->
|
||||||
Text("50 CARDS", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
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
|
@Composable
|
||||||
fun QuestionTypesSection() {
|
fun QuestionTypesSection(
|
||||||
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) }
|
selectedTypes: Set<VocabularyExerciseType>,
|
||||||
|
onTypeSelected: (VocabularyExerciseType) -> Unit
|
||||||
|
) {
|
||||||
Column {
|
Column {
|
||||||
SectionHeader(title = "Question Types")
|
SectionHeader(title = stringResource(R.string.text_question_types))
|
||||||
|
|
||||||
QuestionTypeCard(
|
QuestionTypeCard(
|
||||||
title = "Multiple Choice",
|
title = stringResource(R.string.label_guessing_exercise),
|
||||||
subtitle = "Choose the correct meaning",
|
subtitle = stringResource(R.string.flip_card),
|
||||||
icon = Icons.Default.List,
|
icon = AppIcons.Guessing,
|
||||||
isSelected = selectedTypes.contains("Multiple Choice"),
|
isSelected = selectedTypes.contains(VocabularyExerciseType.GUESSING),
|
||||||
onClick = {
|
onClick = { onTypeSelected(VocabularyExerciseType.GUESSING) }
|
||||||
selectedTypes = if (selectedTypes.contains("Multiple Choice")) selectedTypes - "Multiple Choice" else selectedTypes + "Multiple Choice"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
QuestionTypeCard(
|
QuestionTypeCard(
|
||||||
title = "Spelling",
|
title = stringResource(R.string.label_spelling_exercise),
|
||||||
subtitle = "Type the translated word",
|
subtitle = stringResource(R.string.type_the_translation),
|
||||||
icon = Icons.Default.Edit,
|
icon = AppIcons.SpellCheck,
|
||||||
isSelected = selectedTypes.contains("Spelling"),
|
isSelected = selectedTypes.contains(VocabularyExerciseType.SPELLING),
|
||||||
onClick = {
|
onClick = { onTypeSelected(VocabularyExerciseType.SPELLING) }
|
||||||
selectedTypes = if (selectedTypes.contains("Spelling")) selectedTypes - "Spelling" else selectedTypes + "Spelling"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
QuestionTypeCard(
|
QuestionTypeCard(
|
||||||
title = "Listening",
|
title = stringResource(R.string.label_multiple_choice_exercise),
|
||||||
subtitle = "Recognize spoken words",
|
subtitle = stringResource(R.string.label_choose_exercise_types),
|
||||||
icon = Icons.Default.Hearing,
|
icon = AppIcons.CheckList,
|
||||||
isSelected = selectedTypes.contains("Listening"),
|
isSelected = selectedTypes.contains(VocabularyExerciseType.MULTIPLE_CHOICE),
|
||||||
onClick = {
|
onClick = { onTypeSelected(VocabularyExerciseType.MULTIPLE_CHOICE) }
|
||||||
selectedTypes = if (selectedTypes.contains("Listening")) selectedTypes - "Listening" else selectedTypes + "Listening"
|
)
|
||||||
}
|
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
|
@Composable
|
||||||
fun BottomButtonSection() {
|
fun BottomButtonSection(
|
||||||
|
enabled: Boolean,
|
||||||
|
amount: Int,
|
||||||
|
onStart: () -> Unit
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(24.dp)
|
.padding(24.dp)
|
||||||
) {
|
) {
|
||||||
Button(
|
AppButton(
|
||||||
onClick = { /* TODO: Start Session */ },
|
onClick = onStart,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp),
|
.height(56.dp),
|
||||||
shape = RoundedCornerShape(28.dp),
|
enabled = enabled,
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
shape = RoundedCornerShape(28.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Start Session",
|
text = stringResource(R.string.label_start_exercise_2d, amount),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.PlayArrow,
|
imageVector = Icons.Default.PlayArrow,
|
||||||
contentDescription = "Play",
|
contentDescription = stringResource(R.string.cd_play),
|
||||||
modifier = Modifier.size(20.dp)
|
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.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -28,10 +27,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.Language
|
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||||
import eu.gaudian.translator.viewmodel.ExerciseConfig
|
|
||||||
import eu.gaudian.translator.viewmodel.ScreenState
|
import eu.gaudian.translator.viewmodel.ScreenState
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyExerciseViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
@@ -57,14 +54,7 @@ fun VocabularyExerciseHostScreen(
|
|||||||
|
|
||||||
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
val cardSet by vocabularyViewModel.cardSet.collectAsState()
|
||||||
val screenState by exerciseViewModel.screenState.collectAsState()
|
val screenState by exerciseViewModel.screenState.collectAsState()
|
||||||
|
val pendingConfig by exerciseViewModel.pendingExerciseConfig.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) }
|
|
||||||
|
|
||||||
var finalScore by remember { mutableIntStateOf(0) }
|
var finalScore by remember { mutableIntStateOf(0) }
|
||||||
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
var finalWrongAnswers by remember { mutableIntStateOf(0) }
|
||||||
@@ -76,10 +66,10 @@ fun VocabularyExerciseHostScreen(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(categoryIdsAsJson, stageNamesAsJson, languageIdsAsJson, dailyOnly) {
|
||||||
// Reset exercise state when starting fresh
|
// Only reset and prepare if the host is opened via explicit filters.
|
||||||
|
if (!categoryIdsAsJson.isNullOrBlank() || !stageNamesAsJson.isNullOrBlank() || !languageIdsAsJson.isNullOrBlank() || dailyOnly) {
|
||||||
exerciseViewModel.resetExercise()
|
exerciseViewModel.resetExercise()
|
||||||
|
|
||||||
vocabularyViewModel.prepareExercise(
|
vocabularyViewModel.prepareExercise(
|
||||||
categoryIdsAsJson,
|
categoryIdsAsJson,
|
||||||
stageNamesAsJson,
|
stageNamesAsJson,
|
||||||
@@ -87,6 +77,30 @@ fun VocabularyExerciseHostScreen(
|
|||||||
dailyOnly = dailyOnly,
|
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) {
|
if (cardSet == null && screenState != ScreenState.START) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
@@ -95,68 +109,9 @@ fun VocabularyExerciseHostScreen(
|
|||||||
} else {
|
} else {
|
||||||
when (screenState) {
|
when (screenState) {
|
||||||
ScreenState.START -> {
|
ScreenState.START -> {
|
||||||
StartScreen(
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
cardSet = cardSet,
|
CircularProgressIndicator()
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ScreenState.EXERCISE -> {
|
ScreenState.EXERCISE -> {
|
||||||
ExerciseScreen(
|
ExerciseScreen(
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Exercise configuration state
|
// Exercise configuration state
|
||||||
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
private val _exerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||||
|
private val _pendingExerciseConfig = MutableStateFlow(ExerciseConfig())
|
||||||
|
|
||||||
|
val pendingExerciseConfig: StateFlow<ExerciseConfig> = _pendingExerciseConfig.asStateFlow()
|
||||||
|
|
||||||
// Exercise results state
|
// Exercise results state
|
||||||
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
private val _exerciseResults = MutableStateFlow(ExerciseResults())
|
||||||
@@ -399,12 +402,17 @@ class VocabularyExerciseViewModel @Inject constructor(
|
|||||||
config: ExerciseConfig
|
config: ExerciseConfig
|
||||||
) {
|
) {
|
||||||
_exerciseConfig.value = config
|
_exerciseConfig.value = config
|
||||||
|
_pendingExerciseConfig.value = config
|
||||||
_totalItems.value = items.size
|
_totalItems.value = items.size
|
||||||
_originalItems.value = items
|
_originalItems.value = items
|
||||||
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
startExercise(items, config.selectedExerciseTypes, config.shuffleLanguages)
|
||||||
_screenState.value = ScreenState.EXERCISE
|
_screenState.value = ScreenState.EXERCISE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updatePendingExerciseConfig(config: ExerciseConfig) {
|
||||||
|
_pendingExerciseConfig.value = config
|
||||||
|
}
|
||||||
|
|
||||||
fun finishExercise(score: Int, wrongAnswers: Int) {
|
fun finishExercise(score: Int, wrongAnswers: Int) {
|
||||||
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
_exerciseResults.value = ExerciseResults(score, wrongAnswers)
|
||||||
_screenState.value = ScreenState.RESULT
|
_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) {
|
suspend fun generateVocabularyItems(category: String, amount: Int) {
|
||||||
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
val selectedSourceLanguage = languageRepository.loadSelectedSourceLanguage().first()
|
||||||
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
val selectedTargetLanguage = languageRepository.loadSelectedTargetLanguage().first()
|
||||||
|
|||||||
@@ -292,7 +292,7 @@
|
|||||||
<string name="label_start_exercise_2d">Übung starten (%1$d)</string>
|
<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="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="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="options">Optionen</string>
|
||||||
<string name="shuffle_cards">Karten mischen</string>
|
<string name="shuffle_cards">Karten mischen</string>
|
||||||
<string name="quit">Beenden</string>
|
<string name="quit">Beenden</string>
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
<string name="statistics_are_loading">Statistiken werden geladen…</string>
|
<string name="statistics_are_loading">Statistiken werden geladen…</string>
|
||||||
<string name="to_d">nach %1$s</string>
|
<string name="to_d">nach %1$s</string>
|
||||||
<string name="label_translate_from_2d">Übersetze von %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="correct_answer">Richtige Antwort: %1$s</string>
|
||||||
<string name="label_quit_exercise_qm">Übung beenden?</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>
|
<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 name="label_language_direction">Sprachenrichtung
|
||||||
</string>
|
</string>
|
||||||
<string name="text_language_direction_explanation">Du kannst eine optionale Einstellung vornehmen, welche Sprache zuerst oder zweit kommen soll.</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_spelling_exercise">Rechtschreibung</string>
|
||||||
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
<string name="label_multiple_choice_exercise">Multiple Choice</string>
|
||||||
<string name="label_word_jumble_exercise">Wortwirrwarr</string>
|
<string name="label_word_jumble_exercise">Wortwirrwarr</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user