Add dummy start exercise button and dummy screen

This commit is contained in:
jonasgaudian
2026-02-16 13:52:02 +01:00
parent ef90df2150
commit 5ae96d1f5c
4 changed files with 189 additions and 94 deletions

View File

@@ -285,6 +285,16 @@ fun TranslatorApp(
restoreState = false
}
}
},
onPlayClicked = {
navController.navigate("start_exercise") {
popUpTo(0) {
inclusive = true
saveState = false
}
launchSingleTop = true
restoreState = false
}
}
)
},

View File

@@ -26,6 +26,7 @@ import eu.gaudian.translator.view.dictionary.EtymologyResultScreen
import eu.gaudian.translator.view.dictionary.MainDictionaryScreen
import eu.gaudian.translator.view.exercises.ExerciseSessionScreen
import eu.gaudian.translator.view.exercises.MainExerciseScreen
import eu.gaudian.translator.view.exercises.StartExerciseScreen
import eu.gaudian.translator.view.exercises.YouTubeBrowserScreen
import eu.gaudian.translator.view.exercises.YouTubeExerciseScreen
import eu.gaudian.translator.view.home.HomeScreen
@@ -131,6 +132,10 @@ fun AppNavHost(
StatsScreen(navController = navController)
}
composable("start_exercise") {
StartExerciseScreen(navController = navController)
}
// Define all other navigation graphs at the same top level.
homeGraph(navController)
translationGraph(navController)

View File

@@ -21,8 +21,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -40,7 +45,10 @@ 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.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
@@ -106,6 +114,7 @@ fun BottomNavigationBar(
showLabels: Boolean,
onItemSelected: (Screen) -> Unit,
modifier: Modifier = Modifier,
onPlayClicked: () -> Unit = {}
) {
val showExperimental = LocalShowExperimentalFeatures.current
val screens = remember(showExperimental) { Screen.getAllScreens(showExperimental) }
@@ -115,14 +124,22 @@ fun BottomNavigationBar(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
// Configuration for the play button
val playButtonSize = 56.dp
val glowPadding = 32.dp // Total extra space for the glow (16dp on each side)
// This dictates how far up the button shifts.
// Setting it to around half the button size centers it on the top border.
val upwardOffset = 28.dp
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
initialOffsetY = { it }
),
exit = slideOutVertically(
animationSpec = androidx.compose.animation.core.tween(durationMillis = 220),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetOffsetY = { it }
)
) {
@@ -131,109 +148,146 @@ fun BottomNavigationBar(
val navBarDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
val height = baseHeight + navBarDp
NavigationBar(
modifier = modifier.height(height),
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp,
// Outer Box height is purely determined by the NavigationBar now
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.TopCenter
) {
screens.forEach { screen ->
val isSelected = screen == selectedItem
val title = stringResource(id = screen.title)
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.2f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "iconScale"
)
// The actual Navigation Bar
NavigationBar(
modifier = Modifier.height(height),
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp,
) {
// Create a list of 5 items (2 left, 1 empty spacer, 2 right)
val allNavItems = buildList {
addAll(screens.take(2))
add(null) // Empty spacer for Play Button gap
if (screens.size > 2) {
addAll(screens.drop(2))
}
add(moreScreen)
}
NavigationBarItem(
selected = isSelected,
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onItemSelected(screen)
},
label = if (showLabels) {
{
Text(
text = title,
maxLines = 1,
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
allNavItems.forEach { screen ->
if (screen == null) {
// Dummy item to create the gap
NavigationBarItem(
selected = false,
onClick = {},
enabled = false, // Disables ripples and clicks
icon = { Spacer(modifier = Modifier.size(24.dp)) },
label = if (showLabels) { { Spacer(modifier = Modifier.size(10.dp)) } } else null,
colors = NavigationBarItemDefaults.colors(
disabledIconColor = Color.Transparent,
disabledTextColor = Color.Transparent
)
)
} else {
// Regular or More items
val isSelected = if (screen == Screen.More) {
selectedItem is Screen.More || Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
} else {
screen == selectedItem
}
} else null,
icon = {
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
Icon(
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
contentDescription = title,
modifier = Modifier.scale(scale)
val title = stringResource(id = screen.title)
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.2f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "iconScale"
)
NavigationBarItem(
selected = isSelected,
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
if (screen == Screen.More) showMoreMenu = true else onItemSelected(screen)
},
label = if (showLabels) {
{
Text(
text = title,
maxLines = 1,
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if(isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else null,
icon = {
Crossfade(targetState = isSelected, label = "iconFade") { selected ->
Icon(
imageVector = if (selected) screen.selectedIcon else screen.unselectedIcon,
contentDescription = title,
modifier = Modifier.scale(scale)
)
}
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
}
// More menu item
val moreSelected = selectedItem is Screen.More ||
Screen.getMoreMenuItems(showExperimental).contains(selectedItem)
val moreTitle = stringResource(R.string.label_more)
val moreScale by animateFloatAsState(
targetValue = if (moreSelected) 1.2f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "moreIconScale"
)
NavigationBarItem(
selected = moreSelected,
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showMoreMenu = true
},
label = if (showLabels) {
{
Text(
text = moreTitle,
maxLines = 1,
fontSize = 10.sp,
fontWeight = if (moreSelected) FontWeight.Bold else FontWeight.Normal,
color = if(moreSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else null,
icon = {
Icon(
imageVector = moreScreen.selectedIcon,
contentDescription = moreTitle,
modifier = Modifier.scale(moreScale)
)
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
}
}
// The Glowing Play Button
Box(
modifier = Modifier
// This negative offset pulls the button UP out of the bounding box
// without increasing the layout height of the parent Box.
.offset(y = -upwardOffset)
.size(playButtonSize + glowPadding),
contentAlignment = Alignment.Center
) {
// Background radial glow
Box(
modifier = Modifier
.matchParentSize()
.background(
brush = Brush.radialGradient(
colors = listOf(
Color(0xFF3B82F6).copy(alpha = 0.5f),
Color.Transparent
)
),
shape = CircleShape
)
)
)
// Actual clickable button
Box(
modifier = Modifier
.size(playButtonSize)
.clip(CircleShape)
.background(Color(0xFF3B82F6))
.clickable {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onPlayClicked()
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Play",
tint = Color.White,
modifier = Modifier.size(32.dp)
)
}
}
}
}
// Modal Bottom Sheet for More menu
// Modal Bottom Sheet for More menu (Remains exactly the same)
if (showMoreMenu) {
ModalBottomSheet(
onDismissRequest = { showMoreMenu = false },

View File

@@ -0,0 +1,26 @@
package eu.gaudian.translator.view.exercises
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 StartExerciseScreen(
navController: NavHostController,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Start Exercise Screen",
style = MaterialTheme.typography.headlineMedium
)
}
}