From 0e28231c4a8061ffce97a4bd6f4ba56e4095df24 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Tue, 22 Sep 2026 16:41:55 +0800 Subject: [PATCH] feat(map): run the Match query before navigating to the mutual page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping the grid-QSO dialog "Match" button now starts the pass query on the map page and only navigates to the match page once it finishes, so the page's first frame already contains the results — the time-range card at the top and the pass curves right below, with no scroll flicker. The dialog button shows a spinner and is disabled while calculating. A uiState collector in MainScreen performs the actual navigation (still a push, so the back gesture returns to the map). Failures/exceptions still navigate so the user is never stuck on the map page. --- .../java/com/rtbishop/look4sat/MainScreen.kt | 37 ++++++++++++++----- .../look4sat/feature/map/MapScreen.kt | 31 ++++++++++++---- .../mutual/MutualMatchPrefillScrollTest.kt | 7 ++++ 3 files changed, 58 insertions(+), 17 deletions(-) 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