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.