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() ) } }