From 758dc6d56725ca6f11df345ee50bd1a0192c7471 Mon Sep 17 00:00:00 2001 From: QIU Date: Wed, 26 Aug 2026 01:30:14 +0000 Subject: [PATCH] fix(wavelog): the auto-upload switch had nothing behind it Deleting the ten-minute polling loop left the "auto upload" switch in settings with no consumer - the operator could turn it on and nothing would ever act on it, which is worse than the loop it replaced. Uploading now happens when a contact is saved. That is what the switch always meant, and doing it at that moment means somebody is present to see the outcome: a partial failure says so, and the QSOs that did not go stay in the queue for a manual upload from settings. The loop reported nothing at all - a grid mismatch hit an empty if block and every other failure retried forever in the background. The upload goes through the view model rather than the composable, so the log screen still touches no repository. --- .../src/main/res/values-zh/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + .../rtbishop/look4sat/feature/radar/LogTab.kt | 6 ++++ .../look4sat/feature/radar/RadarScreen.kt | 25 +++++++++++++---- .../look4sat/feature/radar/RadarViewModel.kt | 28 +++++++++++++++++-- 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index c627b671..5e83387e 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -266,6 +266,7 @@ 未配置 WaveLog 服务器 读不到站点信息 - 请检查站点 ID 与密钥权限 上传失败 + 部分联络未能上传, 仍在队列中 LoTW 卫星列表已更新: %1$d 个 LoTW 列表更新失败: %1$s 呼号太短 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 8a28ca34..50d016de 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -297,6 +297,7 @@ WaveLog server not configured Cannot read the station profile - check the station ID and key permissions Upload failed + Some contacts could not be uploaded and stay in the queue LoTW satellite list updated: %1$d entries LoTW list update failed: %1$s Callsign too short diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt index af61d353..ade5ff41 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/LogTab.kt @@ -101,6 +101,7 @@ fun LogTab( txBaseFrequencyHz: Long? = null, aosTimeMs: Long = 0L, onLookupGrid: (String, (QrzGrid) -> Unit) -> Unit, + onAutoUpload: () -> Unit, modifier: Modifier = Modifier ) { val context = androidx.compose.ui.platform.LocalContext.current @@ -177,6 +178,7 @@ fun LogTab( txBaseFrequencyHz = txBaseFrequencyHz, aosTimeMs = aosTimeMs, onLookupGrid = onLookupGrid, + onAutoUpload = onAutoUpload, onSaved = { refreshTick++ } ) } @@ -265,6 +267,7 @@ private fun ExpandedLogInput( txBaseFrequencyHz: Long? = null, aosTimeMs: Long = 0L, onLookupGrid: (String, (QrzGrid) -> Unit) -> Unit, + onAutoUpload: () -> Unit, onSaved: () -> Unit ) { val context = androidx.compose.ui.platform.LocalContext.current @@ -332,6 +335,9 @@ private fun ExpandedLogInput( null -> savedMsg } ) + // Upload now if the operator asked for it. Doing this as the contact is saved, rather than + // on a timer, means the result can actually be shown to somebody. + onAutoUpload() // Grid backfill. The view model owns the cookie and the request; this used to read // SharedPreferences through LocalContext right here, inside composition. Failures now say // something: an expired cookie was indistinguishable from a station with no grid filed. diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index 963b22cb..6cdb87fa 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -59,6 +59,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.rtbishop.look4sat.core.domain.qrz.QrzGrid +import com.rtbishop.look4sat.core.domain.wavelog.UploadOutcome import com.rtbishop.look4sat.core.domain.predict.OrbitalPos import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.MutualPassData @@ -94,7 +95,17 @@ fun RadarDestination(navigateUp: () -> Unit) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() // Captured as a value because `viewModel` also names the composable factory function, so // referring to it inside a lambda resolves to that instead of this local. + val uploadQueuedMsg = stringResource(id = R.string.wavelog_upload_queued) val lookupGrid: (String, (QrzGrid) -> Unit) -> Unit = viewModel::lookupGrid + // Reports through the shared toast so an automatic upload is not silent the way the deleted + // ten-minute loop was. + val autoUpload: () -> Unit = { + viewModel.uploadIfAutomatic { outcome -> + if (outcome is UploadOutcome.Done && outcome.failedCount > 0) { + container.provideShowToast()(uploadQueuedMsg) + } + } + } val mutualData by container.mutualPassData.collectAsStateWithLifecycle() val navigateUpAndClearMutual = { if (container.mutualPassData.value.endTime > 0L) { @@ -143,7 +154,8 @@ fun RadarDestination(navigateUp: () -> Unit) { it.wavelogUrl.isNotBlank() && it.wavelogApiKey.isNotBlank() && it.wavelogStationId.isNotBlank() }, showToast = { msg -> container.provideShowToast()(msg) }, - onLookupGrid = lookupGrid + onLookupGrid = lookupGrid, + onAutoUpload = autoUpload ) } @@ -157,7 +169,8 @@ private fun RadarScreen( wavelogQueue: WavelogQueue, wavelogConfigured: Boolean, showToast: (String) -> Unit, - onLookupGrid: (String, (QrzGrid) -> Unit) -> Unit + onLookupGrid: (String, (QrzGrid) -> Unit) -> Unit, + onAutoUpload: () -> Unit ) { val upcomingPass = uiState.currentPass ?: getDefaultPass() val addToCalendar: () -> Unit = { @@ -211,11 +224,11 @@ private fun RadarScreen( } if (isVertical) { RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f)) - PagerCard(uiState, onAction, requestMicPermission, wavelogQueue, wavelogConfigured, showToast, onLookupGrid, Modifier.weight(1f)) + PagerCard(uiState, onAction, requestMicPermission, wavelogQueue, wavelogConfigured, showToast, onLookupGrid, onAutoUpload, Modifier.weight(1f)) } else { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f)) - PagerCard(uiState, onAction, requestMicPermission, wavelogQueue, wavelogConfigured, showToast, onLookupGrid, Modifier.weight(1f)) + PagerCard(uiState, onAction, requestMicPermission, wavelogQueue, wavelogConfigured, showToast, onLookupGrid, onAutoUpload, Modifier.weight(1f)) } } } @@ -230,6 +243,7 @@ private fun PagerCard( wavelogConfigured: Boolean, showToast: (String) -> Unit, onLookupGrid: (String, (QrzGrid) -> Unit) -> Unit, + onAutoUpload: () -> Unit, modifier: Modifier = Modifier ) { val hasCalculatorPage = remember(uiState.transceivers.transmitters) { @@ -299,7 +313,8 @@ private fun PagerCard( showToast = showToast, txBaseFrequencyHz = uiState.radioControl.txBaseFrequencyHz, aosTimeMs = uiState.currentPass?.aosTime ?: 0L, - onLookupGrid = onLookupGrid + onLookupGrid = onLookupGrid, + onAutoUpload = onAutoUpload ) } } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 96f70856..35c506ea 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -28,6 +28,8 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPass import com.rtbishop.look4sat.core.domain.predict.OrbitalPos import com.rtbishop.look4sat.core.domain.qrz.IQrzGridLookup import com.rtbishop.look4sat.core.domain.qrz.QrzGrid +import com.rtbishop.look4sat.core.domain.wavelog.UploadOutcome +import com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader import com.rtbishop.look4sat.core.domain.repository.IMainContainer import com.rtbishop.look4sat.core.domain.repository.IRadioTrackingService import com.rtbishop.look4sat.core.domain.repository.IReporter @@ -67,7 +69,8 @@ class RadarViewModel( private val cwDecoderFactory: () -> ICwDecoder, private val saveImage: ISaveImage, private val showToast: IShowToast, - private val qrzGridLookup: IQrzGridLookup + private val qrzGridLookup: IQrzGridLookup, + private val wavelogUploader: WavelogUploader ) : ViewModel() { private var stationPos = settingsRepo.stationPosition.value @@ -547,6 +550,26 @@ class RadarViewModel( _uiState.update { it.copy(cw = it.cw.copy(status = CwStatus.Idle)) } } + /** + * Upload the queue now, if the operator asked for automatic uploads. + * + * Called when a contact is saved rather than on a timer. The ten-minute polling loop this + * replaces ran in the background with no way to report anything, so a grid mismatch was + * silently skipped and every other failure retried forever. Uploading as the contact is saved + * means somebody is present to see the result, and a QSO that cannot go now waits in the queue + * for a manual upload from settings. + */ + fun uploadIfAutomatic(onResult: (UploadOutcome?) -> Unit) { + val settings = settingsRepo.otherSettings.value + if (!settings.wavelogAutoUpload || settings.wavelogUrl.isBlank()) { + onResult(null) + return + } + viewModelScope.launch { + onResult(runCatching { wavelogUploader.uploadQueue() }.getOrNull()) + } + } + /** * Look up a station's grid and hand the outcome back. * @@ -619,7 +642,8 @@ class RadarViewModel( cwDecoderFactory = { container.provideCwDecoder() }, saveImage = container.provideSaveImage(), showToast = container.provideShowToast(), - qrzGridLookup = container.provideQrzGridLookup() + qrzGridLookup = container.provideQrzGridLookup(), + wavelogUploader = container.provideWavelogUploader() ) } }