From 7d5bca300d358257921a12c37267abd5546722c1 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Fri, 24 Apr 2026 17:20:23 +0100 Subject: [PATCH] Migrated to Compose Navigation3, updated dependencies --- .../java/com/rtbishop/look4sat/MainScreen.kt | 220 ++++++++++-------- .../look4sat/convention/ApplicationPlugin.kt | 2 + .../convention/CorePresentationPlugin.kt | 3 + .../look4sat/core/presentation/Navigation.kt | 47 +++- .../look4sat/feature/map/MapScreen.kt | 19 +- .../look4sat/feature/passes/PassesScreen.kt | 20 +- .../look4sat/feature/radar/RadarScreen.kt | 31 ++- .../look4sat/feature/radar/RadarState.kt | 3 +- .../look4sat/feature/radar/RadarViewModel.kt | 27 ++- .../radiocontrol/RadioControlScreen.kt | 22 +- .../radiocontrol/RadioControlViewModel.kt | 19 +- .../feature/satellites/SatellitesScreen.kt | 19 +- .../feature/settings/SettingsScreen.kt | 20 +- gradle/libs.versions.toml | 20 +- 14 files changed, 252 insertions(+), 220 deletions(-) diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 31fa967a..5ea30662 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -25,19 +25,17 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -58,128 +56,152 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.hasEnoughHeight import com.rtbishop.look4sat.core.presentation.hasEnoughWidth -import com.rtbishop.look4sat.core.domain.repository.IContainerProvider -import com.rtbishop.look4sat.feature.map.mapDestination -import com.rtbishop.look4sat.feature.passes.passesDestination -import com.rtbishop.look4sat.feature.radar.radarDestination -import com.rtbishop.look4sat.feature.radiocontrol.radioControlDestination -import com.rtbishop.look4sat.feature.satellites.satellitesDestination -import com.rtbishop.look4sat.feature.settings.settingsDestination +import com.rtbishop.look4sat.feature.map.MapDestination +import com.rtbishop.look4sat.feature.passes.PassesDestination +import com.rtbishop.look4sat.feature.radar.RadarDestination +import com.rtbishop.look4sat.feature.radiocontrol.RadioControlDestination +import com.rtbishop.look4sat.feature.satellites.SatellitesDestination +import com.rtbishop.look4sat.feature.settings.SettingsDestination @Composable -fun MainScreen(navController: NavHostController = rememberNavController()) { - val items = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Map, Screen.Settings) - val currentDestination = navController.currentBackStackEntryAsState().value?.destination?.route - val startDestination = Screen.Passes.route +fun MainScreen() { + val backStack = rememberNavBackStack(Screen.Passes) + val currentKey = backStack.lastOrNull() + val navigateBack: () -> Unit = { backStack.removeAt(backStack.size - 1) } + val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350)) + val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar(), Screen.Map, Screen.Settings) - // Observe radio tracking state for the status bar val context = LocalContext.current val container = (context.applicationContext as IContainerProvider).getMainContainer() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() NavigationSuiteScaffold( navigationSuiteItems = { - items.forEach { + navItems.forEach { screen -> + val isSelected = when (currentKey) { + is Screen.Satellites -> screen is Screen.Satellites + is Screen.Passes -> screen is Screen.Passes + is Screen.Radar -> screen is Screen.Radar + is Screen.Map -> screen is Screen.Map + is Screen.Settings -> screen is Screen.Settings + else -> false + } item( - icon = { Icon(painterResource(it.iconResId), stringResource(it.titleResId)) }, - label = { Text(stringResource(it.titleResId)) }, - selected = currentDestination?.contains(it.route) ?: false, + icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) }, + label = { Text(stringResource(screen.titleResId)) }, + selected = isSelected, onClick = { - if (currentDestination?.contains(it.route) ?: false) return@item - navController.navigate(it.route) { - popUpTo(startDestination) { saveState = false } - launchSingleTop = true - restoreState = false - } - }) + if (isSelected) return@item + while (backStack.size > 1) backStack.removeAt(backStack.size - 1) + if (screen !is Screen.Passes) backStack.add(screen) + } + ) } - }, navigationSuiteColors = NavigationSuiteDefaults.colors( + }, + navigationSuiteColors = NavigationSuiteDefaults.colors( navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer - ), layoutType = when { + ), + layoutType = when { !hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail !hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact else -> NavigationSuiteType.ShortNavigationBarMedium } ) { Column { - NavHost( - navController = navController, - startDestination = startDestination, - enterTransition = { fadeIn(animationSpec = tween(350)) }, - exitTransition = { fadeOut(animationSpec = tween(350)) }, - modifier = Modifier.weight(1f) - ) { - satellitesDestination { navController.navigateUp() } - passesDestination { catNum: Int, aosTime: Long -> - val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}" - navController.navigate(radarRoute) - } - radarDestination( - navigateUp = { navController.navigateUp() }, - navigateToRadioControl = { catNum, aosTime -> - val route = "${Screen.RadioControl.route}?catNum=$catNum&aosTime=$aosTime" - navController.navigate(route) - } - ) - radioControlDestination { navController.navigateUp() } - mapDestination() - settingsDestination() - } - - // Radio tracking status banner (above bottom navigation) - if (trackingState.isActive) { - val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse") - val alpha by infiniteTransition.animateFloat( - initialValue = 1f, targetValue = 0.4f, - animationSpec = infiniteRepeatable( - animation = tween(1000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), label = "pulseAlpha" - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.primaryContainer) - .clickable { - val pass = trackingState.currentPass - if (pass != null) { - val route = "${Screen.RadioControl.route}?catNum=${pass.catNum}&aosTime=${pass.aosTime}" - navController.navigate(route) + NavDisplay( + backStack = backStack, + modifier = Modifier.weight(1f), + onBack = navigateBack, + transitionSpec = { fadeTransition }, + popTransitionSpec = { fadeTransition }, + predictivePopTransitionSpec = { fadeTransition }, + entryProvider = entryProvider { + entry { + SatellitesDestination(navigateUp = navigateBack) + } + entry { + PassesDestination { catNum, aosTime -> + backStack.add(Screen.Radar(catNum, aosTime)) } } - .padding(horizontal = 12.dp, vertical = 6.dp) - ) { - Box( + entry { route -> + RadarDestination( + catNum = route.catNum, + aosTime = route.aosTime, + navigateUp = navigateBack, + navigateToRadioControl = { catNum, aosTime -> + backStack.add(Screen.RadioControl(catNum, aosTime)) + } + ) + } + entry { route -> + RadioControlDestination( + catNum = route.catNum, + aosTime = route.aosTime, + navigateUp = navigateBack + ) + } + entry { + MapDestination() + } + entry { + SettingsDestination() + } + } + ) + // Radio tracking status banner + if (trackingState.isActive) { + val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse") + val alpha by infiniteTransition.animateFloat( + initialValue = 1f, targetValue = 0.4f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), label = "pulseAlpha" + ) + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(Color(0xFF4CAF50).copy(alpha = alpha)) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Tracking: ${trackingState.currentPass?.name ?: ""}", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.weight(1f) - ) - val txOk = if (trackingState.txConnected) "TX" else "" - val rxOk = if (trackingState.rxConnected) "RX" else "" - Text( - text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .clickable { + val pass = trackingState.currentPass + if (pass != null) { + backStack.add(Screen.RadioControl(pass.catNum, pass.aosTime)) + } + } + .padding(horizontal = 12.dp, vertical = 6.dp) + ) { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(Color(0xFF4CAF50).copy(alpha = alpha)) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Tracking: ${trackingState.currentPass?.name ?: ""}", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.weight(1f) + ) + val txOk = if (trackingState.txConnected) "TX" else "" + val rxOk = if (trackingState.rxConnected) "RX" else "" + Text( + text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"), + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } } } - } // end Column } } diff --git a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt index 29c00ad9..b802cdff 100644 --- a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt +++ b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/ApplicationPlugin.kt @@ -38,6 +38,8 @@ internal class ApplicationPlugin : Plugin { implementation(project(":feature:satellites")) implementation(project(":feature:settings")) implementation(libs.androidx.core.splashscreen) + implementation(libs.compose.material3.adaptive) + implementation(libs.compose.navigation3) androidTestImplementation(libs.bundles.androidTest) } } diff --git a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/CorePresentationPlugin.kt b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/CorePresentationPlugin.kt index 7eb5ed5b..5a19c154 100644 --- a/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/CorePresentationPlugin.kt +++ b/build-logic/convention/src/main/java/com/rtbishop/look4sat/convention/CorePresentationPlugin.kt @@ -24,12 +24,15 @@ import org.gradle.kotlin.dsl.dependencies @Suppress("Unused") internal class CorePresentationPlugin : Plugin { override fun apply(target: Project) = with(target) { + applyPlugin(libs.plugins.kotlin.serialization) setupAndroidLib() setupCompose() setupKotlin() dependencies { implementation(project(":core:domain")) implementation(libs.androidx.core.splashscreen) + implementation(libs.kotlin.serialization) + implementation(libs.compose.material3.adaptive) } } } diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt index d5bd9662..a7bae8d0 100644 --- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt +++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Navigation.kt @@ -1,10 +1,43 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.rtbishop.look4sat.core.presentation -sealed class Screen(val route: String, val iconResId: Int, val titleResId: Int) { - data object Satellites : Screen("satellites", R.drawable.ic_satellites, R.string.nav_sat) - data object Passes : Screen("passes", R.drawable.ic_passes, R.string.nav_pass) - data object Radar : Screen("radar", R.drawable.ic_radar, R.string.nav_radar) - data object Map : Screen("map", R.drawable.ic_map, R.string.nav_map) - data object Settings : Screen("settings", R.drawable.ic_settings, R.string.nav_prefs) - data object RadioControl : Screen("radiocontrol", R.drawable.ic_radios, R.string.nav_radiocontrol) +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey { + + @Serializable + data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat) + + @Serializable + data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass) + + @Serializable + data class Radar(val catNum: Int = 0, val aosTime: Long = 0L) : Screen(R.drawable.ic_radar, R.string.nav_radar) + + @Serializable + data class RadioControl(val catNum: Int = 0, val aosTime: Long = 0L) : Screen(0, 0) + + @Serializable + data object Map : Screen(R.drawable.ic_map, R.string.nav_map) + + @Serializable + data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs) } diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index 51142805..a3cf8b1b 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -60,15 +60,12 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.domain.predict.OrbitalObject import com.rtbishop.look4sat.core.domain.predict.OrbitalPos import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.isVerticalLayout @@ -110,13 +107,15 @@ private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { } private val iconCache = LruCache(128) -fun NavGraphBuilder.mapDestination() { - composable(Screen.Map.route) { - val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val mapView = rememberMapViewWithLifecycle() - MapScreen(uiState, viewModel::onAction, mapView) - } +@Composable +fun MapDestination() { + val viewModel = viewModel( + modelClass = MapViewModel::class.java, + factory = MapViewModel.Factory + ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val mapView = rememberMapViewWithLifecycle() + MapScreen(uiState, viewModel::onAction, mapView) } @Composable diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt index 2d59ae8b..d0901989 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt @@ -56,8 +56,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.rtbishop.look4sat.core.domain.predict.DeepSpaceObject import com.rtbishop.look4sat.core.domain.predict.NearEarthObject import com.rtbishop.look4sat.core.domain.predict.OrbitalData @@ -68,7 +66,6 @@ import com.rtbishop.look4sat.core.presentation.InfoDialog import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.ScreenColumn import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar @@ -79,15 +76,14 @@ import java.util.Date import java.util.Locale import java.util.TimeZone -fun NavGraphBuilder.passesDestination(navigateToRadar: (Int, Long) -> Unit) { - composable(Screen.Passes.route) { - val viewModel = viewModel( - modelClass = PassesViewModel::class.java, - factory = PassesViewModel.Factory - ) - val uiState = viewModel.uiState.collectAsStateWithLifecycle().value - PassesScreen(uiState, viewModel::onAction, navigateToRadar) - } +@Composable +fun PassesDestination(navigateToRadar: (Int, Long) -> Unit) { + val viewModel = viewModel( + modelClass = PassesViewModel::class.java, + factory = PassesViewModel.Factory + ) + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + PassesScreen(uiState, viewModel::onAction, navigateToRadar) } @Composable diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index 7a0a9a40..dc016494 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -42,6 +42,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,9 +59,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navArgument import com.rtbishop.look4sat.core.domain.model.SatRadio import com.rtbishop.look4sat.core.domain.predict.OrbitalPos import com.rtbishop.look4sat.core.domain.utility.toDegrees @@ -69,7 +67,6 @@ import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.getDefaultPass @@ -77,20 +74,20 @@ import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.layoutPadding -fun NavGraphBuilder.radarDestination( +@Composable +fun RadarDestination( + catNum: Int = 0, + aosTime: Long = 0L, navigateUp: () -> Unit, navigateToRadioControl: (Int, Long) -> Unit = { _, _ -> } ) { - val radarRoute = "${Screen.Radar.route}?catNum={catNum}&aosTime={aosTime}" - val radarArgs = listOf( - navArgument("catNum") { defaultValue = 0 }, - navArgument("aosTime") { defaultValue = 0L } + val viewModel = viewModel( + modelClass = RadarViewModel::class.java, + key = "$catNum-$aosTime", + factory = RadarViewModel.factory(catNum, aosTime) ) - composable(radarRoute, radarArgs) { - val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - RadarScreen(uiState, viewModel::onAction, navigateUp, navigateToRadioControl) - } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + RadarScreen(uiState, viewModel::onAction, navigateUp, navigateToRadioControl) } @Composable @@ -101,7 +98,7 @@ private fun RadarScreen( navigateToRadioControl: (Int, Long) -> Unit ) { val upcomingPass = uiState.currentPass ?: getDefaultPass() - if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp() + LaunchedEffect(uiState.isLos) { if (uiState.isLos) navigateUp() } val addToCalendar: () -> Unit = { uiState.currentPass?.let { pass -> @@ -123,14 +120,14 @@ private fun RadarScreen( if (isVertical) { TopBar { IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) - TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) + TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos) IconCard(action = openRadioControl, resId = R.drawable.ic_radios) } TopBar { NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc) } } else { TopBar { IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) - TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) + TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos) NextPassRow(pass = upcomingPass, modifier = Modifier.weight(1f), isUtc = uiState.isUtc) IconCard(action = openRadioControl, resId = R.drawable.ic_radios) } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt index 477ec994..21dcfa80 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt @@ -24,7 +24,8 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPos data class RadarState( val currentPass: OrbitalPass? = null, val currentTime: String = "00:00:00", - val isCurrentTimeAos: Boolean = true, + val isTimeAos: Boolean = true, + val isLos: Boolean = false, val isUtc: Boolean = false, val orientationValues: Pair = 0f to 0f, val orbitalPos: OrbitalPos? = null, diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 65a8183c..6ac250ea 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -17,10 +17,8 @@ */ package com.rtbishop.look4sat.feature.radar -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory @@ -45,7 +43,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch class RadarViewModel( - private val savedStateHandle: SavedStateHandle, + private val catNum: Int, + private val aosTime: Long, private val bluetoothReporter: IReporter, private val networkReporter: IReporter, private val satelliteRepo: ISatelliteRepo, @@ -85,8 +84,6 @@ class RadarViewModel( } // Resolve which pass we're tracking and start the tick loop viewModelScope.launch { - val catNum = savedStateHandle.get("catNum") ?: 0 - val aosTime = savedStateHandle.get("aosTime") ?: 0L val passes = satelliteRepo.passes.value val currentPass = passes.find { it.catNum == catNum && it.aosTime == aosTime } ?: passes.firstOrNull() @@ -105,7 +102,8 @@ class RadarViewModel( val timeNow = System.currentTimeMillis() val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow) val (time, isAos) = computeTimer(satPass.isDeepSpace, satPass.aosTime, satPass.losTime, timeNow) - _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = isAos, orbitalPos = pos) } + val isLos = !satPass.isDeepSpace && timeNow > satPass.losTime + _uiState.update { it.copy(currentTime = time, isTimeAos = isAos, isLos = isLos, orbitalPos = pos) } processRadios(transmitters, satPass.orbitalObject, timeNow) sendPassData(pos) delay(1000) @@ -204,18 +202,19 @@ class RadarViewModel( } companion object { - val Factory: ViewModelProvider.Factory = viewModelFactory { + fun factory(catNum: Int, aosTime: Long): ViewModelProvider.Factory = viewModelFactory { val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY initializer { val container = (this[applicationKey] as IContainerProvider).getMainContainer() RadarViewModel( - createSavedStateHandle(), - container.provideBluetoothReporter(), - container.provideNetworkReporter(), - container.satelliteRepo, - container.settingsRepo, - container.provideSensorsRepo(), - container.provideAddToCalendar() + catNum = catNum, + aosTime = aosTime, + bluetoothReporter = container.provideBluetoothReporter(), + networkReporter = container.provideNetworkReporter(), + satelliteRepo = container.satelliteRepo, + settingsRepo = container.settingsRepo, + sensorsRepo = container.provideSensorsRepo(), + addToCalendar = container.provideAddToCalendar() ) } } diff --git a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt index 0d26a4fd..acae67ab 100644 --- a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt +++ b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt @@ -51,15 +51,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navArgument import com.rtbishop.look4sat.core.domain.model.SatRadio import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.getDefaultPass @@ -67,17 +63,15 @@ import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.layoutPadding import java.util.Locale -fun NavGraphBuilder.radioControlDestination(navigateUp: () -> Unit) { - val route = "${Screen.RadioControl.route}?catNum={catNum}&aosTime={aosTime}" - val args = listOf( - navArgument("catNum") { defaultValue = 0 }, - navArgument("aosTime") { defaultValue = 0L } +@Composable +fun RadioControlDestination(catNum: Int = 0, aosTime: Long = 0L, navigateUp: () -> Unit) { + val viewModel = viewModel( + modelClass = RadioControlViewModel::class.java, + key = "$catNum-$aosTime", + factory = RadioControlViewModel.factory(catNum, aosTime) ) - composable(route, args) { - val viewModel = viewModel(RadioControlViewModel::class.java, factory = RadioControlViewModel.Factory) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - RadioControlScreen(uiState, viewModel::onAction, navigateUp) - } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + RadioControlScreen(uiState, viewModel::onAction, navigateUp) } @Composable diff --git a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt index fff44907..84e15d5d 100644 --- a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt +++ b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt @@ -17,10 +17,8 @@ */ package com.rtbishop.look4sat.feature.radiocontrol -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory @@ -41,7 +39,8 @@ import kotlinx.coroutines.launch import java.util.Locale class RadioControlViewModel( - savedStateHandle: SavedStateHandle, + private val catNum: Int, + private val aosTime: Long, private val trackingService: IRadioTrackingService, private val satelliteRepo: ISatelliteRepo, settingsRepo: ISettingsRepo @@ -72,9 +71,6 @@ class RadioControlViewModel( val uiState: StateFlow = _uiState init { - val catNum = savedStateHandle.get("catNum") ?: 0 - val aosTime = savedStateHandle.get("aosTime") ?: 0L - // Resolve pass and load transponders viewModelScope.launch { val passes = satelliteRepo.passes.value @@ -190,15 +186,16 @@ class RadioControlViewModel( return String.format(Locale.ENGLISH, "%d.%03d.%03d", mhz, khz, hz) } - val Factory: ViewModelProvider.Factory = viewModelFactory { + fun factory(catNum: Int, aosTime: Long): ViewModelProvider.Factory = viewModelFactory { val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY initializer { val container = (this[applicationKey] as IContainerProvider).getMainContainer() RadioControlViewModel( - createSavedStateHandle(), - container.radioTrackingService, - container.satelliteRepo, - container.settingsRepo + catNum = catNum, + aosTime = aosTime, + trackingService = container.radioTrackingService, + satelliteRepo = container.satelliteRepo, + settingsRepo = container.settingsRepo ) } } diff --git a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt index 0f9d9ad5..08e5e242 100644 --- a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt +++ b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt @@ -56,8 +56,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.rtbishop.look4sat.core.domain.model.SatItem import com.rtbishop.look4sat.core.presentation.CardLoadingIndicator import com.rtbishop.look4sat.core.presentation.EmptyListCard @@ -66,20 +64,19 @@ import com.rtbishop.look4sat.core.presentation.InfoDialog import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.PrimaryIconCard import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.layoutPadding -fun NavGraphBuilder.satellitesDestination(navigateUp: () -> Unit) { - composable(Screen.Satellites.route) { - val viewModel = viewModel( - modelClass = SatellitesViewModel::class.java, factory = SatellitesViewModel.Factory - ) - val uiState = viewModel.uiState.collectAsStateWithLifecycle().value - SatellitesScreen(uiState, viewModel::onAction, navigateUp) - } +@Composable +fun SatellitesDestination(navigateUp: () -> Unit) { + val viewModel = viewModel( + modelClass = SatellitesViewModel::class.java, + factory = SatellitesViewModel.Factory + ) + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + SatellitesScreen(uiState, viewModel::onAction, navigateUp) } @Composable diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index edf47e6b..42ba052d 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -60,8 +60,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.predict.GeoPos import com.rtbishop.look4sat.core.presentation.CardButton @@ -69,7 +67,6 @@ import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.PrimaryIconCard import com.rtbishop.look4sat.core.presentation.R -import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.ScreenColumn import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.infiniteMarquee @@ -78,15 +75,14 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -fun NavGraphBuilder.settingsDestination() { - composable(Screen.Settings.route) { - val viewModel = viewModel( - modelClass = SettingsViewModel::class.java, - factory = SettingsViewModel.Factory - ) - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - SettingsScreen(uiState, viewModel::onAction) - } +@Composable +fun SettingsDestination() { + val viewModel = viewModel( + modelClass = SettingsViewModel::class.java, + factory = SettingsViewModel.Factory + ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + SettingsScreen(uiState, viewModel::onAction) } @Composable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bfa675d5..bd5f779e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,10 +18,10 @@ androidx-core-ktx = "1.18.0" androidx-core-splashscreen = "1.2.0" androidx-room = "2.8.4" -compose-bom = "2026.03.01" +compose-bom = "2026.04.01" compose-activity = "1.13.0" compose-lifecycle = "2.10.0" -compose-navigation = "2.9.7" +compose-navigation3 = "1.1.1" google-ksp = "2.3.6" @@ -52,16 +52,16 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = " #noinspection UnusedVersionCatalogEntry compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } -compose-animation = { group = "androidx.compose.animation", name = "animation" } compose-material3 = { group = "androidx.compose.material3", name = "material3" } -compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } +#noinspection UnusedVersionCatalogEntry +compose-material3-adaptive = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling-preview" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" } -compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } -compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" } -compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-lifecycle" } +#noinspection UnusedVersionCatalogEntry +compose-navigation3 = { module = "androidx.navigation3:navigation3-ui", version.ref = "compose-navigation3" } +compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-lifecycle" } compose-debug-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } compose-debug-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } @@ -99,11 +99,7 @@ convention-featurePlugin = { id = "com.rtbishop.look4sat.convention.featurePlugi [bundles] #noinspection UnusedVersionCatalogEntry -composeAll = [ - "compose-animation", "compose-runtime", "compose-tooling", - "compose-activity", "compose-lifecycle", "compose-material3", - "compose-material3-navigation", "compose-navigation", "compose-viewmodel" -] +composeAll = ["compose-runtime", "compose-tooling", "compose-activity", "compose-material3", "compose-viewmodel"] #noinspection UnusedVersionCatalogEntry composeDebug = ["compose-debug-manifest", "compose-debug-tooling"] #noinspection UnusedVersionCatalogEntry