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:
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 来自每秒多普勒重算, 实时刷新)
|
||||
|
||||
+109
-8
@@ -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
|
||||
|
||||
+16
-2
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user