mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 03:17:21 +00:00
feat(lotw): cancel in-flight sync, honest progress bar, drop stale hint
- Cancel: the dialog's cancel/back while syncing now aborts the download job (cooperative cancellation in the stream reader, CancellationException propagated, nothing written to storage). An accidental full sync can be stopped immediately. - Progress: the size estimate is conservative (900 B/record vs measured 720) so the bar never hits 100% before the download truly ends; storage writes moved to the IO dispatcher (multi-MB per-grid QSO JSON used to freeze the main thread after download); when the bar caps, the UI switches to 'Processing downloaded data…' instead of looking stuck. - Removed the outdated '3-10 seconds' hint (misleading for large accounts).
This commit is contained in:
1 parent
01ca06b43f
commit
5f1eb239ed
7 files changed
+55
-19
No files matched your search
+11
-8
@@ -233,6 +233,8 @@ fun LoTWDialog(
|
||||
progress: com.rtbishop.look4sat.core.domain.repository.LoTWProgress?,
|
||||
message: String?,
|
||||
dismiss: () -> Unit,
|
||||
/** Called on cancel/back while a sync is running: aborts the job. */
|
||||
onCancelSync: () -> Unit,
|
||||
onSave: (com.rtbishop.look4sat.core.domain.model.LoTWSettings) -> Unit,
|
||||
onSyncFull: (com.rtbishop.look4sat.core.domain.model.LoTWSettings) -> Unit,
|
||||
onSyncIncremental: (com.rtbishop.look4sat.core.domain.model.LoTWSettings) -> Unit
|
||||
@@ -241,8 +243,10 @@ fun LoTWDialog(
|
||||
val pass = rememberSaveable { mutableStateOf(initialSettings.password) }
|
||||
SharedDialog(
|
||||
title = stringResource(R.string.prefs_lotw_title),
|
||||
onDismissRequest = dismiss,
|
||||
onCancel = dismiss,
|
||||
// While syncing, cancel/back aborts the download instead of merely
|
||||
// hiding the dialog — a full sync started by mistake must be stoppable.
|
||||
onDismissRequest = { if (isSyncing) onCancelSync() else dismiss() },
|
||||
onCancel = { if (isSyncing) onCancelSync() else dismiss() },
|
||||
// The two sync buttons below are the primary actions: saving
|
||||
// credentials and fetching confirmed grids happen in one step.
|
||||
onAccept = null
|
||||
@@ -276,6 +280,11 @@ fun LoTWDialog(
|
||||
progress == null ||
|
||||
progress.phase == com.rtbishop.look4sat.core.domain.repository.LoTWPhase.Connecting ->
|
||||
stringResource(R.string.lotw_sync_progress_connecting)
|
||||
// Download finished (or the size estimate reached its cap):
|
||||
// parsing and saving may still run — say so instead of the bar
|
||||
// sitting at 100% with no feedback, which reads as "stuck".
|
||||
progress.fraction >= 1f ->
|
||||
stringResource(R.string.lotw_sync_progress_saving)
|
||||
progress.qsoCount > 0 && progress.remainingSeconds > 0 ->
|
||||
stringResource(R.string.lotw_sync_progress_qso, progress.qsoCount, progress.remainingSeconds)
|
||||
progress.qsoCount > 0 ->
|
||||
@@ -307,12 +316,6 @@ fun LoTWDialog(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = LocalSpacing.current.large)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_lotw_sync_duration_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = LocalSpacing.current.large)
|
||||
)
|
||||
}
|
||||
if (message != null) {
|
||||
Text(
|
||||
|
||||
+1
@@ -223,6 +223,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
progress = uiState.lotwProgress,
|
||||
message = lotwErrorMessage,
|
||||
dismiss = { dialogs.lotw = false },
|
||||
onCancelSync = { onAction(SettingsAction.CancelLoTWSync); dialogs.lotw = false },
|
||||
onSave = { onAction(SettingsAction.UpdateLoTW(it)) },
|
||||
onSyncFull = { onAction(SettingsAction.SyncLoTWGrids(it, LoTWSyncMode.Full)) },
|
||||
onSyncIncremental = { onAction(SettingsAction.SyncLoTWGrids(it, LoTWSyncMode.Incremental)) }
|
||||
|
||||
@@ -117,6 +117,8 @@ sealed interface SettingsAction {
|
||||
val settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings,
|
||||
val mode: LoTWSyncMode
|
||||
) : SettingsAction
|
||||
/** Abort an in-flight LoTW sync (wrong button / changed mind). */
|
||||
data object CancelLoTWSync : SettingsAction
|
||||
|
||||
// Update checker
|
||||
data object CheckForUpdate : SettingsAction
|
||||
|
||||
+22
-4
@@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class SettingsViewModel(
|
||||
@@ -49,6 +50,8 @@ class SettingsViewModel(
|
||||
|
||||
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
|
||||
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
|
||||
/** In-flight LoTW sync job, cancelled by CancelLoTWSync. */
|
||||
private var lotwSyncJob: kotlinx.coroutines.Job? = null
|
||||
private val _uiState = MutableStateFlow(
|
||||
SettingsState(
|
||||
appVersionName = settingsRepo.appVersionName,
|
||||
@@ -159,6 +162,7 @@ class SettingsViewModel(
|
||||
// LoTW confirmed grids
|
||||
is SettingsAction.UpdateLoTW -> settingsRepo.updateLoTWSettings(action.settings)
|
||||
is SettingsAction.SyncLoTWGrids -> syncLoTWGrids(action.settings, action.mode)
|
||||
SettingsAction.CancelLoTWSync -> cancelLoTWSync()
|
||||
// Update checker
|
||||
SettingsAction.CheckForUpdate -> checkForUpdate()
|
||||
SettingsAction.DownloadUpdate -> downloadUpdate()
|
||||
@@ -215,7 +219,7 @@ class SettingsViewModel(
|
||||
_uiState.update {
|
||||
it.copy(lotwSyncing = true, lotwSyncMode = effectiveMode, lotwProgress = null, lotwError = null)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
lotwSyncJob = viewModelScope.launch {
|
||||
val result = lotwRepo.fetchConfirmedGridQsos(callsign, settings.password, since) { progress ->
|
||||
_uiState.update { it.copy(lotwProgress = progress) }
|
||||
}
|
||||
@@ -235,9 +239,6 @@ class SettingsViewModel(
|
||||
val mergedRoamed = if (effectiveMode == LoTWSyncMode.Incremental) {
|
||||
existingRoamed + result.roamedGrids
|
||||
} else result.roamedGrids
|
||||
settingsRepo.setWorkedGrids(mergedGrids)
|
||||
settingsRepo.setWorkedGridQsos(mergedQsos)
|
||||
settingsRepo.setRoamedGrids(mergedRoamed)
|
||||
// Remember when/what we synced, so the next incremental pull
|
||||
// asks LoTW for only the confirmations since today.
|
||||
val today = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.US)
|
||||
@@ -245,6 +246,16 @@ class SettingsViewModel(
|
||||
.format(java.util.Date())
|
||||
settingsRepo.setLastLotwSyncDate(today)
|
||||
settingsRepo.setLastLotwSyncCallsign(callsign)
|
||||
// Persist on the IO dispatcher: building the per-grid QSO JSON
|
||||
// and writing SharedPreferences synchronously blocks the
|
||||
// calling thread, and for large accounts (tens of thousands
|
||||
// of QSOs → multi-MB JSON) that froze the main thread after
|
||||
// the download finished — 'bar done but app stuck'.
|
||||
withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
settingsRepo.setWorkedGrids(mergedGrids)
|
||||
settingsRepo.setWorkedGridQsos(mergedQsos)
|
||||
settingsRepo.setRoamedGrids(mergedRoamed)
|
||||
}
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
@@ -284,6 +295,13 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** Abort the in-flight LoTW sync job and reset its progress state. */
|
||||
private fun cancelLoTWSync() {
|
||||
lotwSyncJob?.cancel()
|
||||
lotwSyncJob = null
|
||||
_uiState.update { it.copy(lotwSyncing = false, lotwSyncMode = null, lotwProgress = null) }
|
||||
}
|
||||
|
||||
/** Merge fresh QSO detail into existing per-grid lists, dedup by call + QSO time. */
|
||||
private fun mergeGridQsos(
|
||||
existing: Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>>,
|
||||
|
||||
Reference in new issue
Block a user