feat(wavelog): upload rides the LoTW flow — auto-sync + catch-up (option A)

Standalone Wavelog upload entries are gone: the batch is prepared alongside the LoTW preview (count shown in both preview dialogs), posted right after the LoTW confirm in the radar, settings-logbook and resubmit flows, and the backlog flushes when LoTW has nothing pending. Failures stay unmarked and retry on the next upload.
This commit is contained in:
atsunatsu committed 2026-10-05 02:19:11 +08:00
1 parent a459cfb046
commit 5a8c4281a3
11 files changed
+243 -268

No files matched your search

@@ -68,7 +68,6 @@ import com.rtbishop.look4sat.core.presentation.sheetDialogContainerColor
import com.rtbishop.look4sat.core.presentation.sheetDialogShape
import com.rtbishop.look4sat.core.presentation.gridsLabel
import com.rtbishop.look4sat.core.presentation.LoTWPositionWarningDialog
import com.rtbishop.look4sat.core.presentation.WavelogUploadPreviewDialog
@Composable
fun LogPage(
@@ -215,14 +214,7 @@ fun LogPage(
modifier = Modifier.weight(1f)
) { Text("上传 LoTW", fontSize = 13.sp) }
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = logViewModel::prepareWavelogUpload,
enabled = !logUiState.busy && !logUiState.wavelogBusy,
modifier = Modifier.weight(1f)
) { Text("上传 Wavelog", fontSize = 13.sp) }
}
if (logUiState.busy || logUiState.wavelogBusy) {
if (logUiState.busy) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
CircularProgressIndicator(modifier = Modifier.padding(4.dp), strokeWidth = 2.dp)
Text("Working…", fontSize = 13.sp)
@@ -268,21 +260,13 @@ fun LogPage(
UploadPreviewDialog(
preview = preview,
busy = logUiState.busy,
wavelogCount = logUiState.wavelogPending?.count ?: 0,
onConfirm = logViewModel::confirmUpload,
onDismiss = logViewModel::dismissPreview
)
}
}
logUiState.wavelogPreview?.let { preview ->
WavelogUploadPreviewDialog(
preview = preview,
busy = logUiState.wavelogBusy,
onConfirm = logViewModel::confirmWavelogUpload,
onDismiss = logViewModel::dismissWavelogPreview
)
}
if (logUiState.message.isNotBlank()) {
AlertDialog(
onDismissRequest = logViewModel::clearMessage,
@@ -296,19 +280,6 @@ fun LogPage(
)
}
if (logUiState.wavelogMessage.isNotBlank()) {
AlertDialog(
onDismissRequest = logViewModel::clearWavelogMessage,
shape = sheetDialogShape(),
containerColor = sheetDialogContainerColor(),
title = { SheetDialogTitle("Wavelog Upload") },
text = { Text(logUiState.wavelogMessage) },
confirmButton = {
TextButton(onClick = logViewModel::clearWavelogMessage) { Text("OK") }
}
)
}
// Out-of-window notice: the clock is outside the current pass [aos, los], so the record
// would never show in the window-filtered list. Confirm first, then store it at the pass
// midpoint (there is always an edit dialog afterwards for the exact time).
@@ -436,6 +407,7 @@ private fun PostDialog(
private fun UploadPreviewDialog(
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview,
busy: Boolean,
wavelogCount: Int = 0,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
@@ -484,6 +456,13 @@ private fun UploadPreviewDialog(
}
Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error)
}
if (wavelogCount > 0) {
Text(
"同时同步 Wavelog:$wavelogCount 条",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.primary
)
}
Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8)
}
},
@@ -18,13 +18,14 @@ import com.rtbishop.look4sat.core.domain.logbook.QsoRecord
import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
import com.rtbishop.look4sat.core.domain.logbook.displayMode
import com.rtbishop.look4sat.core.domain.logbook.frequencyBand
import com.rtbishop.look4sat.core.domain.logbook.label
import com.rtbishop.look4sat.core.domain.logbook.officialSatelliteName
import com.rtbishop.look4sat.core.domain.logbook.satelliteIdentity
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
import com.rtbishop.look4sat.core.domain.logbook.wavelogResultMessage
import com.rtbishop.look4sat.core.domain.logbook.wavelogSkipSummary
import com.rtbishop.look4sat.core.domain.logbook.wavelogConfirmedSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogIdleSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogTransportFailureSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogUploadCandidates
import com.rtbishop.look4sat.core.domain.model.WavelogUploadSettings
import com.rtbishop.look4sat.core.domain.repository.ILoTWUploadRepository
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
@@ -85,10 +86,9 @@ data class LogUiState(
val message: String = "",
val postText: String? = null,
val busy: Boolean = false,
/** Wavelog upload: prepared preview awaiting confirmation / user-facing message / busy flag. */
val wavelogPreview: WavelogUploadPreview? = null,
val wavelogMessage: String = "",
val wavelogBusy: Boolean = false,
/** Wavelog piggyback: batch prepared alongside the LoTW preview (see prepareUpload);
* uploaded right after a confirmed LoTW upload. Null when Wavelog is not ready. */
val wavelogPending: WavelogUploadPreview? = null,
/** Out-of-window contact waiting for the operator's confirmation (null when closed). */
val outOfWindowLog: OutOfWindowLog? = null
)
@@ -272,6 +272,16 @@ class LogViewModel(
// LoTW-imported confirmations are the feedback side.
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == QsoStatus.COMPLETE }
val audit = lotwUploadRepository.audit(pending)
// Wavelog piggyback (option A): prepare its batch while we are here, so
// the LoTW preview can show the count and confirm can post it directly.
val wavelogSettings = settingsRepo.wavelogUploadSettings.value
val wavelogPreview = if (wavelogSettings.isReady) {
runCatching {
wavelogUploadRepository.prepare(wavelogUploadCandidates(all), wavelogSettings)
}.getOrNull()
} else {
null
}
if (audit.pending == 0) {
val msg = when {
audit.unavailable > 0 -> unavailableUploadSummary(
@@ -281,7 +291,14 @@ class LogViewModel(
audit.unknown > 0 -> "${audit.unknown} QSO(s) had an unknown upload result — not retried automatically"
else -> "No pending QSOs to upload"
}
_uiState.update { it.copy(busy = false, message = msg) }
// No LoTW work to do — still flush the Wavelog backlog (catch-up): a
// freshly configured Wavelog syncs without waiting for new QSOs.
val wavelogMsg = wavelogPreview?.let {
if (it.count > 0) uploadWavelogNow(it, wavelogSettings) else wavelogIdleSegment(it)
}
_uiState.update {
it.copy(busy = false, message = listOfNotNull(msg, wavelogMsg).joinToString("\n"))
}
return@launch
}
val preview = lotwUploadRepository.prepare(pending, false)
@@ -292,6 +309,7 @@ class LogViewModel(
it.copy(
busy = false,
preview = preview,
wavelogPending = wavelogPreview,
uploadPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
)
}
@@ -320,58 +338,41 @@ class LogViewModel(
LoTWUploadResult.Unknown -> "Unknown result — will not auto-retry"
LoTWUploadResult.ExpiredPreview -> "Preview expired — tap upload again"
}
_uiState.update { it.copy(busy = false, preview = null, message = msg) }
}
}
fun dismissPreview() = _uiState.update { it.copy(preview = null) }
fun prepareWavelogUpload() {
if (_uiState.value.wavelogBusy) return
viewModelScope.launch {
_uiState.update { it.copy(wavelogBusy = true, wavelogMessage = "") }
try {
val pending = wavelogUploadCandidates(qsoRepository.records.first())
val preview = wavelogUploadRepository.prepare(pending, settingsRepo.wavelogUploadSettings.value)
if (preview.count == 0) {
val blocked = preview.blockedBy
val message = when {
blocked != null -> blocked.label()
preview.skipped > 0 -> wavelogSkipSummary(preview)
else -> "No pending QSOs to upload"
}
_uiState.update { it.copy(wavelogBusy = false, wavelogMessage = message) }
return@launch
}
_uiState.update { it.copy(wavelogBusy = false, wavelogPreview = preview) }
} catch (_: Exception) {
_uiState.update { it.copy(wavelogBusy = false, wavelogMessage = "Upload failed") }
// Wavelog piggyback: rides the same confirmation, independent of the LoTW
// outcome — a Wavelog failure leaves records unmarked for the next upload.
val wavelogPending = _uiState.value.wavelogPending
val wavelogMsg = wavelogPending?.let { wl ->
if (wl.count > 0) uploadWavelogNow(wl, settingsRepo.wavelogUploadSettings.value)
else wavelogIdleSegment(wl)
}
_uiState.update {
it.copy(
busy = false, preview = null, wavelogPending = null,
message = listOfNotNull(msg, wavelogMsg).joinToString("\n")
)
}
}
}
fun confirmWavelogUpload() {
val preview = _uiState.value.wavelogPreview ?: return
viewModelScope.launch {
_uiState.update { it.copy(wavelogBusy = true) }
try {
val result = wavelogUploadRepository.upload(preview, settingsRepo.wavelogUploadSettings.value)
if (result is WavelogUploadOutcome.Imported && result.markIds.isNotEmpty()) {
qsoRepository.markWavelogUploaded(result.markIds)
}
_uiState.update {
it.copy(wavelogBusy = false, wavelogPreview = null, wavelogMessage = wavelogResultMessage(result))
}
} catch (_: Exception) {
_uiState.update { it.copy(wavelogBusy = false, wavelogPreview = null, wavelogMessage = "Upload failed") }
}
fun dismissPreview() = _uiState.update { it.copy(preview = null, wavelogPending = null) }
/**
* Uploads one prepared Wavelog batch (piggyback) and marks what the server
* accepted. Returns the operator-facing message segment for this upload.
*/
private suspend fun uploadWavelogNow(
preview: WavelogUploadPreview,
settings: WavelogUploadSettings
): String = try {
val outcome = wavelogUploadRepository.upload(preview, settings)
if (outcome is WavelogUploadOutcome.Imported && outcome.markIds.isNotEmpty()) {
qsoRepository.markWavelogUploaded(outcome.markIds)
}
wavelogConfirmedSegment(outcome, preview)
} catch (_: Exception) {
wavelogTransportFailureSegment()
}
fun dismissWavelogPreview() = _uiState.update { it.copy(wavelogPreview = null) }
fun clearWavelogMessage() = _uiState.update { it.copy(wavelogMessage = "") }
/** Operator chose "ignore" on the position check: keep the prepared preview. */
fun ignorePositionWarning() = _uiState.update { it.copy(uploadPositionWarning = null) }
@@ -379,7 +380,7 @@ class LogViewModel(
fun abandonForGridFix() {
lotwUploadRepository.discardPreview()
lastUploadedIds = emptyList()
_uiState.update { it.copy(uploadPositionWarning = null, preview = null) }
_uiState.update { it.copy(uploadPositionWarning = null, preview = null, wavelogPending = null) }
}
/** Recompute the log-page roaming hint from the freshest station/position data. */
@@ -105,13 +105,8 @@ fun LogbookDialog(
onToggleSelection: (Long) -> Unit = {},
onExitSelection: () -> Unit = {},
onResubmitSelected: () -> Unit = {},
wavelogBusy: Boolean = false,
wavelogPreview: com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview? = null,
wavelogMessage: String = "",
onWavelogUpload: () -> Unit = {},
onConfirmWavelogUpload: () -> Unit = {},
onDismissWavelogPreview: () -> Unit = {},
onDismissWavelogMessage: () -> Unit = {}
/** Submit count of the Wavelog batch riding this upload (0 hides the note). */
wavelogCount: Int = 0
) {
val swipeController = rememberSwipeController()
// Entering selection mode closes any row left swiped open — reveals are off there.
@@ -173,14 +168,6 @@ fun LogbookDialog(
}
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = onWavelogUpload, enabled = !wavelogBusy) {
Text(stringResource(R.string.prefs_logbook_wavelog_upload))
}
}
}
}
@@ -196,6 +183,7 @@ fun LogbookDialog(
LogbookUploadPreviewDialog(
preview = preview,
busy = uploadBusy,
wavelogCount = wavelogCount,
onConfirm = onConfirmUpload,
onDismiss = onDismissPreview
)
@@ -213,27 +201,6 @@ fun LogbookDialog(
)
}
wavelogPreview?.let { preview ->
com.rtbishop.look4sat.core.presentation.WavelogUploadPreviewDialog(
preview = preview,
busy = wavelogBusy,
onConfirm = onConfirmWavelogUpload,
onDismiss = onDismissWavelogPreview
)
}
if (wavelogMessage.isNotBlank()) {
AlertDialog(
onDismissRequest = onDismissWavelogMessage,
shape = sheetDialogShape(),
containerColor = sheetDialogContainerColor(),
title = { SheetDialogTitle("Wavelog Upload") },
text = { Text(wavelogMessage) },
confirmButton = {
TextButton(onClick = onDismissWavelogMessage) { Text("OK") }
}
)
}
editTarget?.let { target ->
QsoEditDialog(
record = target,
@@ -251,6 +218,7 @@ fun LogbookDialog(
private fun LogbookUploadPreviewDialog(
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview,
busy: Boolean,
wavelogCount: Int,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
@@ -291,6 +259,13 @@ private fun LogbookUploadPreviewDialog(
color = MaterialTheme.colorScheme.primary
)
}
if (wavelogCount > 0) {
Text(
stringResource(R.string.prefs_logbook_wavelog_sync, wavelogCount),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.primary
)
}
Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8)
}
},
@@ -319,13 +319,7 @@ private fun SettingsScreen(
onToggleSelection = { onAction(SettingsAction.ToggleLogbookSelection(it)) },
onExitSelection = { onAction(SettingsAction.ExitLogbookSelection) },
onResubmitSelected = { onAction(SettingsAction.ResubmitSelectedLogbook) },
wavelogBusy = uiState.wavelogUploadBusy,
wavelogPreview = uiState.wavelogPreview,
wavelogMessage = uiState.wavelogUploadMessage.orEmpty(),
onWavelogUpload = { onAction(SettingsAction.PrepareWavelogUpload) },
onConfirmWavelogUpload = { onAction(SettingsAction.ConfirmWavelogUpload) },
onDismissWavelogPreview = { onAction(SettingsAction.DismissWavelogPreview) },
onDismissWavelogMessage = { onAction(SettingsAction.ClearWavelogUploadMessage) }
wavelogCount = uiState.wavelogPending?.count ?: 0
)
}
if (dialogs.lotwUpload) {
@@ -67,8 +67,10 @@ data class SettingsState(
val wavelogUploadStations: List<com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo> = emptyList(),
val wavelogUploadRights: String = "",
val wavelogUploadProbeBusy: Boolean = false,
val wavelogPreview: com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview? = null,
val wavelogUploadBusy: Boolean = false,
/** Wavelog batch prepared alongside the logbook LoTW preview; uploaded when the
* LoTW upload is confirmed (null when Wavelog is not ready). */
val wavelogPending: com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview? = null,
/** Config-block feedback (probe failures etc.); also shown inside the config dialog. */
val wavelogUploadMessage: String? = null,
val lotwSettings: com.rtbishop.look4sat.core.domain.model.LoTWSettings = com.rtbishop.look4sat.core.domain.model.LoTWSettings(),
val lotwSyncing: Boolean = false,
@@ -163,10 +165,6 @@ sealed interface SettingsAction {
data class UpdateWavelogUpload(val settings: WavelogUploadSettings) : SettingsAction
data class FetchWavelogUploadStations(val url: String, val apiKey: String) : SettingsAction
data class SelectWavelogUploadStation(val station: com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo) : SettingsAction
data object PrepareWavelogUpload : SettingsAction
data object ConfirmWavelogUpload : SettingsAction
data object DismissWavelogPreview : SettingsAction
data object ClearWavelogUploadMessage : SettingsAction
// LoTW confirmed grids
data class UpdateLoTW(val settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings) : SettingsAction
@@ -40,14 +40,15 @@ import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
import com.rtbishop.look4sat.core.domain.repository.IWavelogUploadRepository
import com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadOutcome
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
import com.rtbishop.look4sat.core.domain.logbook.label
import com.rtbishop.look4sat.core.domain.logbook.resubmitCandidates
import com.rtbishop.look4sat.core.domain.logbook.toConfirmedRecord
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
import com.rtbishop.look4sat.core.domain.logbook.wavelogResultMessage
import com.rtbishop.look4sat.core.domain.logbook.wavelogSkipSummary
import com.rtbishop.look4sat.core.domain.logbook.wavelogConfirmedSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogIdleSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogTransportFailureSegment
import com.rtbishop.look4sat.core.domain.logbook.wavelogUploadCandidates
import com.rtbishop.look4sat.core.presentation.R
import kotlinx.coroutines.flow.MutableStateFlow
@@ -240,14 +241,10 @@ class SettingsViewModel(
// Wavelog worked grids
is SettingsAction.UpdateWavelog -> settingsRepo.updateWavelogSettings(action.settings)
is SettingsAction.SyncWorkedGrids -> syncWorkedGrids(action.settings)
// Wavelog upload (separate config block + upload flow)
// Wavelog upload config (the upload itself rides the LoTW logbook flow)
is SettingsAction.UpdateWavelogUpload -> settingsRepo.updateWavelogUploadSettings(action.settings)
is SettingsAction.FetchWavelogUploadStations -> fetchWavelogUploadStations(action.url, action.apiKey)
is SettingsAction.SelectWavelogUploadStation -> selectWavelogUploadStation(action.station)
SettingsAction.PrepareWavelogUpload -> prepareWavelogUpload()
SettingsAction.ConfirmWavelogUpload -> confirmWavelogUpload()
SettingsAction.DismissWavelogPreview -> dismissWavelogPreview()
SettingsAction.ClearWavelogUploadMessage -> clearWavelogUploadMessage()
// LoTW confirmed grids
is SettingsAction.UpdateLoTW -> settingsRepo.updateLoTWSettings(action.settings)
is SettingsAction.SyncLoTWGrids -> syncLoTWGrids(action.settings, action.mode)
@@ -347,58 +344,34 @@ class SettingsViewModel(
)
}
private fun prepareWavelogUpload() {
if (_uiState.value.wavelogUploadBusy) return
viewModelScope.launch {
_uiState.update { it.copy(wavelogUploadBusy = true, wavelogUploadMessage = null) }
try {
val pending = wavelogUploadCandidates(qsoRepository.records.first())
val preview = wavelogUploadRepository.prepare(pending, settingsRepo.wavelogUploadSettings.value)
if (preview.count == 0) {
val blocked = preview.blockedBy
val message = when {
blocked != null -> blocked.label()
preview.skipped > 0 -> wavelogSkipSummary(preview)
else -> "No pending QSOs to upload"
}
_uiState.update { it.copy(wavelogUploadBusy = false, wavelogUploadMessage = message) }
return@launch
}
_uiState.update { it.copy(wavelogUploadBusy = false, wavelogPreview = preview) }
} catch (_: Exception) {
_uiState.update { it.copy(wavelogUploadBusy = false, wavelogUploadMessage = "Upload failed") }
}
}
/** Prepares the Wavelog batch riding the LoTW upload (null when Wavelog is not ready). */
private suspend fun prepareWavelogPiggyback(
records: List<com.rtbishop.look4sat.core.domain.logbook.QsoRecord>
): WavelogUploadPreview? {
val settings = settingsRepo.wavelogUploadSettings.value
if (!settings.isReady) return null
return runCatching {
wavelogUploadRepository.prepare(wavelogUploadCandidates(records), settings)
}.getOrNull()
}
private fun confirmWavelogUpload() {
val preview = _uiState.value.wavelogPreview ?: return
viewModelScope.launch {
_uiState.update { it.copy(wavelogUploadBusy = true) }
try {
val result = wavelogUploadRepository.upload(preview, settingsRepo.wavelogUploadSettings.value)
if (result is WavelogUploadOutcome.Imported && result.markIds.isNotEmpty()) {
qsoRepository.markWavelogUploaded(result.markIds)
}
_uiState.update {
it.copy(
wavelogUploadBusy = false,
wavelogPreview = null,
wavelogUploadMessage = wavelogResultMessage(result)
)
}
} catch (_: Exception) {
_uiState.update {
it.copy(wavelogUploadBusy = false, wavelogPreview = null, wavelogUploadMessage = "Upload failed")
}
}
/**
* Uploads one prepared Wavelog batch (piggyback) and marks what the server
* accepted. Returns the operator-facing message segment for this upload.
*/
private suspend fun uploadWavelogNow(
preview: WavelogUploadPreview,
settings: WavelogUploadSettings
): String = try {
val outcome = wavelogUploadRepository.upload(preview, settings)
if (outcome is WavelogUploadOutcome.Imported && outcome.markIds.isNotEmpty()) {
qsoRepository.markWavelogUploaded(outcome.markIds)
}
wavelogConfirmedSegment(outcome, preview)
} catch (_: Exception) {
wavelogTransportFailureSegment()
}
private fun dismissWavelogPreview() = _uiState.update { it.copy(wavelogPreview = null) }
private fun clearWavelogUploadMessage() = _uiState.update { it.copy(wavelogUploadMessage = null) }
private fun syncLoTWGrids(
settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings,
mode: LoTWSyncMode
@@ -508,6 +481,9 @@ class SettingsViewModel(
// LoTW-imported confirmations are the feedback side.
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE }
val audit = lotwUploadRepository.audit(pending)
// Wavelog piggyback (option A): prepare its batch while we are here, so
// the logbook preview can show the count and confirm can post it directly.
val wavelogPreview = prepareWavelogPiggyback(all)
if (audit.pending == 0) {
val msg = when {
audit.unavailable > 0 -> com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary(
@@ -517,7 +493,17 @@ class SettingsViewModel(
audit.unknown > 0 -> "${audit.unknown} QSO(s) had an unknown upload result — not retried automatically"
else -> "No pending QSOs to upload"
}
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = msg) }
// No LoTW work to do — still flush the Wavelog backlog (catch-up): a
// freshly configured Wavelog syncs without waiting for new QSOs.
val wavelogMsg = wavelogPreview?.let {
if (it.count > 0) uploadWavelogNow(it, settingsRepo.wavelogUploadSettings.value) else wavelogIdleSegment(it)
}
_uiState.update {
it.copy(
logbookUploadBusy = false,
logbookUploadMessage = listOfNotNull(msg, wavelogMsg).joinToString("\n")
)
}
return@launch
}
val preview = lotwUploadRepository.prepare(pending, false)
@@ -528,6 +514,7 @@ class SettingsViewModel(
it.copy(
logbookUploadBusy = false,
logbookPreview = preview,
wavelogPending = wavelogPreview,
logbookPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
)
}
@@ -555,11 +542,19 @@ class SettingsViewModel(
com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult.Unknown -> "Unknown result — will not auto-retry"
com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult.ExpiredPreview -> "Preview expired — tap upload again"
}
// Wavelog piggyback: rides the same confirmation, independent of the LoTW
// outcome — a Wavelog failure leaves records unmarked for the next upload.
val wavelogPending = _uiState.value.wavelogPending
val wavelogMsg = wavelogPending?.let { wl ->
if (wl.count > 0) uploadWavelogNow(wl, settingsRepo.wavelogUploadSettings.value)
else wavelogIdleSegment(wl)
}
_uiState.update {
it.copy(
logbookUploadBusy = false,
logbookPreview = null,
logbookUploadMessage = msg,
wavelogPending = null,
logbookUploadMessage = listOfNotNull(msg, wavelogMsg).joinToString("\n"),
// An accepted resubmit is done — drop the checks; a failed one keeps
// them so retrying stays one tap away.
logbookSelectionMode = if (accepted) false else it.logbookSelectionMode,
@@ -569,7 +564,7 @@ class SettingsViewModel(
}
}
private fun dismissLogbookPreview() = _uiState.update { it.copy(logbookPreview = null) }
private fun dismissLogbookPreview() = _uiState.update { it.copy(logbookPreview = null, wavelogPending = null) }
/** Operator chose "ignore" on the position check: keep the prepared preview. */
private fun ignoreLogbookPositionWarning() = _uiState.update { it.copy(logbookPositionWarning = null) }
@@ -578,7 +573,7 @@ class SettingsViewModel(
private fun abandonLogbookForGridFix() {
lotwUploadRepository.discardPreview()
lastLogbookUploadIds = emptyList()
_uiState.update { it.copy(logbookPositionWarning = null, logbookPreview = null) }
_uiState.update { it.copy(logbookPositionWarning = null, logbookPreview = null, wavelogPending = null) }
}
/** Long-press entry: selection mode with the pressed record checked. */
@@ -605,7 +600,8 @@ class SettingsViewModel(
viewModelScope.launch {
_uiState.update { it.copy(logbookUploadBusy = true, logbookUploadMessage = "") }
try {
val selected = resubmitCandidates(qsoRepository.records.first(), _uiState.value.logbookSelectedIds)
val all = qsoRepository.records.first()
val selected = resubmitCandidates(all, _uiState.value.logbookSelectedIds)
if (selected.isEmpty()) {
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "No complete QSOs in the selection") }
return@launch
@@ -621,10 +617,13 @@ class SettingsViewModel(
return@launch
}
lastLogbookUploadIds = preview.submittedIds
// Wavelog piggyback: the resubmit confirmation syncs the backlog too.
val wavelogPending = prepareWavelogPiggyback(all)
_uiState.update {
it.copy(
logbookUploadBusy = false,
logbookPreview = preview,
wavelogPending = wavelogPending,
logbookPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
)
}