implement LibraryScreen, migrate Vocabulary to legacy, and refactor StartExerciseScreen UI

This commit is contained in:
jonasgaudian
2026-02-16 14:28:28 +01:00
parent 5ae96d1f5c
commit 972b2226d0
5 changed files with 447 additions and 5 deletions

View File

@@ -30,6 +30,7 @@ import eu.gaudian.translator.view.exercises.StartExerciseScreen
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
import eu.gaudian.translator.view.home.HomeScreen import eu.gaudian.translator.view.home.HomeScreen
import eu.gaudian.translator.view.library.LibraryScreen
import eu.gaudian.translator.view.settings.DictionaryOptionsScreen import eu.gaudian.translator.view.settings.DictionaryOptionsScreen
import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.settings.SettingsRoutes
import eu.gaudian.translator.view.settings.TranslationSettingsScreen import eu.gaudian.translator.view.settings.TranslationSettingsScreen
@@ -61,6 +62,7 @@ fun AppNavHost(
// 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs) // 1. Define your Main Tab "Leaf" Routes (the actual start destinations of your graphs)
val mainTabRoutes = setOf( val mainTabRoutes = setOf(
Screen.Home.route, Screen.Home.route,
Screen.Library.route,
Screen.Stats.route, Screen.Stats.route,
Screen.Translation.route, Screen.Translation.route,
Screen.Vocabulary.route, Screen.Vocabulary.route,
@@ -132,6 +134,10 @@ fun AppNavHost(
StatsScreen(navController = navController) StatsScreen(navController = navController)
} }
composable(Screen.Library.route) {
LibraryScreen(navController = navController)
}
composable("start_exercise") { composable("start_exercise") {
StartExerciseScreen(navController = navController) StartExerciseScreen(navController = navController)
} }

View File

@@ -73,7 +73,8 @@ sealed class Screen(
object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home) object Home : Screen("home", R.string.label_home, AppIcons.Home, AppIcons.Home)
object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics) object Stats : Screen("stats", R.string.label_stats, AppIcons.Statistics, AppIcons.Statistics)
object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined) object Translation : Screen("translation", R.string.label_translation, AppIcons.TranslateFilled, AppIcons.TranslateOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined) object Library : Screen("library", R.string.label_library, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Vocabulary : Screen("vocabulary", R.string.label_legacy_vocabulary, AppIcons.VocabularyFilled, AppIcons.VocabularyOutlined)
object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined) object Settings : Screen("settings", R.string.title_settings, AppIcons.SettingsFilled, AppIcons.SettingsOutlined)
object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreVert) object More : Screen("more", R.string.label_more, AppIcons.MoreVert, AppIcons.MoreVert)
object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined) object Dictionary : Screen("dictionary", R.string.label_dictionary, AppIcons.DictionaryFilled, AppIcons.DictionaryOutlined)
@@ -81,12 +82,13 @@ sealed class Screen(
companion object { companion object {
fun getAllScreens(showExperimental: Boolean = false): List<Screen> { fun getAllScreens(showExperimental: Boolean = false): List<Screen> {
return listOf(Home, Vocabulary, Stats) return listOf(Home, Library, Stats)
} }
fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> { fun getMoreMenuItems(showExperimental: Boolean = false): List<Screen> {
val items = mutableListOf<Screen>() val items = mutableListOf<Screen>()
items.add(Translation) items.add(Translation)
items.add(Vocabulary) // Legacy vocabulary moved to More
items.add(Dictionary) items.add(Dictionary)
items.add(Settings) items.add(Settings)
if (showExperimental) { if (showExperimental) {

View File

@@ -1,12 +1,56 @@
package eu.gaudian.translator.view.exercises package eu.gaudian.translator.view.exercises
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
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.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.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@Composable @Composable
@@ -16,11 +60,373 @@ fun StartExerciseScreen(
) { ) {
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.widthIn(max = 700.dp) // Keeps it from over-stretching on tablets
.fillMaxSize()
) {
TopBarSection(onBackClick = { navController.popBackStack() })
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
item { LanguagePairSection() }
item { CategoriesSection() }
item { DifficultySection() }
item { NumberOfCardsSection() }
item { QuestionTypesSection() }
item { Spacer(modifier = Modifier.height(24.dp)) }
}
BottomButtonSection()
}
}
}
@Composable
fun TopBarSection(onBackClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onBackClick,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back",
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
}
Text(
text = "Start Exercise",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
// Spacer to balance the back button for centering
Spacer(modifier = Modifier.size(48.dp))
}
}
@Composable
fun SectionHeader(title: String, actionText: String? = null, onActionClick: () -> Unit = {}) {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (actionText != null) {
Text(
text = actionText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { onActionClick() }
)
}
}
}
@Composable
fun LanguagePairSection() {
var selectedPair by remember { mutableStateOf(0) }
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 }
)
}
}
}
@Composable
fun LanguageChip(text: String, isSelected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
modifier = modifier.height(56.dp),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
// Dummy overlapping flags
Box(modifier = Modifier.width(32.dp)) {
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Red).align(Alignment.CenterStart))
Box(modifier = Modifier.size(20.dp).clip(CircleShape).background(Color.Blue).align(Alignment.CenterEnd))
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoriesSection() {
val categories = listOf("Travel", "Business", "Food", "Technology", "Slang", "Academic", "Relationships")
var selectedCategories by remember { mutableStateOf(setOf("Travel", "Food")) }
Column {
SectionHeader(title = "Categories")
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
categories.forEach { category ->
val isSelected = selectedCategories.contains(category)
Surface(
shape = RoundedCornerShape(20.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
onClick = {
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
}
) {
Text(
text = category,
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
)
}
}
}
}
}
@Composable
fun DifficultySection() {
val difficulties = listOf("Easy", "Medium", "Hard")
var selectedDifficulty by remember { mutableStateOf("Medium") }
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
) {
difficulties.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 contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "Start Exercise Screen", text = level,
style = MaterialTheme.typography.headlineMedium color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
@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",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primary,
) {
Text(
text = sliderPosition.toInt().toString(),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 5f..50f,
steps = 45
)
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)
}
}
}
@Composable
fun QuestionTypesSection() {
var selectedTypes by remember { mutableStateOf(setOf("Multiple Choice", "Spelling")) }
Column {
SectionHeader(title = "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"
}
)
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"
}
)
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"
}
) )
} }
} }
@Composable
fun QuestionTypeCard(title: String, subtitle: String, icon: ImageVector, isSelected: Boolean, onClick: () -> Unit) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)) else null,
onClick = onClick
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Icon(imageVector = icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Icon(
imageVector = if (isSelected) Icons.Default.CheckCircle else Icons.Outlined.Circle,
contentDescription = null,
tint = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
fun BottomButtonSection() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Button(
onClick = { /* TODO: Start Session */ },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Start Session",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Play",
modifier = Modifier.size(20.dp)
)
}
}
}
}

View File

@@ -0,0 +1,26 @@
package eu.gaudian.translator.view.library
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
@Composable
fun LibraryScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Library Screen",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -1116,4 +1116,6 @@
<string name="message_test_success">This is a test success message!</string> <string name="message_test_success">This is a test success message!</string>
<string name="message_test_error">Oops, something went wrong :(</string> <string name="message_test_error">Oops, something went wrong :(</string>
<string name="label_stats">Stats</string> <string name="label_stats">Stats</string>
<string name="label_library">Library</string>
<string name="label_legacy_vocabulary">Legacy Vocabulary</string>
</resources> </resources>