feat(wavelog): group log entries by pass session (satellite + AOS timestamp)

User request: logs should be separated by satellite/pass. Each pass
session gets an ID = satellite name + AOS timestamp (the second the
elevation hits 0, from OrbitalPass.aosTime), e.g.
"ASRTU-1-20260804-2014". Log page groups entries by session:
group title (satellite - local time) + thick divider line between
groups (md --- style); legacy entries without sessionId fall into
"Ungrouped" at the end.

- WavelogQso: +sessionId (persisted in queue JSON)
- LogTab: sessionId built from satelliteName + aosTimeMs (radar page
  passes currentPass.aosTime)
- WavelogLogScreen: grouped rendering + wavelog_ungrouped string (4 locales)
- sessionId UTC yyyyMMdd-HHmm; display converts to local time

Verified: check_strings OK (9 files); :app:compileDebugKotlin
BUILD SUCCESSFUL. Not released (batched).
This commit is contained in:
mckero committed 2026-08-04 14:39:01 +00:00
1 parent a23bf34ccf
commit dd378990fe
8 files changed
+79 -3

No files matched your search

@@ -88,6 +88,7 @@ fun LogTab(
wavelogConfigured: Boolean,
showToast: (String) -> Unit,
txBaseFrequencyHz: Long? = null,
aosTimeMs: Long = 0L,
modifier: Modifier = Modifier
) {
var selectedUuid by remember { mutableStateOf<String?>(null) }
@@ -159,6 +160,7 @@ fun LogTab(
queue = queue,
showToast = showToast,
txBaseFrequencyHz = txBaseFrequencyHz,
aosTimeMs = aosTimeMs,
onSaved = { refreshTick++ }
)
}
@@ -225,6 +227,7 @@ private fun ExpandedLogInput(
queue: WavelogQueue,
showToast: (String) -> Unit,
txBaseFrequencyHz: Long? = null,
aosTimeMs: Long = 0L,
onSaved: () -> Unit
) {
var callsign by remember { mutableStateOf("") }
@@ -246,7 +249,8 @@ private fun ExpandedLogInput(
mode = mode.trim().ifBlank { "FM" }.uppercase(),
freqTxHz = tx,
freqRxHz = rx,
satName = satelliteName
satName = satelliteName,
sessionId = buildSessionId(satelliteName, aosTimeMs)
)
)
callsign = ""
@@ -448,3 +452,14 @@ private fun formatLocalTime(utcMs: Long): String {
cal.timeInMillis = utcMs
return "%02d:%02d".format(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
}
/** 场次 ID: 卫星名-AOS 时间戳(UTC yyyyMMdd-HHmm); 无 AOS 时用回车时刻 */
private fun buildSessionId(satName: String, aosTimeMs: Long): String {
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.timeInMillis = if (aosTimeMs > 0) aosTimeMs else System.currentTimeMillis()
val stamp = "%04d%02d%02d-%02d%02d".format(
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)
)
return "$satName-$stamp"
}
@@ -299,7 +299,8 @@ private fun PagerCard(
queue = wavelogQueue,
wavelogConfigured = wavelogConfigured,
showToast = showToast,
txBaseFrequencyHz = uiState.radioControl.txBaseFrequencyHz
txBaseFrequencyHz = uiState.radioControl.txBaseFrequencyHz,
aosTimeMs = uiState.currentPass?.aosTime ?: 0L
)
}
}
@@ -95,7 +95,27 @@ fun WavelogLogScreen(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp)
)
} else {
entries.forEachIndexed { index, entry ->
// 按场次分组(sessionId = 卫星名-AOS 时间戳); 空 sessionId 归"未分组"放最后
val groups = entries.groupBy { it.sessionId.ifBlank { UNGROUPED } }
.toSortedMap(compareByDescending<String> { id ->
entries.filter { it.sessionId.ifBlank { UNGROUPED } == id }.minOfOrNull { it.timeUtcMs } ?: 0L
}.thenByDescending { it })
groups.forEach { (sessionId, groupEntries) ->
// 组标题(场次名 + 本地时间)
val (satPart, timePart) = parseSession(sessionId)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = if (sessionId == UNGROUPED) stringResource(id = R.string.wavelog_ungrouped)
else "$satPart - ${formatSessionTime(timePart)}",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
)
}
// 组间横线(md --- 效果)
HorizontalDivider(thickness = 1.5.dp, color = GridLineColor)
groupEntries.forEach { entry ->
SwipeDeleteRow(
onDelete = {
queue.remove(entry.id)
@@ -133,6 +153,7 @@ fun WavelogLogScreen(
}
}
}
}
}
}
}
@@ -173,6 +194,38 @@ private fun RowScope.Cell(text: String, width: Dp, weight: Float = 0f) {
)
}
private const val UNGROUPED = "__ungrouped__"
/** 解析场次 ID: "ASRTU-1-20260804-2014" → (卫星名, "20260804-2014") */
private fun parseSession(sessionId: String): Pair<String, String> {
val parts = sessionId.split('-')
if (parts.size < 3) return sessionId to ""
val timePart = parts.takeLast(2).joinToString("-")
val satPart = parts.dropLast(2).joinToString("-")
return satPart to timePart
}
/** 场次时间(UTC "20260804-2014") → 本地 "MM-dd HH:mm" */
private fun formatSessionTime(timePart: String): String {
if (timePart.length != 13 || timePart[8] != '-') return timePart
return try {
val year = timePart.substring(0, 4).toInt()
val month = timePart.substring(4, 6).toInt()
val day = timePart.substring(6, 8).toInt()
val hour = timePart.substring(9, 11).toInt()
val minute = timePart.substring(11, 13).toInt()
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.set(year, month - 1, day, hour, minute, 0)
cal.timeInMillis = cal.timeInMillis + TimeZone.getDefault().getOffset(cal.timeInMillis)
"%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)
)
} catch (_: Exception) {
timePart
}
}
private fun formatLocalTime(utcMs: Long): String {
val cal = Calendar.getInstance(TimeZone.getDefault())
cal.timeInMillis = utcMs