mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-06 12:37:26 +00:00
fix(wavelog,radar): sat name LoTW normalization, full grid lines, RX freq restored (4.5.2 overwrite 4)
This commit is contained in:
1 parent
ab1a0c0891
commit
0bcf73e1f9
3 files changed
+146
-78
No files matched your search
@@ -106,6 +106,19 @@ object WaveLogApi {
|
|||||||
WavelogResult.Success("")
|
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) */
|
/** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */
|
||||||
suspend fun postQso(
|
suspend fun postQso(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -117,6 +130,8 @@ object WaveLogApi {
|
|||||||
val base = normalizeUrl(url)
|
val base = normalizeUrl(url)
|
||||||
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
|
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
|
||||||
|
|
||||||
|
val satName = normalizeSatName(qso.satName)
|
||||||
|
|
||||||
// v2: POST /index.php/api/v2/qso(JSON 字段)
|
// v2: POST /index.php/api/v2/qso(JSON 字段)
|
||||||
val v2Body = JSONObject().apply {
|
val v2Body = JSONObject().apply {
|
||||||
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
|
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
|
||||||
@@ -128,7 +143,7 @@ object WaveLogApi {
|
|||||||
put("freq", qso.freqTxHz)
|
put("freq", qso.freqTxHz)
|
||||||
put("freq_rx", qso.freqRxHz)
|
put("freq_rx", qso.freqRxHz)
|
||||||
put("gridsquare", gridsquare)
|
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())
|
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
|
||||||
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
|
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
|
||||||
@@ -139,7 +154,7 @@ object WaveLogApi {
|
|||||||
put("key", apiKey)
|
put("key", apiKey)
|
||||||
put("station_profile_id", stationProfileId)
|
put("station_profile_id", stationProfileId)
|
||||||
put("type", "adif")
|
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())
|
val (code1, resp1) = httpRequest("$base/index.php/api/qso", "POST", apiKey, v1Body.toString())
|
||||||
if (code1 in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
|
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 })}")
|
WavelogResult.Failure("上传失败: v2 HTTP $code, v1 HTTP $code1 — ${shortError(resp1.ifBlank { resp1b })}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数) */
|
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数, sat_name 已规范化) */
|
||||||
private fun toAdif(qso: WavelogQso, gridsquare: String): String {
|
private fun toAdif(qso: WavelogQso, gridsquare: String, satName: String): String {
|
||||||
fun field(name: String, value: String): String {
|
fun field(name: String, value: String): String {
|
||||||
val bytes = value.toByteArray(Charsets.UTF_8).size
|
val bytes = value.toByteArray(Charsets.UTF_8).size
|
||||||
return "<$name:$bytes>$value"
|
return "<$name:$bytes>$value"
|
||||||
@@ -168,8 +183,8 @@ object WaveLogApi {
|
|||||||
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
|
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
|
||||||
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
|
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
|
||||||
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
|
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
|
||||||
if (qso.satName.isNotBlank()) {
|
if (satName.isNotBlank()) {
|
||||||
append(field("sat_name", qso.satName))
|
append(field("sat_name", satName))
|
||||||
append(field("prop_mode", "SAT"))
|
append(field("prop_mode", "SAT"))
|
||||||
}
|
}
|
||||||
append("<eor>")
|
append("<eor>")
|
||||||
|
|||||||
@@ -253,15 +253,19 @@ private fun ExpandedLogInput(
|
|||||||
showToast(savedMsg)
|
showToast(savedMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 频率显示与转发器栏完全一致(不再另算): TX = 当前调谐(txBaseFrequencyHz), 线性附加频段范围
|
// 频率显示与转发器栏完全一致: TX 行 + RX 行(线性附加频段范围)
|
||||||
val txForDisplay = txBaseFrequencyHz ?: (radio.uplinkLow ?: radio.downlinkLow ?: 0L)
|
val txForDisplay = txBaseFrequencyHz ?: (radio.uplinkLow ?: radio.downlinkLow ?: 0L)
|
||||||
|
val rxForDisplay = radio.downlinkLow ?: radio.uplinkLow ?: 0L
|
||||||
val upLow = radio.uplinkLow
|
val upLow = radio.uplinkLow
|
||||||
val upHigh = radio.uplinkHigh
|
val upHigh = radio.uplinkHigh
|
||||||
|
val downLow = radio.downlinkLow
|
||||||
|
val downHigh = radio.downlinkHigh
|
||||||
val isLinearRange = upLow != null && upHigh != null && upLow != upHigh
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = "TX ",
|
text = "TX: ",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
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(
|
OutlinedTextField(
|
||||||
value = callsign,
|
value = callsign,
|
||||||
onValueChange = { callsign = it.take(12) },
|
onValueChange = { callsign = it.take(12) },
|
||||||
|
|||||||
+97
-69
@@ -2,8 +2,7 @@
|
|||||||
* WavelogLogScreen.kt — 「日志」页面(更多菜单, 4.5.2 修复)。
|
* WavelogLogScreen.kt — 「日志」页面(更多菜单, 4.5.2 修复)。
|
||||||
*
|
*
|
||||||
* 表格形式展示本地存储的日志: 时间 | 频率 | 卫星 | 呼号 | 已上传(✓)。
|
* 表格形式展示本地存储的日志: 时间 | 频率 | 卫星 | 呼号 | 已上传(✓)。
|
||||||
* 数据来自 WavelogQueue(与雷达页 Log 标签共用)。空态提示; 表格行
|
* 完整格子线(表头横线 + 行横线 + 列竖线 + 外边框); 行复用左滑删除。
|
||||||
* 复用左滑删除(同 LogTab 交互)。
|
|
||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.feature.radar
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -35,6 +33,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
|
||||||
@@ -44,6 +43,7 @@ import java.util.Calendar
|
|||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
private val CheckGreen = Color(0xFF4CAF50)
|
private val CheckGreen = Color(0xFF4CAF50)
|
||||||
|
private val GridLineColor = Color(0xFF3A3A3A)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WavelogLogScreen(
|
fun WavelogLogScreen(
|
||||||
@@ -57,66 +57,80 @@ fun WavelogLogScreen(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(8.dp),
|
.padding(8.dp)
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
) {
|
||||||
// 表头
|
// 表格外边框(左右 + 上下由表头/行底线构成)
|
||||||
Row(
|
Column(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
HeaderCell(stringResource(id = R.string.wavelog_col_time), 52.dp)
|
// 表头
|
||||||
HeaderCell(stringResource(id = R.string.wavelog_col_freq), 70.dp)
|
Row(
|
||||||
HeaderCell(stringResource(id = R.string.wavelog_col_sat), 0.dp, weight = 1.2f)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
HeaderCell(stringResource(id = R.string.wavelog_col_call), 0.dp, weight = 1f)
|
modifier = Modifier
|
||||||
HeaderCell(stringResource(id = R.string.wavelog_col_uploaded), 44.dp)
|
.fillMaxWidth()
|
||||||
}
|
.height(38.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()) {
|
if (entries.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.wavelog_empty),
|
text = stringResource(id = R.string.wavelog_empty),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
entries.forEachIndexed { index, entry ->
|
entries.forEachIndexed { index, entry ->
|
||||||
SwipeDeleteRow(
|
SwipeDeleteRow(
|
||||||
onDelete = {
|
onDelete = {
|
||||||
queue.remove(entry.id)
|
queue.remove(entry.id)
|
||||||
refreshTick++
|
refreshTick++
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
.height(36.dp)
|
||||||
.padding(horizontal = 10.dp, vertical = 8.dp)
|
) {
|
||||||
) {
|
Cell(formatLocalTime(entry.timeUtcMs), 72.dp)
|
||||||
Cell(formatLocalTime(entry.timeUtcMs), 74.dp)
|
GridVLine()
|
||||||
Cell(formatFrequency(entry.freqTxHz), 62.dp)
|
Cell(formatFrequency(entry.freqTxHz), 64.dp)
|
||||||
Cell(entry.satName, 0.dp, weight = 1.2f)
|
GridVLine()
|
||||||
Cell(entry.call, 0.dp, weight = 1f)
|
Cell(entry.satName, 0.dp, weight = 1.2f)
|
||||||
Box(modifier = Modifier.width(44.dp)) {
|
GridVLine()
|
||||||
if (entry.uploaded) {
|
Cell(entry.call, 0.dp, weight = 1f)
|
||||||
Text(
|
GridVLine()
|
||||||
text = "✓",
|
Box(modifier = Modifier.width(44.dp)) {
|
||||||
fontSize = 15.sp,
|
if (entry.uploaded) {
|
||||||
fontWeight = FontWeight.Bold,
|
Text(
|
||||||
color = CheckGreen
|
text = "✓",
|
||||||
)
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = CheckGreen
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 行分隔线
|
||||||
|
HorizontalDivider(thickness = 1.dp, color = GridLineColor)
|
||||||
}
|
}
|
||||||
// 表格分隔线
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,32 +138,46 @@ fun WavelogLogScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 列竖线 */
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.HeaderCell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
|
private fun GridVLine() {
|
||||||
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
|
Box(
|
||||||
Text(
|
modifier = Modifier
|
||||||
text = text,
|
.width(1.dp)
|
||||||
fontSize = 11.sp,
|
.fillMaxHeight()
|
||||||
fontWeight = FontWeight.Bold,
|
.background(GridLineColor)
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = mod
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowScope.Cell(text: String, width: androidx.compose.ui.unit.Dp, weight: Float = 0f) {
|
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 = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = mod.padding(horizontal = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.Cell(text: String, width: Dp, weight: Float = 0f) {
|
||||||
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
|
val mod = if (weight > 0f) Modifier.weight(weight) else Modifier.width(width)
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = mod
|
modifier = mod.padding(horizontal = 6.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatLocalTime(utcMs: Long): String {
|
private fun formatLocalTime(utcMs: Long): String {
|
||||||
val cal = Calendar.getInstance(TimeZone.getDefault())
|
val cal = Calendar.getInstance(TimeZone.getDefault())
|
||||||
cal.timeInMillis = utcMs
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user