feat(wavelog): QRZ grid lookup, LoTW satellite list refresh, RST 59

User-prioritized WaveLog fixes (4.5.5, commit-only per instruction):

- QRZ 对方网格爬虫: QrzGridClient (domain, pure JVM) fetches
  https://www.qrz.com/db/{call} with user-supplied cookies (parses
  EditThisCookie JSON or raw "k=v; k=v"), extracts Grid Square from
  the Detail table. Cookies NEVER built in - entered in settings.
- Settings: WaveLog card top-right gear opens QRZ cookie dialog with
  test query button (detects logged-in callsign from cookie, looks up
  its grid, shows result or failure in the dialog).
- LogTab: on Enter, async lookup of the other station's grid ->
  queue.updateGridsquare -> uploaded with QSO (postQso gridsquare).
- LoTW satellite list: 112 names embedded (lotw.arrl.org config.tq6),
  normalizeSatName maps Celestrak TLE names to LoTW names (SAUDISAT-1C
  -> SO-50, FUNCUBE-1 -> AO-73, DIWATA-2B -> PO-101, ZARYA/ARISS ->
  ARISS); "Update sats" button in WaveLog card downloads the live list
  (LotwSatellitesRepo, SharedPreferences persisted, never in build).
- RST: rst_sent/rst_rcvd = 59/59 in both v1 ADIF and v2 JSON.
- Grid mismatch dialog kept (cloud station grid vs station QTH);
  QSO gridsquare no longer uses station grid.
- New strings in 5 locales; check_strings 9 files OK.
Verified: core:domain/data + feature:settings/radar + app
compileDebugKotlin BUILD SUCCESSFUL.
This commit is contained in:
mckero committed 2026-08-05 06:23:47 +00:00
1 parent 2c26272e0b
commit 0e767f4250
18 files changed
+375 -20

No files matched your search

