diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index 52cf08a5..7f287051 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -123,13 +123,13 @@ class AprsForegroundService : Service() { // An unverified login needs its own message: the write succeeded, so a bare // failure notice would send the operator looking at their network when the // problem is the passcode - and APRS-IS is dropping every packet meanwhile. + // No receive-only notice. APRS-IS lets an unverified station connect and then + // discards its packets, but this app only reports its own position - there is no + // receiving side to it - so telling the operator they are "in receive-only mode" + // named a state that does not exist here. Without a passcode the packet does not + // arrive, and that is what the failure notice says. val msg = when { report.ok -> getString(R.string.aprs_toast_ok) - // Receive-only first: it logs in with -1 exactly as a wrong passcode does and - // the server answers "unverified" to both, so without this branch the one safe - // way to test a setup reported itself as a configuration error. - !report.verified && report.receiveOnly -> - getString(R.string.aprs_toast_receive_only) !report.verified -> getString(R.string.aprs_toast_unverified) else -> getString(R.string.aprs_toast_fail, report.detail) } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt index ed4fa666..67b39c24 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt @@ -52,7 +52,7 @@ data class AprsReport( * test a setup without putting anything on the network - was reported as a wrong passcode * and sent the operator to fix something they had set on purpose. */ - val receiveOnly: Boolean = false + ) /** Report scheduler (periodic + manual trigger); connection management lives in the foreground service */ @@ -127,13 +127,6 @@ class AprsReporter( } val packetLine = (beacon as AprsBeacon.Result.Line).text - // Receive-only is a deliberate choice and has to be told apart from a typo, because - // both log in with -1 and the server answers "unverified" to each. Classifying rather - // than asking canTransmit: that collapses ReceiveOnly and Mismatch into one boolean, - // so a mistyped passcode would be told it is in receive-only mode - the same - // mis-diagnosis as before, pointing the other way. - val wantsReceiveOnly = - AprsPasscode.classify(cfg.callsign, cfg.passcode) is AprsPasscode.Entry.ReceiveOnly val c = client ?: AprsIsClient( host = cfg.server, port = cfg.port, @@ -166,14 +159,13 @@ class AprsReporter( // report, say that instead. val reported = when { ok -> detail - refused && wantsReceiveOnly -> "receive-only, not forwarded" refused -> "login not verified" else -> detail } onReport( AprsReport( System.currentTimeMillis(), packetLine, ok, reported, - verified = !refused, receiveOnly = wantsReceiveOnly + verified = !refused ) ) if (ok) onState(AprsState.Connected) else onState(AprsState.Error) diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index 31bb11fb..810e8505 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -39,7 +39,6 @@ APRS: laporan terkirim APRS: laporan gagal - %1$s APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan. - APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim. APRS belum dikonfigurasi - isi panggilan dulu Laporan terakhir: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index c88f61a2..35b6749f 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -39,7 +39,6 @@ APRS: laporan terkirim APRS: laporan gagal - %1$s APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan. - APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim. APRS belum dikonfigurasi - isi panggilan dulu Laporan terakhir: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 91e8695e..77b0862d 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -40,7 +40,6 @@ APRS: rapor gönderildi APRS: rapor başarısız - %1$s APRS-IS passcode bilginizi doğrulamadı. Raporlarınız iletilmiyor - Ayarlar bölümünde çağrı işareti ve passcode bilgisini kontrol edin. - APRS-IS baglantiyi yalnizca alma modunda kabul etti, raporlariniz iletilmiyor. Gondermek icin passcode girin. APRS yapılandırılmadı - önce çağrı girin Son rapor: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index ce6ff4f9..c44f19e7 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -58,7 +58,6 @@ APRS: 上报成功 APRS: 上报失败 - %1$s APRS-IS 未验证你的 passcode, 上报不会被转发 - 请在设置里检查呼号与 passcode。 - APRS-IS 以只收模式接受了连接, 上报不会被转发。填入 passcode 才能发射。 APRS 未配置,请先填呼号 上次上报: %1$s %2$s 成功 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 9802d5c3..879701e2 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -59,7 +59,6 @@ APRS: report sent OK APRS: report failed - %1$s APRS-IS did not verify your passcode. Your reports are not being passed on - check the callsign and passcode in Settings. - APRS-IS accepted the connection in receive-only mode, so your reports are not forwarded. Enter your passcode to transmit. APRS not configured - set callsign first Last report: %1$s %2$s OK diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt index ae799e9b..8f77c306 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt @@ -51,8 +51,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import kotlinx.coroutines.launch +import androidx.compose.ui.draw.rotate +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -75,7 +77,6 @@ import kotlin.math.roundToInt * Not zero: an animated scroll settles a pixel or two short of the maximum, and an exact * comparison would drop out of follow-mode the moment it did. */ -private const val AUTOSCROLL_SLACK_PX = 4 /** * Full-page CW decoder backed by DeepCW. @@ -103,6 +104,10 @@ fun CwDecodeScreen() { } var permanentlyDenied by remember { mutableStateOf(false) } var isListening by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + // Hoisted so the toolbar button can reach it: the record below does not follow + // the decode, so jumping to the newest text has to be an explicit action. + val transcriptScroll = rememberScrollState() val decodedText by decoder.decodedText.collectAsState() val historyText by decoder.historyText.collectAsState() @@ -157,6 +162,17 @@ fun CwDecodeScreen() { .weight(1f) .padding(start = 12.dp) ) + // Jump to the newest text. The record below deliberately does not follow the decode, + // so this is how you get back to the bottom after reading earlier traffic. + IconButton(onClick = { scope.launch { transcriptScroll.animateScrollTo(transcriptScroll.maxValue) } }) { + Icon( + // The shared arrow points right; rotated to point down. Adding a second + // drawable for the same shape is how icon sets start to drift. + painter = painterResource(CoreR.drawable.ic_arrow), + contentDescription = stringResource(R.string.cw_scroll_newest), + modifier = Modifier.rotate(90f) + ) + } IconButton(onClick = { isListening = !isListening }) { Icon( painter = painterResource( @@ -232,34 +248,18 @@ fun CwDecodeScreen() { .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) ) { val transcript = (historyText + decodedText).ifEmpty { "…" } - val scroll = rememberScrollState() - // Follow the newest text, but stop as soon as the operator scrolls away, so - // reading back over earlier traffic is not undone by the next decode. + // This pane does not follow the decode. The single line above it is where new + // characters appear; this is the record, and a record that scrolls itself is worse + // than paper - you cannot read back over what just arrived because it keeps moving. // - // A boolean rather than comparing position against maxValue: maxValue is - // written during layout, after the composition that would read it, so such a - // comparison tests the previous frame's height and drifts short of the true - // bottom until it latches out of follow-mode altogether. - var following by remember { mutableStateOf(true) } - LaunchedEffect(scroll) { - snapshotFlow { scroll.isScrollInProgress to scroll.value } - .collect { (scrolling, value) -> - if (scrolling) following = value >= scroll.maxValue - AUTOSCROLL_SLACK_PX - } - } - LaunchedEffect(transcript, following) { - if (!following) return@LaunchedEffect - // Twice: the first pass lands at the height known when it started, the - // second covers growth that arrived while it was animating. - repeat(2) { - if (scroll.value < scroll.maxValue) scroll.animateScrollTo(scroll.maxValue) - } - } + // It used to auto-scroll to the bottom on every decode, dropping out of follow mode + // only once the operator had already fought it by scrolling away. Now it stays where + // it was put, and the button below jumps to the newest text on request. Text( text = transcript, modifier = Modifier .fillMaxSize() - .verticalScroll(scroll) + .verticalScroll(transcriptScroll) .padding(8.dp), fontSize = 16.sp, fontFamily = FontFamily.Monospace, diff --git a/feature/cw/src/main/res/values-id/app_values.xml b/feature/cw/src/main/res/values-id/app_values.xml index 3569a21a..d7092aea 100644 --- a/feature/cw/src/main/res/values-id/app_values.xml +++ b/feature/cw/src/main/res/values-id/app_values.xml @@ -3,6 +3,7 @@ Jeda pendekodean Lanjutkan pendekodean Hapus teks hasil dekode + Lompat ke teks terbaru Izin mikrofon diperlukan untuk pendekodean CW Berikan izin Buka pengaturan aplikasi diff --git a/feature/cw/src/main/res/values-in/app_values.xml b/feature/cw/src/main/res/values-in/app_values.xml index 3569a21a..d7092aea 100644 --- a/feature/cw/src/main/res/values-in/app_values.xml +++ b/feature/cw/src/main/res/values-in/app_values.xml @@ -3,6 +3,7 @@ Jeda pendekodean Lanjutkan pendekodean Hapus teks hasil dekode + Lompat ke teks terbaru Izin mikrofon diperlukan untuk pendekodean CW Berikan izin Buka pengaturan aplikasi diff --git a/feature/cw/src/main/res/values-tr/app_values.xml b/feature/cw/src/main/res/values-tr/app_values.xml index 52fe7962..6e66824a 100644 --- a/feature/cw/src/main/res/values-tr/app_values.xml +++ b/feature/cw/src/main/res/values-tr/app_values.xml @@ -3,6 +3,7 @@ Çözmeyi duraklat Çözmeyi sürdür Çözülen metni temizle + En yeni metne git CW çözümü için mikrofon izni gerekli İzin ver Uygulama ayarlarını aç diff --git a/feature/cw/src/main/res/values-zh/app_values.xml b/feature/cw/src/main/res/values-zh/app_values.xml index 42f7493f..a0e942ff 100644 --- a/feature/cw/src/main/res/values-zh/app_values.xml +++ b/feature/cw/src/main/res/values-zh/app_values.xml @@ -3,6 +3,7 @@ 暂停解码 继续解码 清空解码文本 + 跳到最新 CW 解码需要麦克风权限 授予权限 打开应用设置 diff --git a/feature/cw/src/main/res/values/app_values.xml b/feature/cw/src/main/res/values/app_values.xml index a7ec9789..2a3767da 100644 --- a/feature/cw/src/main/res/values/app_values.xml +++ b/feature/cw/src/main/res/values/app_values.xml @@ -3,6 +3,7 @@ Pause decoding Resume decoding Clear decoded text + Jump to the newest text Microphone permission is required for CW decoding Grant permission Open app settings