mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 03:57:24 +00:00
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:
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. */
|
||||
|
||||
+11
-36
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
+1
-7
@@ -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) {
|
||||
|
||||
+4
-6
@@ -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
|
||||
|
||||
+59
-60
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user