@@ -45,11 +45,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -72,6 +74,7 @@ import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.formatFrequency
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.TimeZone
import java.util.UUID
@@ -92,6 +95,8 @@ fun LogTab(
aosTimeMs: Long = 0L,
modifier: Modifier = Modifier
) {
val context = androidx.compose.ui.platform.LocalContext.current
val scope = rememberCoroutineScope()
var selectedUuid by remember { mutableStateOf<String?>(null) }
var refreshTick by remember { mutableIntStateOf(0) }
val entries = remember(refreshTick) { queue.all() }
@@ -244,6 +249,8 @@ private fun ExpandedLogInput(
aosTimeMs: Long = 0L,
onSaved: () -> Unit
) {
val context = androidx.compose.ui.platform.LocalContext.current
val scope = rememberCoroutineScope()
var callsign by remember { mutableStateOf("") }
var mode by remember { mutableStateOf(radio.uplinkMode ?: "FM") }
@@ -255,9 +262,10 @@ private fun ExpandedLogInput(
// 频率直接取转发器栏的数字(radio 每秒多普勒修正, 回车那一秒的值)
val tx = radio.uplinkLow ?: radio.downlinkLow ?: 0L
val rx = radio.downlinkLow ?: radio.uplinkLow ?: 0L
val qsoId = UUID.randomUUID().toString()
queue.add(
WavelogQso(
id = UUID.randomUUID().toString(),
id = qsoId,
timeUtcMs = System.currentTimeMillis(),
call = call,
mode = mode.trim().ifBlank { "FM" }.uppercase(),
@@ -270,6 +278,17 @@ private fun ExpandedLogInput(
callsign = ""
onSaved()
showToast(savedMsg)
// QRZ 对方网格异步回填(4.5.5): 设置页填了 Cookie 才查, 失败静默
scope.launch {
val prefs = context.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
val rawCookie = prefs.getString("cookie", "") ?: ""
if (rawCookie.isNotBlank()) {
val grid = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.lookupGrid(
call, com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.parseCookies(rawCookie)
)
if (grid != null) queue.updateGridsquare(qsoId, grid)
}
}
}
// 频率显示与转发器栏完全一致: TX 行 + RX 行(radio 来自每秒多普勒重算, 实时刷新)
@@ -50,6 +50,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -64,6 +65,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@@ -109,6 +111,7 @@ import com.rtbishop.look4sat.core.presentation.isVerticalLayout
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.launch
@Composable
fun SettingsDestination() {
@@ -116,7 +119,7 @@ fun SettingsDestination() {
val container = (context.applicationContext as IContainerProvider).getMainContainer()
val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.factory(container))
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
SettingsScreen(uiState, viewModel::onAction)
SettingsScreen(uiState, viewModel::onAction, container.provideLotwSatellitesRepo())
// WaveLog 网格不一致确认弹窗(4.5.2)
val gridConfirm = viewModel.gridConfirm
@@ -177,7 +180,11 @@ fun SettingsDestination() {
}
@Composable
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
private fun SettingsScreen(
uiState: SettingsState,
onAction: (SettingsAction) -> Unit,
lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
) {
val dialogs = rememberDialogVisibility()
val pendingCustomSourcesGrant = remember { mutableStateOf<(() -> Unit)?>(null) }
val pendingCustomSourcesDeny = remember { mutableStateOf<(() -> Unit)?>(null) }
@@ -392,7 +399,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
)
}
item { OtherCard(uiState.otherSettings, onAction) }
item { WavelogCard(uiState.otherSettings, onAction) }
item { WavelogCard(uiState.otherSettings, onAction, lotwRepo) }
item {
UiSettingsCard(
hiddenScreens = uiState.otherSettings.hiddenScreens,
@@ -629,17 +636,35 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
@Composable
private fun WavelogCard(
settings: OtherSettings,
onAction: (SettingsAction) -> Unit
onAction: (SettingsAction) -> Unit,
lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
) {
val qrzContext = LocalContext.current
var showQrzDialog by remember { mutableStateOf(false) }
var qrzCookie by remember {
mutableStateOf(
qrzContext.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
.getString("cookie", "") ?: ""
)
}
var qrzTestResult by remember { mutableStateOf<String?>(null) }
var qrzTesting by remember { mutableStateOf(false) }
val qrzScope = rememberCoroutineScope()
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_wavelog_title),
color = MaterialTheme.colorScheme.primary
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(id = R.string.prefs_wavelog_title),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { showQrzDialog = true }) {
Text("\u2699")
}
}
OutlinedTextField(
value = settings.wavelogUrl,
onValueChange = {
@@ -680,6 +705,11 @@ private fun WavelogCard(
text = stringResource(id = R.string.prefs_wavelog_test),
modifier = Modifier.weight(1f)
)
CardButton(
onClick = { onAction(SettingsAction.UpdateLotwSatellites) },
text = stringResource(id = R.string.prefs_wavelog_update_sats),
modifier = Modifier.weight(1f)
)
CardButton(
onClick = { onAction(SettingsAction.UploadWavelogQueue) },
text = stringResource(id = R.string.prefs_wavelog_upload),
@@ -688,6 +718,77 @@ private fun WavelogCard(
}
}
}
if (showQrzDialog) {
AlertDialog(
onDismissRequest = { showQrzDialog = false },
title = { Text(text = stringResource(id = R.string.prefs_wavelog_qrz_title)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(
text = stringResource(id = R.string.prefs_wavelog_qrz_hint),
fontSize = 12.sp
)
OutlinedTextField(
value = qrzCookie,
onValueChange = { qrzCookie = it },
label = { Text(stringResource(id = R.string.prefs_wavelog_qrz_label)) },
minLines = 3,
maxLines = 8,
textStyle = LocalTextStyle.current.copy(fontSize = 12.sp),
modifier = Modifier.fillMaxWidth()
)
if (qrzTestResult != null) {
Text(
text = qrzTestResult!!,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
TextButton(
onClick = {
qrzTesting = true
qrzTestResult = null
qrzScope.launch {
val header = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.parseCookies(qrzCookie)
if (header.isBlank()) {
qrzTestResult = "Cookie 为空, 请先粘贴"
qrzTesting = false
return@launch
}
val own = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.fetchOwnCallsign(header)
if (own == null) {
qrzTestResult = "Cookie 无效或已过期(无法识别登录呼号)"
} else {
val grid = com.rtbishop.look4sat.core.domain.qrz.QrzGridClient.lookupGrid(own, header)
qrzTestResult = if (grid != null) {
"登录呼号: $own 网格: $grid ✅"
} else {
"登录呼号: $own 网格: 未填写"
}
}
qrzTesting = false
}
},
enabled = !qrzTesting
) {
Text(text = if (qrzTesting) "测试中…" else "测试查询")
}
}
},
confirmButton = {
TextButton(onClick = {
qrzContext.getSharedPreferences("qrz_cookie", android.content.Context.MODE_PRIVATE)
.edit().putString("cookie", qrzCookie.trim()).apply()
showQrzDialog = false
}) { Text(text = stringResource(id = R.string.btn_accept)) }
},
dismissButton = {
TextButton(onClick = { showQrzDialog = false }) {
Text(text = stringResource(id = R.string.wavelog_cancel))
}
}
)
}
}
@Composable
@@ -82,6 +82,8 @@ sealed interface SettingsAction {
data object TestWavelogConnection : SettingsAction
// WaveLog: 手动上传队列
data object UploadWavelogQueue : SettingsAction
// WaveLog: 更新 LoTW 卫星列表
data object UpdateLotwSatellites : SettingsAction
// Remote control
data class UpdateRC(val settings: RCSettings) : SettingsAction
@@ -41,7 +41,8 @@ import androidx.compose.runtime.setValue
class SettingsViewModel(
private val databaseRepo: IDatabaseRepo,
private val settingsRepo: ISettingsRepo,
private val showToast: IShowToast
private val showToast: IShowToast,
private val lotwRepo: com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo
) : ViewModel() {
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
@@ -156,6 +157,7 @@ class SettingsViewModel(
}
SettingsAction.TestWavelogConnection -> testWavelogConnection()
SettingsAction.UploadWavelogQueue -> uploadWavelogQueue()
SettingsAction.UpdateLotwSatellites -> updateLotwSatellites()
// System
is SettingsAction.ShowToast -> showToast(action.message)
}
@@ -264,6 +266,17 @@ class SettingsViewModel(
}
}
private fun updateLotwSatellites() {
viewModelScope.launch {
when (val r = lotwRepo.refresh()) {
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Ok ->
showToast("LoTW 卫星列表已更新: ${r.count} 个")
is com.rtbishop.look4sat.core.domain.wavelog.ILotwSatellitesRepo.RefreshResult.Error ->
wavelogError = "LoTW 卫星列表更新失败: ${r.message}"
}
}
}
private fun uploadWavelogQueue() {
viewModelScope.launch {
val result = pendingUploader?.uploadQueue()
@@ -306,7 +319,8 @@ class SettingsViewModel(
SettingsViewModel(
databaseRepo = container.databaseRepo,
settingsRepo = container.settingsRepo,
showToast = container.provideShowToast()
showToast = container.provideShowToast(),
lotwRepo = container.provideLotwSatellitesRepo()
).apply { pendingUploader = container.provideWavelogUploader() }
}
}