fix(wavelog,radar): sat name LoTW normalization, full grid lines, RX freq restored (4.5.2 overwrite 4)

This commit is contained in:
mckero committed 2026-08-04 12:29:31 +00:00
1 parent ab1a0c0891
commit 0bcf73e1f9
3 files changed
+103 -35

No files matched your search

@@ -106,6 +106,19 @@ object WaveLogApi {
WavelogResult.Success("")
}
/** LoTW 认可的卫星名: 取括号前主名 + 大写(ISS 特判) */
fun normalizeSatName(raw: String): String {
val main = raw.substringBefore('(').trim()
.ifBlank { raw.trim() }
.uppercase(Locale.ENGLISH)
// ISS 特判: 主名是 ISS/ZARYA/ARISS 变体 → ISS(LoTW 认可名)
return when {
main == "ZARYA" || main.startsWith("ISS") -> "ISS"
main == "ARISS" -> "ISS"
else -> main
}
}
/** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */
suspend fun postQso(
url: String,
@@ -117,6 +130,8 @@ object WaveLogApi {
val base = normalizeUrl(url)
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
val satName = normalizeSatName(qso.satName)
// v2: POST /index.php/api/v2/qso(JSON 字段)
val v2Body = JSONObject().apply {
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
@@ -128,7 +143,7 @@ object WaveLogApi {
put("freq", qso.freqTxHz)
put("freq_rx", qso.freqRxHz)
put("gridsquare", gridsquare)
put("sat_name", qso.satName)
put("sat_name", satName)
}
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
@@ -139,7 +154,7 @@ object WaveLogApi {
put("key", apiKey)
put("station_profile_id", stationProfileId)
put("type", "adif")
put("string", toAdif(qso, gridsquare))
put("string", toAdif(qso, gridsquare, satName))
}
val (code1, resp1) = httpRequest("$base/index.php/api/qso", "POST", apiKey, v1Body.toString())
if (code1 in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
@@ -151,8 +166,8 @@ object WaveLogApi {
WavelogResult.Failure("上传失败: v2 HTTP $code, v1 HTTP $code1 — ${shortError(resp1.ifBlank { resp1b })}")
}
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数) */
private fun toAdif(qso: WavelogQso, gridsquare: String): String {
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数, sat_name 已规范化) */
private fun toAdif(qso: WavelogQso, gridsquare: String, satName: String): String {
fun field(name: String, value: String): String {
val bytes = value.toByteArray(Charsets.UTF_8).size
return "<$name:$bytes>$value"
@@ -168,8 +183,8 @@ object WaveLogApi {
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
if (qso.satName.isNotBlank()) {
append(field("sat_name", qso.satName))
if (satName.isNotBlank()) {
append(field("sat_name", satName))
append(field("prop_mode", "SAT"))
}
append("<eor>")
@@ -253,15 +253,19 @@ private fun ExpandedLogInput(
showToast(savedMsg)
}
// 频率显示与转发器栏完全一致(不再另算): TX = 当前调谐(txBaseFrequencyHz), 线性附加频段范围
// 频率显示与转发器栏完全一致: TX 行 + RX 行(线性附加频段范围)
val txForDisplay = txBaseFrequencyHz ?: (radio.uplinkLow ?: radio.downlinkLow ?: 0L)
val rxForDisplay = radio.downlinkLow ?: radio.uplinkLow ?: 0L
val upLow = radio.uplinkLow
val upHigh = radio.uplinkHigh
val downLow = radio.downlinkLow
val downHigh = radio.downlinkHigh
val isLinearRange = upLow != null && upHigh != null && upLow != upHigh
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
// TX 行
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "TX ",
text = "TX: ",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -279,6 +283,27 @@ private fun ExpandedLogInput(
)
}
}
// RX 行(与转发器栏 RX 一致)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "RX: ",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${formatFrequency(rxForDisplay)} MHz",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
if (downLow != null && downHigh != null && downLow != downHigh) {
Text(
text = " (${formatFrequency(downLow)} – ${formatFrequency(downHigh)})",
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
OutlinedTextField(
value = callsign,
onValueChange = { callsign = it.take(12) },
@@ -2,8 +2,7 @@
* WavelogLogScreen.kt — 「日志」页面(更多菜单, 4.5.2 修复)。
*
* 表格形式展示本地存储的日志: 时间 | 频率 | 卫星 | 呼号 | 已上传(✓)。
* 数据来自 WavelogQueue(与雷达页 Log 标签共用)。空态提示; 表格行
* 复用左滑删除(同 LogTab 交互)。
* 完整格子线(表头横线 + 行横线 + 列竖线 + 外边框); 行复用左滑删除。
*/
package com.rtbishop.look4sat.feature.radar
@@ -13,21 +12,20 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -35,6 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
@@ -44,6 +43,7 @@ import java.util.Calendar
import java.util.TimeZone
private val CheckGreen = Color(0xFF4CAF50)
private val GridLineColor = Color(0xFF3A3A3A)
@Composable
fun WavelogLogScreen(
@@ -57,29 +57,42 @@ fun WavelogLogScreen(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
.padding(8.dp)
) {
// 表格外边框(左右 + 上下由表头/行底线构成)
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer)
) {
// 表头
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 6.dp)
.height(38.dp)
) {
HeaderCell(stringResource(id = R.string.wavelog_col_time), 52.dp)
HeaderCell(stringResource(id = R.string.wavelog_col_freq), 70.dp)
HeaderCell(stringResource(id = R.string.wavelog_col_time), 72.dp)
GridVLine()
HeaderCell(stringResource(id = R.string.wavelog_col_freq), 64.dp)
GridVLine()
HeaderCell(stringResource(id = R.string.wavelog_col_sat), 0.dp, weight = 1.2f)
GridVLine()
HeaderCell(stringResource(id = R.string.wavelog_col_call), 0.dp, weight = 1f)
GridVLine()
HeaderCell(stringResource(id = R.string.wavelog_col_uploaded), 44.dp)
}
// 表头横线
HorizontalDivider(thickness = 1.5.dp, color = GridLineColor)
// 表头与数据区留白
Box(modifier = Modifier.height(8.dp))
if (entries.isEmpty()) {
Text(
text = stringResource(id = R.string.wavelog_empty),
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 12.dp)
modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp)
)
} else {
entries.forEachIndexed { index, entry ->
@@ -94,13 +107,16 @@ fun WavelogLogScreen(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 10.dp, vertical = 8.dp)
.height(36.dp)
) {
Cell(formatLocalTime(entry.timeUtcMs), 74.dp)
Cell(formatFrequency(entry.freqTxHz), 62.dp)
Cell(formatLocalTime(entry.timeUtcMs), 72.dp)
GridVLine()
Cell(formatFrequency(entry.freqTxHz), 64.dp)
GridVLine()
Cell(entry.satName, 0.dp, weight = 1.2f)
GridVLine()
Cell(entry.call, 0.dp, weight = 1f)
GridVLine()
Box(modifier = Modifier.width(44.dp)) {
if (entry.uploaded) {
Text(
@@ -112,11 +128,9 @@ fun WavelogLogScreen(
}
}
}
// 表格分隔线
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)
)
// 行分隔线
HorizontalDivider(thickness = 1.dp, color = GridLineColor)
}
}
}
}
@@ -124,32 +138,46 @@ fun WavelogLogScreen(
}
}
/** 列竖线 */
@Composable
private fun RowScope.HeaderCell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
private fun GridVLine() {
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(GridLineColor)
)
}
@Composable
private fun RowScope.HeaderCell(text: String, width: Dp, weight: Float = 0f) {
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
Text(
text = text,
fontSize = 11.sp,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
modifier = mod
modifier = mod.padding(horizontal = 6.dp)
)
}
@Composable
private fun RowScope.Cell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
private fun RowScope.Cell(text: String, width: Dp, weight: Float = 0f) {
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
Text(
text = text,
fontSize = 12.sp,
maxLines = 1,
modifier = mod
modifier = mod.padding(horizontal = 6.dp)
)
}
private fun formatLocalTime(utcMs: Long): String {
val cal = Calendar.getInstance(TimeZone.getDefault())
cal.timeInMillis = utcMs
return "%02d-%02d %02d:%02d".format(cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
return "%02d-%02d %02d:%02d".format(
cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)
)
}