@@ -25,19 +25,17 @@ import androidx.compose.animation.core.rememberInfiniteTransition
2525import androidx.compose.animation.core.tween
2626import androidx.compose.animation.fadeIn
2727import androidx.compose.animation.fadeOut
28+ import androidx.compose.animation.togetherWith
2829import androidx.compose.foundation.background
2930import androidx.compose.foundation.clickable
3031import androidx.compose.foundation.layout.Box
3132import androidx.compose.foundation.layout.Column
3233import androidx.compose.foundation.layout.Row
3334import androidx.compose.foundation.layout.Spacer
34- import androidx.compose.foundation.layout.WindowInsets
3535import androidx.compose.foundation.layout.fillMaxWidth
3636import androidx.compose.foundation.layout.padding
3737import androidx.compose.foundation.layout.size
38- import androidx.compose.foundation.layout.statusBars
3938import androidx.compose.foundation.layout.width
40- import androidx.compose.foundation.layout.windowInsetsPadding
4139import androidx.compose.foundation.shape.CircleShape
4240import androidx.compose.material3.Icon
4341import androidx.compose.material3.MaterialTheme
@@ -58,128 +56,152 @@ import androidx.compose.ui.text.font.FontWeight
5856import androidx.compose.ui.unit.dp
5957import androidx.compose.ui.unit.sp
6058import androidx.lifecycle.compose.collectAsStateWithLifecycle
61- import androidx.navigation.NavHostController
62- import androidx.navigation.compose.NavHost
63- import androidx.navigation.compose.currentBackStackEntryAsState
64- import androidx.navigation.compose.rememberNavController
59+ import androidx.navigation3.runtime.entryProvider
60+ import androidx.navigation3.runtime.rememberNavBackStack
61+ import androidx.navigation3.ui.NavDisplay
62+ import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
6563import com.rtbishop.look4sat.core.presentation.Screen
6664import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
6765import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
68- import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
69- import com.rtbishop.look4sat.feature.map.mapDestination
70- import com.rtbishop.look4sat.feature.passes.passesDestination
71- import com.rtbishop.look4sat.feature.radar.radarDestination
72- import com.rtbishop.look4sat.feature.radiocontrol.radioControlDestination
73- import com.rtbishop.look4sat.feature.satellites.satellitesDestination
74- import com.rtbishop.look4sat.feature.settings.settingsDestination
66+ import com.rtbishop.look4sat.feature.map.MapDestination
67+ import com.rtbishop.look4sat.feature.passes.PassesDestination
68+ import com.rtbishop.look4sat.feature.radar.RadarDestination
69+ import com.rtbishop.look4sat.feature.radiocontrol.RadioControlDestination
70+ import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
71+ import com.rtbishop.look4sat.feature.settings.SettingsDestination
7572
7673@Composable
77- fun MainScreen (navController : NavHostController = rememberNavController()) {
78- val items = listOf (Screen .Satellites , Screen .Passes , Screen .Radar , Screen .Map , Screen .Settings )
79- val currentDestination = navController.currentBackStackEntryAsState().value?.destination?.route
80- val startDestination = Screen .Passes .route
74+ fun MainScreen () {
75+ val backStack = rememberNavBackStack(Screen .Passes )
76+ val currentKey = backStack.lastOrNull()
77+ val navigateBack: () -> Unit = { backStack.removeAt(backStack.size - 1 ) }
78+ val fadeTransition = fadeIn(animationSpec = tween(350 )) togetherWith fadeOut(animationSpec = tween(350 ))
79+ val navItems = listOf (Screen .Satellites , Screen .Passes , Screen .Radar (), Screen .Map , Screen .Settings )
8180
82- // Observe radio tracking state for the status bar
8381 val context = LocalContext .current
8482 val container = (context.applicationContext as IContainerProvider ).getMainContainer()
8583 val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
8684
8785 NavigationSuiteScaffold (
8886 navigationSuiteItems = {
89- items.forEach {
87+ navItems.forEach { screen ->
88+ val isSelected = when (currentKey) {
89+ is Screen .Satellites -> screen is Screen .Satellites
90+ is Screen .Passes -> screen is Screen .Passes
91+ is Screen .Radar -> screen is Screen .Radar
92+ is Screen .Map -> screen is Screen .Map
93+ is Screen .Settings -> screen is Screen .Settings
94+ else -> false
95+ }
9096 item(
91- icon = { Icon (painterResource(it .iconResId), stringResource(it .titleResId)) },
92- label = { Text (stringResource(it .titleResId)) },
93- selected = currentDestination?.contains(it.route) ? : false ,
97+ icon = { Icon (painterResource(screen .iconResId), stringResource(screen .titleResId)) },
98+ label = { Text (stringResource(screen .titleResId)) },
99+ selected = isSelected ,
94100 onClick = {
95- if (currentDestination?.contains(it.route) ? : false ) return @item
96- navController.navigate(it.route) {
97- popUpTo(startDestination) { saveState = false }
98- launchSingleTop = true
99- restoreState = false
100- }
101- })
101+ if (isSelected) return @item
102+ while (backStack.size > 1 ) backStack.removeAt(backStack.size - 1 )
103+ if (screen !is Screen .Passes ) backStack.add(screen)
104+ }
105+ )
102106 }
103- }, navigationSuiteColors = NavigationSuiteDefaults .colors(
107+ },
108+ navigationSuiteColors = NavigationSuiteDefaults .colors(
104109 navigationRailContainerColor = MaterialTheme .colorScheme.surfaceContainer
105- ), layoutType = when {
110+ ),
111+ layoutType = when {
106112 ! hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType .NavigationRail
107113 ! hasEnoughWidth() -> NavigationSuiteType .ShortNavigationBarCompact
108114 else -> NavigationSuiteType .ShortNavigationBarMedium
109115 }
110116 ) {
111117 Column {
112- NavHost (
113- navController = navController,
114- startDestination = startDestination,
115- enterTransition = { fadeIn(animationSpec = tween(350 )) },
116- exitTransition = { fadeOut(animationSpec = tween(350 )) },
117- modifier = Modifier .weight(1f )
118- ) {
119- satellitesDestination { navController.navigateUp() }
120- passesDestination { catNum: Int , aosTime: Long ->
121- val radarRoute = " ${Screen .Radar .route} ?catNum=${catNum} &aosTime=${aosTime} "
122- navController.navigate(radarRoute)
123- }
124- radarDestination(
125- navigateUp = { navController.navigateUp() },
126- navigateToRadioControl = { catNum, aosTime ->
127- val route = " ${Screen .RadioControl .route} ?catNum=$catNum &aosTime=$aosTime "
128- navController.navigate(route)
129- }
130- )
131- radioControlDestination { navController.navigateUp() }
132- mapDestination()
133- settingsDestination()
134- }
135-
136- // Radio tracking status banner (above bottom navigation)
137- if (trackingState.isActive) {
138- val infiniteTransition = rememberInfiniteTransition(label = " trackingPulse" )
139- val alpha by infiniteTransition.animateFloat(
140- initialValue = 1f , targetValue = 0.4f ,
141- animationSpec = infiniteRepeatable(
142- animation = tween(1000 , easing = LinearEasing ),
143- repeatMode = RepeatMode .Reverse
144- ), label = " pulseAlpha"
145- )
146- Row (
147- verticalAlignment = Alignment .CenterVertically ,
148- modifier = Modifier
149- .fillMaxWidth()
150- .background(MaterialTheme .colorScheme.primaryContainer)
151- .clickable {
152- val pass = trackingState.currentPass
153- if (pass != null ) {
154- val route = " ${Screen .RadioControl .route} ?catNum=${pass.catNum} &aosTime=${pass.aosTime} "
155- navController.navigate(route)
118+ NavDisplay (
119+ backStack = backStack,
120+ modifier = Modifier .weight(1f ),
121+ onBack = navigateBack,
122+ transitionSpec = { fadeTransition },
123+ popTransitionSpec = { fadeTransition },
124+ predictivePopTransitionSpec = { fadeTransition },
125+ entryProvider = entryProvider {
126+ entry<Screen .Satellites > {
127+ SatellitesDestination (navigateUp = navigateBack)
128+ }
129+ entry<Screen .Passes > {
130+ PassesDestination { catNum, aosTime ->
131+ backStack.add(Screen .Radar (catNum, aosTime))
156132 }
157133 }
158- .padding(horizontal = 12 .dp, vertical = 6 .dp)
159- ) {
160- Box (
161- modifier = Modifier
162- .size(8 .dp)
163- .clip(CircleShape )
164- .background(Color (0xFF4CAF50 ).copy(alpha = alpha))
165- )
166- Spacer (modifier = Modifier .width(8 .dp))
167- Text (
168- text = " Tracking: ${trackingState.currentPass?.name ? : " " } " ,
169- fontSize = 13 .sp,
170- fontWeight = FontWeight .Medium ,
171- color = MaterialTheme .colorScheme.onPrimaryContainer,
172- modifier = Modifier .weight(1f )
173- )
174- val txOk = if (trackingState.txConnected) " TX" else " "
175- val rxOk = if (trackingState.rxConnected) " RX" else " "
176- Text (
177- text = listOf (txOk, rxOk).filter { it.isNotBlank() }.joinToString(" /" ),
178- fontSize = 12 .sp,
179- color = MaterialTheme .colorScheme.onPrimaryContainer
134+ entry<Screen .Radar > { route ->
135+ RadarDestination (
136+ catNum = route.catNum,
137+ aosTime = route.aosTime,
138+ navigateUp = navigateBack,
139+ navigateToRadioControl = { catNum, aosTime ->
140+ backStack.add(Screen .RadioControl (catNum, aosTime))
141+ }
142+ )
143+ }
144+ entry<Screen .RadioControl > { route ->
145+ RadioControlDestination (
146+ catNum = route.catNum,
147+ aosTime = route.aosTime,
148+ navigateUp = navigateBack
149+ )
150+ }
151+ entry<Screen .Map > {
152+ MapDestination ()
153+ }
154+ entry<Screen .Settings > {
155+ SettingsDestination ()
156+ }
157+ }
158+ )
159+ // Radio tracking status banner
160+ if (trackingState.isActive) {
161+ val infiniteTransition = rememberInfiniteTransition(label = " trackingPulse" )
162+ val alpha by infiniteTransition.animateFloat(
163+ initialValue = 1f , targetValue = 0.4f ,
164+ animationSpec = infiniteRepeatable(
165+ animation = tween(1000 , easing = LinearEasing ),
166+ repeatMode = RepeatMode .Reverse
167+ ), label = " pulseAlpha"
180168 )
169+ Row (
170+ verticalAlignment = Alignment .CenterVertically ,
171+ modifier = Modifier
172+ .fillMaxWidth()
173+ .background(MaterialTheme .colorScheme.primaryContainer)
174+ .clickable {
175+ val pass = trackingState.currentPass
176+ if (pass != null ) {
177+ backStack.add(Screen .RadioControl (pass.catNum, pass.aosTime))
178+ }
179+ }
180+ .padding(horizontal = 12 .dp, vertical = 6 .dp)
181+ ) {
182+ Box (
183+ modifier = Modifier
184+ .size(8 .dp)
185+ .clip(CircleShape )
186+ .background(Color (0xFF4CAF50 ).copy(alpha = alpha))
187+ )
188+ Spacer (modifier = Modifier .width(8 .dp))
189+ Text (
190+ text = " Tracking: ${trackingState.currentPass?.name ? : " " } " ,
191+ fontSize = 13 .sp,
192+ fontWeight = FontWeight .Medium ,
193+ color = MaterialTheme .colorScheme.onPrimaryContainer,
194+ modifier = Modifier .weight(1f )
195+ )
196+ val txOk = if (trackingState.txConnected) " TX" else " "
197+ val rxOk = if (trackingState.rxConnected) " RX" else " "
198+ Text (
199+ text = listOf (txOk, rxOk).filter { it.isNotBlank() }.joinToString(" /" ),
200+ fontSize = 12 .sp,
201+ color = MaterialTheme .colorScheme.onPrimaryContainer
202+ )
203+ }
181204 }
182205 }
183- } // end Column
184206 }
185207}
0 commit comments