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