mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 12:07:25 +00:00
feat(logbook): 窗口外录入 + 网格显示链 + 双倍修复 + 上传前核对 + 重传选择 + 报错拆分
.2.5–.3 调试系列收口: - 雷达窗口外录入:不在过境窗口内按 Log 时按当前过境中点落点(弹窗确认,可事后编辑改时间) - 网格显示链:上传盖章(markUploaded 记录台址网格集)+ 日志本行尾 / 雷达页「自己的格子」+ QSL 槽对方网格(绿 #4CD964)+ 缩写规则统一(gridsLabel 上移 core:presentation); QsoDatabase v2→v3 增列 theirVuccGrids - 全量同步「双倍」三层修复:键无关身份±60s 镜像折叠、matchableMode 归一(MFSK→FT4)、 双确认对按 contentScore 折叠 - 上传前核对:批记录网格与台址完全不交集 → 弹窗(修改台址带预填跳转 / 忽略),预览被闸住 - 重传选中:日志本长按多选(含已上传/已确认)→ prepare(resubmit=true) → 成功后网格盖章更新; 退出日志本重置选择(需重新长按) - 报错拆分:QSO_FUTURE(记录时间在未来)与 QSO_DATE(超出证书范围)分开提示 - 全 App 弹窗样式统一(深色圆角 sheet) 测试:domain 172 / data 116 全绿。
This commit is contained in:
1 parent
d13af978af
commit
5b7c7d6df5
36 files changed
+1145
-99
No files matched your search
+118
-12
@@ -9,7 +9,9 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.settings
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -26,6 +28,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -33,6 +36,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -47,6 +52,8 @@ import com.rtbishop.look4sat.core.presentation.QsoEditDialog
|
||||
import com.rtbishop.look4sat.core.presentation.SheetDialogTitle
|
||||
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.LoTWGridWarningDialog
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.SharedDialog
|
||||
import com.rtbishop.look4sat.core.presentation.SwipeController
|
||||
@@ -82,24 +89,39 @@ fun LogbookDialog(
|
||||
uploadBusy: Boolean,
|
||||
uploadMessage: String,
|
||||
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview?,
|
||||
gridWarning: com.rtbishop.look4sat.core.domain.repository.LoTWGridWarning? = null,
|
||||
onDismiss: () -> Unit,
|
||||
onDelete: (Long) -> Unit,
|
||||
onEdit: (QsoRecord) -> Unit,
|
||||
onUpload: () -> Unit,
|
||||
onConfirmUpload: () -> Unit,
|
||||
onDismissPreview: () -> Unit,
|
||||
onDismissMessage: () -> Unit
|
||||
onDismissMessage: () -> Unit,
|
||||
onIgnoreGridWarning: () -> Unit = {},
|
||||
onFixGrid: (List<String>) -> Unit = {},
|
||||
selectionMode: Boolean = false,
|
||||
selectedIds: Set<Long> = emptySet(),
|
||||
onStartSelection: (Long) -> Unit = {},
|
||||
onToggleSelection: (Long) -> Unit = {},
|
||||
onExitSelection: () -> Unit = {},
|
||||
onResubmitSelected: () -> Unit = {}
|
||||
) {
|
||||
val swipeController = rememberSwipeController()
|
||||
// Entering selection mode closes any row left swiped open — reveals are off there.
|
||||
LaunchedEffect(selectionMode) { if (selectionMode) swipeController.close() }
|
||||
// Record currently open in the edit dialog (null when closed).
|
||||
var editTarget by remember { mutableStateOf<QsoRecord?>(null) }
|
||||
SharedDialog(
|
||||
title = stringResource(R.string.prefs_logbook_title),
|
||||
onDismissRequest = onDismiss,
|
||||
onCancel = onDismiss,
|
||||
onAccept = onUpload,
|
||||
acceptText = stringResource(R.string.prefs_logbook_upload),
|
||||
acceptEnabled = !uploadBusy
|
||||
onCancel = if (selectionMode) onExitSelection else onDismiss,
|
||||
onAccept = if (selectionMode) onResubmitSelected else onUpload,
|
||||
acceptText = if (selectionMode) {
|
||||
stringResource(R.string.prefs_logbook_resubmit, selectedIds.size)
|
||||
} else {
|
||||
stringResource(R.string.prefs_logbook_upload)
|
||||
},
|
||||
acceptEnabled = if (selectionMode) selectedIds.isNotEmpty() && !uploadBusy else !uploadBusy
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
|
||||
@@ -108,6 +130,21 @@ fun LogbookDialog(
|
||||
if (records.isEmpty()) {
|
||||
Text(stringResource(R.string.prefs_logbook_empty), fontSize = 14.sp)
|
||||
} else {
|
||||
// Selection mode shows the checked count; otherwise a hint that long-press
|
||||
// opens it (the path to resubmitting already-uploaded records).
|
||||
if (selectionMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_logbook_selected, selectedIds.size),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_logbook_select_hint),
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(max = 420.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
@@ -115,9 +152,16 @@ fun LogbookDialog(
|
||||
items(records, key = { it.id }) { record ->
|
||||
LogbookRow(
|
||||
record = record,
|
||||
selectionMode = selectionMode,
|
||||
selected = record.id in selectedIds,
|
||||
swipeController = swipeController,
|
||||
onDelete = { onDelete(record.id) },
|
||||
onClick = { editTarget = record }
|
||||
onClick = {
|
||||
if (selectionMode) onToggleSelection(record.id) else editTarget = record
|
||||
},
|
||||
onLongClick = {
|
||||
if (selectionMode) onToggleSelection(record.id) else onStartSelection(record.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,7 +169,15 @@ fun LogbookDialog(
|
||||
}
|
||||
}
|
||||
|
||||
if (preview != null) {
|
||||
if (gridWarning != null) {
|
||||
// Roaming guard: the batch would be signed under a station grid it was not operated
|
||||
// from — confirm the station location before the preview opens.
|
||||
LoTWGridWarningDialog(
|
||||
warning = gridWarning,
|
||||
onFixStation = { onFixGrid(gridWarning.recordGrids) },
|
||||
onIgnore = onIgnoreGridWarning
|
||||
)
|
||||
} else if (preview != null) {
|
||||
LogbookUploadPreviewDialog(
|
||||
preview = preview,
|
||||
busy = uploadBusy,
|
||||
@@ -194,6 +246,15 @@ private fun LogbookUploadPreviewDialog(
|
||||
}
|
||||
Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
if (preview.resubmit) {
|
||||
// LoTW treats an identical contact as an update, except grids already
|
||||
// locked in by award credits — say so before the operator commits.
|
||||
Text(
|
||||
stringResource(R.string.prefs_logbook_resubmit_note),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8)
|
||||
}
|
||||
},
|
||||
@@ -208,12 +269,19 @@ private fun LogbookUploadPreviewDialog(
|
||||
)
|
||||
}
|
||||
|
||||
/** Confirmed-grid colour: the map's worked-grid green, so "confirmed" reads the same everywhere. */
|
||||
private val ConfirmedGreen = Color(0xFF4CD964)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun LogbookRow(
|
||||
record: QsoRecord,
|
||||
selectionMode: Boolean,
|
||||
selected: Boolean,
|
||||
swipeController: SwipeController,
|
||||
onDelete: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
val time = remember(record.startUtcMillis) {
|
||||
SimpleDateFormat("MM-dd HH:mm'Z'", Locale.US).apply {
|
||||
@@ -222,11 +290,13 @@ private fun LogbookRow(
|
||||
}
|
||||
val satShort = record.satelliteName.substringBefore('(').trim()
|
||||
val band = record.band.ifBlank { frequencyBand(record.txFrequencyHz) }
|
||||
// 右划露出删除按钮(短信式);整行点击不再删除。
|
||||
val haptics = LocalHapticFeedback.current
|
||||
// 右划露出删除按钮(短信式);整行点击不再删除。选择模式下右划禁用、长按进入多选。
|
||||
SwipeRevealRow(
|
||||
key = record.id.toString(),
|
||||
controller = swipeController,
|
||||
revealAction = onDelete,
|
||||
gesturesEnabled = !selectionMode,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Row style mirrors the worked-grid QSO details on the map: callsign in
|
||||
@@ -234,10 +304,30 @@ private fun LogbookRow(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick()
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
// Checkbox slot: only while picking records for a resubmit.
|
||||
if (selectionMode) {
|
||||
Text(
|
||||
text = if (selected) "✓" else "○",
|
||||
fontSize = 15.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = record.theirCallsign,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -247,14 +337,30 @@ private fun LogbookRow(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
// Confirmed contacts show the opposite station's grid set from the LoTW
|
||||
// report (single or multi-grid, abbreviated) in the worked-grid green
|
||||
// instead of the word QSL; without any grid the QSL text stays.
|
||||
if (record.lotwConfirmed) {
|
||||
Text(text = "QSL", fontSize = 13.sp, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace)
|
||||
val oppositeGrids = gridsLabel(
|
||||
record.theirVuccGrids.ifEmpty { listOf(record.theirGrid) }
|
||||
)
|
||||
if (oppositeGrids.isNotEmpty()) {
|
||||
Text(text = oppositeGrids, fontSize = 13.sp, color = ConfirmedGreen, fontFamily = FontFamily.Monospace)
|
||||
} else {
|
||||
Text(text = "QSL", fontSize = 13.sp, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
} else if (record.lotwUploaded) {
|
||||
Text(text = "UP", fontSize = 13.sp, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = listOf(satShort, record.displayMode, band, time).filter { it.isNotBlank() }.joinToString(" · "),
|
||||
// Own station grid: the set the upload was stamped with, or — for rows from
|
||||
// before the stamp existed — the grid the QSO was logged under, so every
|
||||
// stored contact shows where the operator was.
|
||||
text = listOf(
|
||||
satShort, record.displayMode, band, time,
|
||||
gridsLabel(record.vuccGrids.ifEmpty { listOf(record.myGrid) })
|
||||
).filter { it.isNotBlank() }.joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
|
||||
+44
-5
@@ -94,14 +94,29 @@ fun SettingsDestination(onOpenGridFinder: () -> Unit = {}) {
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.factory(container, context))
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction, onOpenGridFinder)
|
||||
// Station-grid prefill for the "fix it" jump from the logbook grid check (single-use,
|
||||
// mirrors the Grid Finder's "Set as LoTW station" hand-off).
|
||||
val pendingStationGrid by container.pendingLoTWStationGrid.collectAsStateWithLifecycle()
|
||||
SettingsScreen(
|
||||
uiState = uiState,
|
||||
onAction = viewModel::onAction,
|
||||
onOpenGridFinder = onOpenGridFinder,
|
||||
pendingStationGrid = pendingStationGrid.orEmpty(),
|
||||
onFixStationGrid = { grids ->
|
||||
if (grids.isNotEmpty()) container.setPendingLoTWStationGrid(grids.joinToString(","))
|
||||
},
|
||||
onClearStationGridPrefill = { container.setPendingLoTWStationGrid(null) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsScreen(
|
||||
uiState: SettingsState,
|
||||
onAction: (SettingsAction) -> Unit,
|
||||
onOpenGridFinder: () -> Unit
|
||||
onOpenGridFinder: () -> Unit,
|
||||
pendingStationGrid: String = "",
|
||||
onFixStationGrid: (List<String>) -> Unit = {},
|
||||
onClearStationGridPrefill: () -> Unit = {}
|
||||
) {
|
||||
var showUpdateChecker by rememberSaveable { mutableStateOf(false) }
|
||||
var showMapSettings by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -277,13 +292,33 @@ private fun SettingsScreen(
|
||||
uploadBusy = uiState.logbookUploadBusy,
|
||||
uploadMessage = uiState.logbookUploadMessage,
|
||||
preview = uiState.logbookPreview,
|
||||
onDismiss = { dialogs.logbook = false },
|
||||
gridWarning = uiState.logbookGridWarning,
|
||||
onDismiss = {
|
||||
// Leaving the logbook resets the resubmit selection: re-entering starts
|
||||
// clean — the operator long-presses again to pick records.
|
||||
onAction(SettingsAction.ExitLogbookSelection)
|
||||
dialogs.logbook = false
|
||||
},
|
||||
onDelete = { onAction(SettingsAction.DeleteLogbookRecord(it)) },
|
||||
onEdit = { onAction(SettingsAction.UpdateLogbookRecord(it)) },
|
||||
onUpload = { onAction(SettingsAction.PrepareLogbookUpload) },
|
||||
onConfirmUpload = { onAction(SettingsAction.ConfirmLogbookUpload) },
|
||||
onDismissPreview = { onAction(SettingsAction.DismissLogbookPreview) },
|
||||
onDismissMessage = { onAction(SettingsAction.ClearLogbookMessage) }
|
||||
onDismissMessage = { onAction(SettingsAction.ClearLogbookMessage) },
|
||||
onIgnoreGridWarning = { onAction(SettingsAction.IgnoreLogbookGridWarning) },
|
||||
onFixGrid = { grids ->
|
||||
onAction(SettingsAction.AbandonLogbookForGridFix)
|
||||
onAction(SettingsAction.ExitLogbookSelection)
|
||||
dialogs.logbook = false
|
||||
onFixStationGrid(grids)
|
||||
dialogs.lotwUpload = true
|
||||
},
|
||||
selectionMode = uiState.logbookSelectionMode,
|
||||
selectedIds = uiState.logbookSelectedIds,
|
||||
onStartSelection = { onAction(SettingsAction.StartLogbookSelection(it)) },
|
||||
onToggleSelection = { onAction(SettingsAction.ToggleLogbookSelection(it)) },
|
||||
onExitSelection = { onAction(SettingsAction.ExitLogbookSelection) },
|
||||
onResubmitSelected = { onAction(SettingsAction.ResubmitSelectedLogbook) }
|
||||
)
|
||||
}
|
||||
if (dialogs.lotwUpload) {
|
||||
@@ -294,7 +329,11 @@ private fun SettingsScreen(
|
||||
busy = uiState.lotwUploadBusy,
|
||||
error = uiState.lotwUploadError,
|
||||
errorDetail = uiState.lotwUploadErrorDetail,
|
||||
onDismiss = { dialogs.lotwUpload = false },
|
||||
initialGrid = pendingStationGrid,
|
||||
onDismiss = {
|
||||
dialogs.lotwUpload = false
|
||||
onClearStationGridPrefill()
|
||||
},
|
||||
onImport = { bytes, password -> onAction(SettingsAction.ImportLoTWCertificate(bytes, password)) },
|
||||
onRemove = { onAction(SettingsAction.RemoveLoTWCertificate) },
|
||||
onSaveStation = { onAction(SettingsAction.SaveLoTWStation(it)) }
|
||||
|
||||
+17
@@ -85,9 +85,16 @@ data class SettingsState(
|
||||
val lotwUploadErrorDetail: String = "",
|
||||
/** One-click logbook upload: prepared preview awaiting confirmation. */
|
||||
val logbookPreview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview? = null,
|
||||
/** Roaming guard: the prepared batch holds records whose own grids fall outside the
|
||||
* station-location grids — shown as a dialog before the preview opens. */
|
||||
val logbookGridWarning: com.rtbishop.look4sat.core.domain.repository.LoTWGridWarning? = null,
|
||||
val logbookUploadBusy: Boolean = false,
|
||||
/** User-facing upload message shown inside the logbook dialog ("" = none). */
|
||||
val logbookUploadMessage: String = "",
|
||||
/** Logbook multi-select mode (entered by long-pressing a row); checked records can be
|
||||
* re-uploaded so a corrected station location updates them on LoTW (resubmit). */
|
||||
val logbookSelectionMode: Boolean = false,
|
||||
val logbookSelectedIds: Set<Long> = emptySet(),
|
||||
/** 指南针校准精度等级 (校准对话框进度条). */
|
||||
val compassAccuracy: CompassAccuracy = CompassAccuracy.UNRELIABLE,
|
||||
/** 校正后航向(度, 含磁偏角+手动偏置), 校准对话框实时显示. */
|
||||
@@ -160,6 +167,16 @@ sealed interface SettingsAction {
|
||||
data object PrepareLogbookUpload : SettingsAction
|
||||
data object ConfirmLogbookUpload : SettingsAction
|
||||
data object DismissLogbookPreview : SettingsAction
|
||||
/** The operator acknowledged the grid mismatch and wants to upload anyway. */
|
||||
data object IgnoreLogbookGridWarning : SettingsAction
|
||||
/** The operator chose to fix the station location first; the preview is discarded. */
|
||||
data object AbandonLogbookForGridFix : SettingsAction
|
||||
/** Long-press on a row: enter selection mode with that record checked. */
|
||||
data class StartLogbookSelection(val id: Long) : SettingsAction
|
||||
data class ToggleLogbookSelection(val id: Long) : SettingsAction
|
||||
data object ExitLogbookSelection : SettingsAction
|
||||
/** Re-upload the checked records — uploaded/confirmed rows included (resubmit). */
|
||||
data object ResubmitSelectedLogbook : SettingsAction
|
||||
data object ClearLogbookMessage : SettingsAction
|
||||
|
||||
// LoTW upload configuration (certificate + station)
|
||||
|
||||
+84
-4
@@ -37,7 +37,9 @@ import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
||||
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.presentation.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -233,6 +235,12 @@ class SettingsViewModel(
|
||||
SettingsAction.PrepareLogbookUpload -> prepareLogbookUpload()
|
||||
SettingsAction.ConfirmLogbookUpload -> confirmLogbookUpload()
|
||||
SettingsAction.DismissLogbookPreview -> dismissLogbookPreview()
|
||||
SettingsAction.IgnoreLogbookGridWarning -> ignoreLogbookGridWarning()
|
||||
SettingsAction.AbandonLogbookForGridFix -> abandonLogbookForGridFix()
|
||||
is SettingsAction.StartLogbookSelection -> startLogbookSelection(action.id)
|
||||
is SettingsAction.ToggleLogbookSelection -> toggleLogbookSelection(action.id)
|
||||
SettingsAction.ExitLogbookSelection -> exitLogbookSelection()
|
||||
SettingsAction.ResubmitSelectedLogbook -> resubmitSelectedLogbook()
|
||||
SettingsAction.ClearLogbookMessage -> clearLogbookMessage()
|
||||
// LoTW upload configuration
|
||||
SettingsAction.LoadLoTWUploadStatus -> loadLoTWUploadStatus()
|
||||
@@ -396,7 +404,7 @@ class SettingsViewModel(
|
||||
// Only the records that actually made it into the TQ8 may be
|
||||
// marked uploaded later — never the whole candidate list.
|
||||
lastLogbookUploadIds = preview.submittedIds
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookPreview = preview) }
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookPreview = preview, logbookGridWarning = preview.gridWarning) }
|
||||
} catch (e: com.rtbishop.look4sat.core.domain.repository.LoTWOperationException) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload unavailable: ${e.reason}") }
|
||||
} catch (_: Exception) {
|
||||
@@ -410,8 +418,9 @@ class SettingsViewModel(
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(logbookUploadBusy = true) }
|
||||
val result = lotwUploadRepository.upload(preview.id)
|
||||
if (result is com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult.Accepted && lastLogbookUploadIds.isNotEmpty()) {
|
||||
qsoRepository.markUploaded(lastLogbookUploadIds)
|
||||
val accepted = result is com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult.Accepted
|
||||
if (accepted && lastLogbookUploadIds.isNotEmpty()) {
|
||||
qsoRepository.markUploaded(lastLogbookUploadIds, preview.grids)
|
||||
lastLogbookUploadIds = emptyList()
|
||||
}
|
||||
val msg = when (result) {
|
||||
@@ -420,12 +429,83 @@ 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"
|
||||
}
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookPreview = null, logbookUploadMessage = msg) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
logbookUploadBusy = false,
|
||||
logbookPreview = null,
|
||||
logbookUploadMessage = msg,
|
||||
// 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,
|
||||
logbookSelectedIds = if (accepted) emptySet() else it.logbookSelectedIds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissLogbookPreview() = _uiState.update { it.copy(logbookPreview = null) }
|
||||
|
||||
/** Operator chose "ignore" on the grid check: keep the prepared preview. */
|
||||
private fun ignoreLogbookGridWarning() = _uiState.update { it.copy(logbookGridWarning = null) }
|
||||
|
||||
/** Operator chose to fix the station grid first: drop the prepared preview and leave. */
|
||||
private fun abandonLogbookForGridFix() {
|
||||
lotwUploadRepository.discardPreview()
|
||||
lastLogbookUploadIds = emptyList()
|
||||
_uiState.update { it.copy(logbookGridWarning = null, logbookPreview = null) }
|
||||
}
|
||||
|
||||
/** Long-press entry: selection mode with the pressed record checked. */
|
||||
private fun startLogbookSelection(id: Long) = _uiState.update {
|
||||
it.copy(logbookSelectionMode = true, logbookSelectedIds = setOf(id))
|
||||
}
|
||||
|
||||
private fun toggleLogbookSelection(id: Long) = _uiState.update {
|
||||
val selection = it.logbookSelectedIds.toMutableSet().apply { if (!add(id)) remove(id) }
|
||||
it.copy(logbookSelectedIds = selection)
|
||||
}
|
||||
|
||||
private fun exitLogbookSelection() = _uiState.update {
|
||||
it.copy(logbookSelectionMode = false, logbookSelectedIds = emptySet())
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-upload the checked records — already-uploaded and confirmed rows included — so a
|
||||
* corrected station location reaches LoTW and the server updates the existing contacts.
|
||||
* The rest is the normal prepare → preview → upload cycle, resubmit flag included.
|
||||
*/
|
||||
private fun resubmitSelectedLogbook() {
|
||||
if (_uiState.value.logbookUploadBusy) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(logbookUploadBusy = true, logbookUploadMessage = "") }
|
||||
try {
|
||||
val selected = resubmitCandidates(qsoRepository.records.first(), _uiState.value.logbookSelectedIds)
|
||||
if (selected.isEmpty()) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "No complete QSOs in the selection") }
|
||||
return@launch
|
||||
}
|
||||
val preview = lotwUploadRepository.prepare(selected, true)
|
||||
if (preview.count == 0) {
|
||||
// Every checked record proved un-signable — say why instead of opening
|
||||
// a dead preview whose POST could only expire.
|
||||
val message = unavailableUploadSummary(
|
||||
preview.unavailableSkipped, preview.unavailableReasons, duplicates = preview.duplicateSkipped
|
||||
).ifBlank { "No QSOs to resubmit" }
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = message) }
|
||||
return@launch
|
||||
}
|
||||
lastLogbookUploadIds = preview.submittedIds
|
||||
_uiState.update {
|
||||
it.copy(logbookUploadBusy = false, logbookPreview = preview, logbookGridWarning = preview.gridWarning)
|
||||
}
|
||||
} catch (e: com.rtbishop.look4sat.core.domain.repository.LoTWOperationException) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload unavailable: ${e.reason}") }
|
||||
} catch (_: Exception) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload failed") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearLogbookMessage() = _uiState.update { it.copy(logbookUploadMessage = "") }
|
||||
|
||||
private fun loadLoTWUploadStatus() {
|
||||
|
||||
Reference in new issue
Block a user