diff --git a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt index 212fe35..b5c2628 100644 --- a/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt +++ b/app/src/main/java/eu/gaudian/translator/view/MainActivity.kt @@ -285,6 +285,16 @@ fun TranslatorApp( restoreState = false } } + }, + onPlayClicked = { + navController.navigate("start_exercise") { + popUpTo(0) { + inclusive = true + saveState = false + } + launchSingleTop = true + restoreState = false + } } ) }, diff --git a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt index bdb2a91..49f5837 100644 --- a/app/src/main/java/eu/gaudian/translator/view/Navigation.kt +++ b/app/src/main/java/eu/gaudian/translator/view/Navigation.kt @@ -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) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt index d27a3dc..e1670fa 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/BottomNavigationBar.kt @@ -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 }, @@ -325,4 +379,4 @@ fun BottomNavigationBarPreview() { ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt new file mode 100644 index 0000000..3947b95 --- /dev/null +++ b/app/src/main/java/eu/gaudian/translator/view/exercises/StartExerciseScreen.kt @@ -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 + ) + } +}