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