feat(map): grid QSO dialog Match button prefills the match page

- Grid-QSO dialog gains a Match action (radio-tower icon + label) in its
  header; tapping it navigates to the match page pre-filled with the tapped
  grid as the opposite station and a 24h time range, auto-starts the query,
  and scrolls the page straight to the time-range card.
- Every grid is now tappable (previously only worked grids opened the
  dialog); unworked grids show an empty QSO list instead of a hint.
This commit is contained in:
atsunatsu committed 2026-09-22 10:46:56 +08:00
1 parent 6beaeb3e1a
commit 3e0874e6a8
6 files changed
+120 -37

No files matched your search

@@ -246,7 +246,7 @@ private fun MutualContent(
}
// Input form
item {
item(key = "stationInputs") {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
@@ -323,16 +323,21 @@ private fun MutualContent(
)
}
}
MatchSearchCard(
hoursAhead = state.hoursAhead,
isCalculating = state.isCalculating,
onHoursAhead = onHoursAhead,
onQuery = onQuery
)
}
}
// Time range + query card. Its own LazyColumn item so a prefill from
// the map (grid-QSO dialog "Match" button) can scroll the page exactly
// to the time-range selector.
item(key = "matchSearch") {
MatchSearchCard(
hoursAhead = state.hoursAhead,
isCalculating = state.isCalculating,
onHoursAhead = onHoursAhead,
onQuery = onQuery
)
}
// Results
itemsIndexed(state.mutualPasses) { index, pass ->
val mutualData = MutualPassData(
@@ -364,6 +369,17 @@ private fun MutualContent(
)
}
}
// Prefill from the map's grid-QSO dialog "Match" button: jump straight to
// the time-range card. Index accounts for the optional error card at the
// top (error = 0, station inputs = 1, time range = 2; else 1).
val matchSearchIndex = if (state.errorMessage != null) 2 else 1
LaunchedEffect(state.scrollToTimeRange, matchSearchIndex) {
if (state.scrollToTimeRange) {
listState.scrollToItem(matchSearchIndex)
viewModel.consumeScrollToTimeRange()
}
}
}
@Composable
@@ -55,7 +55,11 @@ data class MutualUiState(
val hasSearched: Boolean = false,
val selectedPassIndex: Int = -1,
val isUtc: Boolean = false,
val errorMessage: String? = null
val errorMessage: String? = null,
// One-shot flag set by prefillMatchFromGrid() (map grid → Match button):
// the page scrolls to the time-range card, then consumeScrollToTimeRange()
// clears it.
val scrollToTimeRange: Boolean = false
)
class MutualViewModel(
@@ -183,6 +187,32 @@ class MutualViewModel(
fun onHoursAhead(value: Int) = _uiState.update { it.copy(hoursAhead = value) }
fun onSelectPass(index: Int) = _uiState.update { it.copy(selectedPassIndex = index) }
/**
* Pre-fill the match page for a target grid picked from the map's grid-QSO
* dialog ("Match" button): set the opposite-station grid (with its
* coordinates), reset the time range to 24h, flag the page to scroll
* straight to the time-range card, and start the query immediately so
* results are ready when the page opens.
*/
fun prefillMatchFromGrid(grid: String) {
val g = grid.trim().uppercase()
val pos = qthToPosition(g)
_uiState.update {
it.copy(
stationBGrid = g,
stationBLat = pos?.let { p -> "%.4f".format(p.latitude) } ?: it.stationBLat,
stationBLon = pos?.let { p -> "%.4f".format(p.longitude) } ?: it.stationBLon,
hoursAhead = 24,
scrollToTimeRange = true
)
}
queryMutualPasses()
}
fun consumeScrollToTimeRange() {
_uiState.update { it.copy(scrollToTimeRange = false) }
}
fun queryMutualPasses() {
val state = _uiState.value