feat(logbook): LoTW 日志本官方名统一、上传候选集、编辑已传记录新建、记录分钟/kHz 规范化

This commit is contained in:
atsunatsu committed 2026-09-28 23:46:47 +08:00
1 parent 1871c05002
commit 5acf71b2a6
26 files changed
+2145 -98

No files matched your search

@@ -26,7 +26,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -38,7 +41,12 @@ import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.logbook.QsoRecord
import com.rtbishop.look4sat.core.domain.logbook.displayMode
import com.rtbishop.look4sat.core.domain.logbook.frequencyBand
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
import com.rtbishop.look4sat.core.presentation.LocalSpacing
import com.rtbishop.look4sat.core.presentation.QsoEditDialog
import com.rtbishop.look4sat.core.presentation.SheetDialogTitle
import com.rtbishop.look4sat.core.presentation.sheetDialogContainerColor
import com.rtbishop.look4sat.core.presentation.sheetDialogShape
import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.SwipeController
@@ -70,17 +78,21 @@ fun LogbookCard(recordCount: Int, showLogbookDialog: () -> Unit) {
@Composable
fun LogbookDialog(
records: List<QsoRecord>,
satelliteCandidates: List<String>,
uploadBusy: Boolean,
uploadMessage: String,
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview?,
onDismiss: () -> Unit,
onDelete: (Long) -> Unit,
onEdit: (QsoRecord) -> Unit,
onUpload: () -> Unit,
onConfirmUpload: () -> Unit,
onDismissPreview: () -> Unit,
onDismissMessage: () -> Unit
) {
val swipeController = rememberSwipeController()
// Record currently open in the edit dialog (null when closed).
var editTarget by remember { mutableStateOf<QsoRecord?>(null) }
SharedDialog(
title = stringResource(R.string.prefs_logbook_title),
onDismissRequest = onDismiss,
@@ -101,7 +113,12 @@ fun LogbookDialog(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
items(records, key = { it.id }) { record ->
LogbookRow(record, swipeController, onDelete = { onDelete(record.id) })
LogbookRow(
record = record,
swipeController = swipeController,
onDelete = { onDelete(record.id) },
onClick = { editTarget = record }
)
}
}
}
@@ -119,13 +136,27 @@ fun LogbookDialog(
if (uploadMessage.isNotBlank()) {
AlertDialog(
onDismissRequest = onDismissMessage,
title = { Text("LoTW Upload") },
shape = sheetDialogShape(),
containerColor = sheetDialogContainerColor(),
title = { SheetDialogTitle("LoTW Upload") },
text = { Text(uploadMessage) },
confirmButton = {
TextButton(onClick = onDismissMessage) { Text("OK") }
}
)
}
editTarget?.let { target ->
QsoEditDialog(
record = target,
satelliteCandidates = satelliteCandidates,
onDismiss = { editTarget = null },
onSave = { updated ->
onEdit(updated)
editTarget = null
}
)
}
}
@Composable
@@ -137,7 +168,9 @@ private fun LogbookUploadPreviewDialog(
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.prefs_logbook_upload_title)) },
shape = sheetDialogShape(),
containerColor = sheetDialogContainerColor(),
title = { SheetDialogTitle(stringResource(R.string.prefs_logbook_upload_title)) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
@@ -147,9 +180,17 @@ private fun LogbookUploadPreviewDialog(
)
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
val parts = buildList {
if (preview.skipped > 0) add("${preview.skipped} already uploaded/duplicate")
val alreadyUploaded = preview.skipped - preview.duplicateSkipped - preview.unknownSkipped
if (alreadyUploaded > 0) add("$alreadyUploaded already uploaded")
if (preview.duplicateSkipped > 0) add("${preview.duplicateSkipped} duplicate")
if (preview.unknownSkipped > 0) add("${preview.unknownSkipped} unknown result")
if (preview.unavailableSkipped > 0) add("${preview.unavailableSkipped} un-uploadable")
if (preview.unavailableSkipped > 0) add(
unavailableUploadSummary(
preview.unavailableSkipped,
preview.unavailableReasons,
duplicates = preview.duplicateSkipped
)
)
}
Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error)
}
@@ -168,7 +209,12 @@ private fun LogbookUploadPreviewDialog(
}
@Composable
private fun LogbookRow(record: QsoRecord, swipeController: SwipeController, onDelete: () -> Unit) {
private fun LogbookRow(
record: QsoRecord,
swipeController: SwipeController,
onDelete: () -> Unit,
onClick: () -> Unit
) {
val time = remember(record.startUtcMillis) {
SimpleDateFormat("MM-dd HH:mm'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
@@ -188,6 +234,7 @@ private fun LogbookRow(record: QsoRecord, swipeController: SwipeController, onDe
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 4.dp, vertical = 8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
@@ -195,15 +242,15 @@ private fun LogbookRow(record: QsoRecord, swipeController: SwipeController, onDe
text = record.theirCallsign,
style = MaterialTheme.typography.titleMedium,
fontFamily = FontFamily.Monospace,
color = Color(0xFFFFE082),
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
if (record.lotwConfirmed) {
Text(text = "QSL", fontSize = 13.sp, color = Color(0xFFFFE082), fontFamily = FontFamily.Monospace)
Text(text = "QSL", fontSize = 13.sp, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace)
} else if (record.lotwUploaded) {
Text(text = "UP", fontSize = 13.sp, color = Color(0xFFFFE082), fontFamily = FontFamily.Monospace)
Text(text = "UP", fontSize = 13.sp, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace)
}
}
Text(
@@ -163,6 +163,11 @@ class SettingsViewModel(
_uiState.update { it.copy(logbookRecords = records) }
}
}
// ARRL satellite names for the logbook edit dialog's satellite picker.
viewModelScope.launch {
val catalog = runCatching { lotwUploadRepository.satelliteCatalog() }.getOrDefault(emptyList())
_uiState.update { it.copy(satelliteCatalog = catalog) }
}
// Load the LoTW upload certificate + station once at startup so the
// settings card reflects the real state on first frame (previously it
// stayed "not imported" until the config dialog was opened).
@@ -197,7 +202,15 @@ class SettingsViewModel(
is SettingsAction.SetCompassOffset -> settingsRepo.updateOtherSettings {
it.copy(compassOffsetDegrees = action.degrees.coerceIn(-180f, 180f))
}
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings {
// The red night filter is a dark-screen aid; turning the light theme on
// switches it off so the UI isn't red-on-white. Turning the light theme
// off leaves the filter as the user set it (off).
it.copy(
stateOfLightTheme = action.value,
stateOfNightMode = if (action.value) false else it.stateOfNightMode
)
}
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
is SettingsAction.UpdateMapSettings -> settingsRepo.updateOtherSettings {
it.copy(mapSource = action.mapSource, tiandituKey = action.tiandituKey)
@@ -216,6 +229,7 @@ class SettingsViewModel(
// Logbook
SettingsAction.RefreshLogbook -> refreshLogbook()
is SettingsAction.DeleteLogbookRecord -> viewModelScope.launch { qsoRepository.delete(action.id) }
is SettingsAction.UpdateLogbookRecord -> viewModelScope.launch { qsoRepository.save(action.record) }
SettingsAction.PrepareLogbookUpload -> prepareLogbookUpload()
SettingsAction.ConfirmLogbookUpload -> confirmLogbookUpload()
SettingsAction.DismissLogbookPreview -> dismissLogbookPreview()
@@ -364,11 +378,14 @@ class SettingsViewModel(
val all = qsoRepository.records.first()
// Only local (non-confirmed) records are candidates for upload;
// LoTW-imported confirmations are the feedback side.
val pending = all.filter { !it.lotwConfirmed && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE }
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE }
val audit = lotwUploadRepository.audit(pending)
if (audit.pending == 0) {
val msg = when {
audit.unavailable > 0 -> "${audit.unavailable} QSO(s) can't be uploaded (invalid call/date — check the logbook)"
audit.unavailable > 0 -> com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary(
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
)
audit.unknown > 0 -> "${audit.unknown} QSO(s) had an unknown upload result — not retried automatically"
else -> "No pending QSOs to upload"
}