From cb3ebe78707130544b8c57e655db7a45154fafe6 Mon Sep 17 00:00:00 2001 From: QIU Date: Wed, 26 Aug 2026 10:38:24 +0000 Subject: [PATCH] feat(log): the counterpart grid can be typed On FM satellites the grid is the exchange - it is what the other station sends you and what you send back. Until now it could only arrive by scraping QRZ, which needs a cookie the operator may not have pasted, and which returns nothing at all for a station with no locator on file. So the field that carries the actual content of an FM contact was the one field the operator could not fill in. It is the second field, optional, six characters, uppercased. A typed grid also skips the QRZ lookup entirely rather than racing it: what the operator heard on the air beats what a web page says, and letting the scrape overwrite it would silently replace good data with a guess. The value is captured before the field clears, so the QSO carries it and the next contact starts empty. WavelogQso.gridsquare already existed for the scraper to fill, so nothing about the stored shape changes and no migration is needed. This was the interaction study's second-ranked conclusion, after the editable time. Both come from the same observation: the screen was built for someone typing during a pass, and the operators it is for are working the radio instead. --- .../src/main/res/values-zh/strings.xml | 2 ++ .../src/main/res/values/strings.xml | 2 ++ .../rtbishop/look4sat/feature/radar/LogTab.kt | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index e56b46b1..8359625f 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -276,6 +276,8 @@ 日志 选择转发器 对方呼号 + 网格 (可选) + 对方报给你的网格。留空则从 QRZ 查询。 模式 修改模式 时间 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 1cf5724a..b0358b64 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -307,6 +307,8 @@ Log Select transponder Callsign + Grid (optional) + What they sent you. Left empty, it is looked up on QRZ. Mode Edit the mode Time diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt index dcbee301..30e01720 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt @@ -295,6 +295,10 @@ private fun ExpandedLogInput( // deliberately NOT restored when the user swipes the app away - that is them ending the session. var timeEntry by rememberSaveable { mutableStateOf("") } var timeEditable by rememberSaveable { mutableStateOf(false) } + // Typed by the operator. On FM satellites the grid IS the exchange, and until now it could only + // arrive by scraping QRZ - which needs a cookie the operator may not have and cannot supply for + // a station that has no locator on file. + var gridEntry by rememberSaveable { mutableStateOf("") } // Calls logged during this pass, so a repeat can be mentioned without being blocked: the same // station on a later pass is a legitimate new contact. This replaces a 300ms window that // swallowed what it guessed were accidental double submissions - a guess that could discard @@ -333,6 +337,7 @@ private fun ExpandedLogInput( val tx = radio.uplinkLow ?: radio.downlinkLow ?: 0L val rx = radio.downlinkLow ?: radio.uplinkLow ?: 0L val qsoId = UUID.randomUUID().toString() + val typedGrid = gridEntry.trim().uppercase() queue.add( WavelogQso( id = qsoId, @@ -347,10 +352,12 @@ private fun ExpandedLogInput( freqRxHz = rx, satName = satelliteName, catnum = satelliteCatnum, + gridsquare = typedGrid, sessionId = buildSessionId(satelliteName, aosTimeMs) ) ) callsign = "" + gridEntry = "" workedThisSession = workedThisSession + call onSaved() showToast( @@ -366,6 +373,9 @@ private fun ExpandedLogInput( // Grid backfill. The view model owns the cookie and the request; this used to read // SharedPreferences through LocalContext right here, inside composition. Failures now say // something: an expired cookie was indistinguishable from a station with no grid filed. + // A typed grid is better evidence than a scrape, so the lookup is skipped entirely rather + // than allowed to overwrite it. + if (typedGrid.isNotBlank()) return onLookupGrid(call) { outcome -> when (outcome) { is QrzGrid.Found -> { @@ -441,6 +451,26 @@ private fun ExpandedLogInput( textStyle = MaterialTheme.typography.bodyMedium, modifier = Modifier.fillMaxWidth() ) + // The counterpart grid, optional and second. On FM satellites this is the exchange, and it + // could previously only arrive by scraping QRZ - which needs a cookie, and returns nothing + // for a station with no locator on file. Typed here it also skips the lookup entirely, + // because what the operator heard beats what a web page says. + OutlinedTextField( + value = gridEntry, + onValueChange = { gridEntry = it.take(6).uppercase() }, + label = { Text(stringResource(id = R.string.wavelog_grid_hint)) }, + supportingText = { + Text( + text = stringResource(id = R.string.wavelog_grid_help), + style = MaterialTheme.typography.bodySmall + ) + }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { submit() }), + textStyle = MaterialTheme.typography.bodyMedium, + modifier = Modifier.fillMaxWidth() + ) // The mode comes from the transponder, so it is shown rather than typed - one less field // during a pass that lasts eight minutes. Tapping it reveals the field, because a // transponder record can be wrong and the operator has to be able to say so.