diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index c337da5f..81fd48d8 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -177,6 +177,28 @@ fun MainScreen( factory = MapFilterViewModel.factory() ) + // Map grid-QSO dialog "Match" button: pre-fill the match page for that + // grid and run the query BEFORE navigating, so the page's first frame + // already contains the results (time-range card at the top, pass curves + // right below — no scroll flicker). Navigation happens once the query + // finishes (see the collector below). + val mutualState by mutualViewModel.uiState.collectAsStateWithLifecycle() + val matchCalculating = mutualState.pendingNavigation && mutualState.isCalculating + + LaunchedEffect(mutualViewModel) { + mutualViewModel.uiState.collect { state -> + if (state.pendingNavigation && !state.isCalculating) { + mutualViewModel.consumePendingNavigation() + // Push Mutual on top of the Map entry instead of replacing the + // stack (bottom-nav style): the system back gesture then pops + // back to the map page, which is the page the user came from. + if (backStack.lastOrNull() !is Screen.Mutual) { + backStack.add(Screen.Mutual) + } + } + } + } + CompositionLocalProvider( LocalElevationThresholds provides ElevationThresholds( low = otherSettings.lowElevation, @@ -246,17 +268,14 @@ fun MainScreen( entry { MapDestination( mapFilterViewModel = mapFilterViewModel, + matchCalculating = matchCalculating, onMatchGrid = { grid -> - // Grid-QSO dialog "Match" button: pre-fill the - // match page for that grid and open it. + // Grid-QSO dialog "Match" button: pre-fill + // the match page for that grid and start the + // query. Navigation to the match page is + // handled by the uiState collector above, + // once the query finishes. mutualViewModel.prefillMatchFromGrid(grid) - // Push Mutual on top of the Map entry instead of - // replacing the stack (bottom-nav style): the - // system back gesture then pops back to the map - // page, which is the page the user came from. - if (backStack.lastOrNull() !is Screen.Mutual) { - backStack.add(Screen.Mutual) - } } ) } 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 0037805f..e45fa522 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 @@ -54,6 +54,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -153,7 +154,10 @@ fun MapDestination( mapFilterViewModel: MapFilterViewModel, // Invoked with the tapped grid when the QSO dialog's "Match" button is // pressed: navigate to the match page pre-filled for that grid. - onMatchGrid: (String) -> Unit = {} + onMatchGrid: (String) -> Unit = {}, + // True while the map "Match" query is running (the page only navigates + // once the query finished, so the match page opens with results ready). + matchCalculating: Boolean = false ) { val context = LocalContext.current val container = (context.applicationContext as IContainerProvider).getMainContainer() @@ -190,7 +194,7 @@ fun MapDestination( viewModel.onAction(MapAction.SetVisible(false)) } } - MapScreen(uiState, viewModel::onAction, mapView, mapFilterViewModel, onMatchGrid) + MapScreen(uiState, viewModel::onAction, mapView, mapFilterViewModel, onMatchGrid, matchCalculating) } @Composable @@ -199,7 +203,8 @@ private fun MapScreen( onAction: (MapAction) -> Unit, mapView: MapView, mapFilterViewModel: MapFilterViewModel, - onMatchGrid: (String) -> Unit + onMatchGrid: (String) -> Unit, + matchCalculating: Boolean ) { val rotateMod = Modifier.rotate(180f) val timeString = uiState.mapData?.aosTime ?: "00:00:00" @@ -423,6 +428,7 @@ private fun MapScreen( grid = grid, qsos = uiState.workedGridQsos[grid].orEmpty().sortedBy { it.epochMs }, isUtc = uiState.isUtc, + matchCalculating = matchCalculating, onDismiss = { selectedGrid = null }, onMatch = { onMatchGrid(grid) } ) @@ -436,6 +442,7 @@ private fun WorkedGridQsoDialog( grid: String, qsos: List, isUtc: Boolean, + matchCalculating: Boolean, onDismiss: () -> Unit, onMatch: () -> Unit ) { @@ -467,16 +474,24 @@ private fun WorkedGridQsoDialog( ) } TextButton( + enabled = !matchCalculating, onClick = { onDismiss() onMatch() } ) { - Icon( - painter = painterResource(R.drawable.ic_radio_tower), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + if (matchCalculating) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_radio_tower), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } Spacer(Modifier.width(4.dp)) Text(stringResource(R.string.grid_qso_match)) } diff --git a/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/MutualMatchPrefillScrollTest.kt b/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/MutualMatchPrefillScrollTest.kt index 5ae4bdeb..c82c66b9 100644 --- a/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/MutualMatchPrefillScrollTest.kt +++ b/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/MutualMatchPrefillScrollTest.kt @@ -146,6 +146,13 @@ class MutualMatchPrefillScrollTest { // ...and the target grid + 24h range pre-filled. org.junit.Assert.assertEquals("OL62", s.stationBGrid) org.junit.Assert.assertEquals(24, s.hoursAhead) + // ...and navigation to the match page is pending until the query done. + assertTrue("navigation should be pending", s.pendingNavigation) + // The early-return guard means the query finished immediately (no + // satellites): the map page must now be free to navigate. + org.junit.Assert.assertFalse("query finished -> not calculating", s.isCalculating) + vm.consumePendingNavigation() + org.junit.Assert.assertFalse("consume clears pending navigation", vm.uiState.value.pendingNavigation) } @Test