mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
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:
1 parent
6beaeb3e1a
commit
3e0874e6a8
6 files changed
+120
-37
No files matched your search
@@ -31,10 +31,12 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -46,10 +48,12 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -63,6 +67,7 @@ import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color as ComposeColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -145,7 +150,10 @@ private val moonIconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
|
||||
@Composable
|
||||
fun MapDestination(
|
||||
mapFilterViewModel: MapFilterViewModel
|
||||
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 = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
@@ -182,7 +190,7 @@ fun MapDestination(
|
||||
viewModel.onAction(MapAction.SetVisible(false))
|
||||
}
|
||||
}
|
||||
MapScreen(uiState, viewModel::onAction, mapView, mapFilterViewModel)
|
||||
MapScreen(uiState, viewModel::onAction, mapView, mapFilterViewModel, onMatchGrid)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -190,7 +198,8 @@ private fun MapScreen(
|
||||
uiState: MapState,
|
||||
onAction: (MapAction) -> Unit,
|
||||
mapView: MapView,
|
||||
mapFilterViewModel: MapFilterViewModel
|
||||
mapFilterViewModel: MapFilterViewModel,
|
||||
onMatchGrid: (String) -> Unit
|
||||
) {
|
||||
val rotateMod = Modifier.rotate(180f)
|
||||
val timeString = uiState.mapData?.aosTime ?: "00:00:00"
|
||||
@@ -261,7 +270,8 @@ private fun MapScreen(
|
||||
// appear (GRID_ZOOM_SUB); grid labels now appear at that zoom too.
|
||||
if (zoom < MaidenheadGridOverlay.GRID_ZOOM_SUB) return false
|
||||
val grid = gridOfPoint(p.latitude, p.longitude) ?: return false
|
||||
if (grid !in workedGrids) return false
|
||||
// Every grid is tappable: worked grids show their confirmed QSOs,
|
||||
// unworked grids open the same dialog with an empty list.
|
||||
selectedGrid = grid
|
||||
return true
|
||||
}
|
||||
@@ -413,7 +423,8 @@ private fun MapScreen(
|
||||
grid = grid,
|
||||
qsos = uiState.workedGridQsos[grid].orEmpty().sortedBy { it.epochMs },
|
||||
isUtc = uiState.isUtc,
|
||||
onDismiss = { selectedGrid = null }
|
||||
onDismiss = { selectedGrid = null },
|
||||
onMatch = { onMatchGrid(grid) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -425,7 +436,8 @@ private fun WorkedGridQsoDialog(
|
||||
grid: String,
|
||||
qsos: List<com.rtbishop.look4sat.core.domain.model.GridQso>,
|
||||
isUtc: Boolean,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
onMatch: () -> Unit
|
||||
) {
|
||||
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
|
||||
// Centered card, ~85% width, internal scroll for long lists.
|
||||
@@ -434,29 +446,43 @@ private fun WorkedGridQsoDialog(
|
||||
modifier = Modifier.fillMaxWidth(0.88f)
|
||||
) {
|
||||
Column {
|
||||
// Header: grid + counts
|
||||
Column(Modifier.padding(horizontal = 16.dp, vertical = 10.dp)) {
|
||||
Text(
|
||||
text = grid,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
|
||||
)
|
||||
val distinctCalls = qsos.map { it.call }.distinct().size
|
||||
Text(
|
||||
text = stringResource(R.string.grid_qso_calls_count, distinctCalls, qsos.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
// Header: grid + counts on the left, "Match" action on the right
|
||||
// (jumps to the match page pre-filled with this grid).
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = grid,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
|
||||
)
|
||||
val distinctCalls = qsos.map { it.call }.distinct().size
|
||||
Text(
|
||||
text = stringResource(R.string.grid_qso_calls_count, distinctCalls, qsos.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
onMatch()
|
||||
}
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
androidx.compose.material3.HorizontalDivider()
|
||||
if (qsos.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.grid_qso_no_sat),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
} else {
|
||||
if (qsos.isNotEmpty()) {
|
||||
// Group by callsign preserving first-contact order (list is
|
||||
// already sorted oldest-first); expandable rows.
|
||||
val grouped = remember(qsos) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+31
-1
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user