From ee32c7a1eb3fc2bbfe65476a609ea653596b19d0 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Sat, 22 Aug 2026 20:50:39 +0800 Subject: [PATCH] feat: confirm AMSAT report uploads --- .../src/main/res/values-zh/strings.xml | 5 + .../src/main/res/values/strings.xml | 5 + .../feature/status/SatStatusScreen.kt | 73 ++++++++++-- .../feature/status/SatStatusViewModel.kt | 112 ++++++++++++++---- 4 files changed, 162 insertions(+), 33 deletions(-) diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index e4d99acc..71f4e419 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -54,12 +54,17 @@ 此时间段无报告 关闭 上传 + 返回 上传报告到 AMSAT 状态 呼号 网格 时间:当前 UTC %s 提交到 AMSAT + 确认上传 + 确认上传到 AMSAT + 这条公开报告将上传到 AMSAT。\n卫星:%1$s\n状态:%2$s\n呼号:%3$s\n网格:%4$s + 未填写 报告已上传 上传失败:%s 请填写呼号 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 4ab064c8..c1069158 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -71,12 +71,17 @@ No reports for this period Close Upload + Back Upload report to AMSAT Status Callsign Grid Time: current UTC %s Submit to AMSAT + Upload + Confirm AMSAT upload + This public report will be uploaded to AMSAT.\nSatellite: %1$s\nStatus: %2$s\nCallsign: %3$s\nGrid: %4$s + Not provided Report uploaded Upload failed: %s Callsign is required diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt index 4beecb95..bc8c2dc8 100644 --- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilterChip @@ -113,7 +114,9 @@ fun SatStatusDestination() { onUploadReportChange = viewModel::setUploadReport, onUploadCallsignChange = viewModel::setUploadCallsign, onUploadGridChange = viewModel::setUploadGrid, - onSubmitUpload = viewModel::submitReport, + onRequestSubmitUpload = viewModel::requestSubmitConfirmation, + onConfirmSubmitUpload = viewModel::confirmSubmitReport, + onDismissSubmitConfirmation = viewModel::dismissSubmitConfirmation, onDismissUpload = viewModel::collapseUploadPanel ) } @@ -126,7 +129,9 @@ private fun SatStatusScreen( onUploadReportChange: (String) -> Unit, onUploadCallsignChange: (String) -> Unit, onUploadGridChange: (String) -> Unit, - onSubmitUpload: (String) -> Unit, + onRequestSubmitUpload: () -> Unit, + onConfirmSubmitUpload: (String) -> Unit, + onDismissSubmitConfirmation: () -> Unit, onDismissUpload: () -> Unit ) { var selectedDay by remember { mutableStateOf?>(null) } @@ -172,7 +177,9 @@ private fun SatStatusScreen( onReportChange = onUploadReportChange, onCallsignChange = onUploadCallsignChange, onGridChange = onUploadGridChange, - onSubmitReport = { onSubmitUpload(status.name) }, + onRequestSubmitReport = onRequestSubmitUpload, + onConfirmSubmitReport = { onConfirmSubmitUpload(status.name) }, + onDismissSubmitConfirmation = onDismissSubmitConfirmation, onDismiss = { selectedDay = null onDismissUpload() @@ -342,7 +349,9 @@ private fun ReportDialog( onReportChange: (String) -> Unit, onCallsignChange: (String) -> Unit, onGridChange: (String) -> Unit, - onSubmitReport: () -> Unit, + onRequestSubmitReport: () -> Unit, + onConfirmSubmitReport: () -> Unit, + onDismissSubmitConfirmation: () -> Unit, onDismiss: () -> Unit ) { val dayReports = day.slots.flatMap { it.reportIds }.mapNotNull { reports[it] } @@ -351,7 +360,10 @@ private fun ReportDialog( onDismiss = onDismiss, onAccept = onDismiss, extraAction = { - CardButton(onClick = onToggleUpload, text = stringResource(R.string.amsat_upload)) + CardButton( + onClick = onToggleUpload, + text = stringResource(if (upload.isExpanded) R.string.amsat_upload_back else R.string.amsat_upload) + ) } ) { AnimatedVisibility(visible = upload.isExpanded) { @@ -361,7 +373,15 @@ private fun ReportDialog( onReportChange = onReportChange, onCallsignChange = onCallsignChange, onGridChange = onGridChange, - onSubmitReport = onSubmitReport + onRequestSubmitReport = onRequestSubmitReport + ) + } + if (upload.isConfirmingSubmit) { + AmSatUploadConfirmDialog( + statusName = statusName, + upload = upload, + onConfirm = onConfirmSubmitReport, + onDismiss = onDismissSubmitConfirmation ) } if (dayReports.isEmpty()) { @@ -405,7 +425,7 @@ private fun AmSatUploadPanel( onReportChange: (String) -> Unit, onCallsignChange: (String) -> Unit, onGridChange: (String) -> Unit, - onSubmitReport: () -> Unit + onRequestSubmitReport: () -> Unit ) { val options = remember(statusName) { if (statusName.startsWith("ISS")) UPLOAD_OPTIONS else UPLOAD_OPTIONS.filter { it.apiValue != AMSAT_REPORT_CREW_ACTIVE } @@ -488,7 +508,7 @@ private fun AmSatUploadPanel( ) } Button( - onClick = onSubmitReport, + onClick = onRequestSubmitReport, enabled = !upload.isSubmitting && upload.callsign.isNotBlank(), modifier = Modifier.fillMaxWidth() ) { @@ -517,6 +537,43 @@ private fun AmSatUploadPanel( } } +@Composable +private fun AmSatUploadConfirmDialog( + statusName: String, + upload: AmSatUploadUiState, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val option = UPLOAD_OPTIONS.firstOrNull { it.apiValue == upload.selectedReport } ?: UPLOAD_OPTIONS.first() + val statusLabel = stringResource(option.labelResId) + val grid = upload.gridSquare.ifBlank { stringResource(R.string.amsat_upload_grid_none) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.amsat_upload_confirm_title)) }, + text = { + Text( + text = stringResource( + R.string.amsat_upload_confirm_message, + statusName, + statusLabel, + upload.callsign, + grid + ) + ) + }, + confirmButton = { + TextButton(onClick = onConfirm, enabled = !upload.isSubmitting) { + Text(stringResource(R.string.amsat_upload_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss, enabled = !upload.isSubmitting) { + Text(stringResource(R.string.btn_cancel)) + } + } + ) +} + @Composable private fun uploadErrorText(error: String): String { return when (error) { diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt index 1686fe91..544f6cf3 100644 --- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt +++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt @@ -31,6 +31,7 @@ data class AmSatUploadUiState( val callsign: String = "", val gridSquare: String = "", val isSubmitting: Boolean = false, + val isConfirmingSubmit: Boolean = false, val message: String? = null, val error: String? = null ) @@ -125,56 +126,75 @@ class SatStatusViewModel( error = null ) } else { - upload.copy(isExpanded = false, message = null, error = null) + upload.copy(isExpanded = false, isConfirmingSubmit = false, message = null, error = null) } ) } } fun collapseUploadPanel() { - _uiState.update { it.copy(upload = it.upload.copy(isExpanded = false, message = null, error = null)) } + _uiState.update { it.copy(upload = it.upload.copy(isExpanded = false, isConfirmingSubmit = false, message = null, error = null)) } } fun setUploadReport(report: String) { - _uiState.update { it.copy(upload = it.upload.copy(selectedReport = report, message = null, error = null)) } + _uiState.update { it.copy(upload = it.upload.copy(selectedReport = report, isConfirmingSubmit = false, message = null, error = null)) } } fun setUploadCallsign(callsign: String) { val normalized = callsign.trim().uppercase(Locale.US) settingsRepo.setAmSatCallsign(normalized) - _uiState.update { it.copy(upload = it.upload.copy(callsign = normalized, message = null, error = null)) } + _uiState.update { it.copy(upload = it.upload.copy(callsign = normalized, isConfirmingSubmit = false, message = null, error = null)) } } fun setUploadGrid(grid: String) { val normalized = grid.trim().uppercase(Locale.US) - _uiState.update { it.copy(upload = it.upload.copy(gridSquare = normalized, message = null, error = null)) } + _uiState.update { it.copy(upload = it.upload.copy(gridSquare = normalized, isConfirmingSubmit = false, message = null, error = null)) } } - fun submitReport(satelliteName: String) { + fun requestSubmitConfirmation() { + val prepared = validateUpload(_uiState.value.upload) ?: return + _uiState.update { + it.copy( + upload = it.upload.copy( + callsign = prepared.callsign, + gridSquare = prepared.grid, + isConfirmingSubmit = true, + message = null, + error = null + ) + ) + } + } + + fun dismissSubmitConfirmation() { + _uiState.update { it.copy(upload = it.upload.copy(isConfirmingSubmit = false)) } + } + + fun confirmSubmitReport(satelliteName: String) { val upload = _uiState.value.upload if (upload.isSubmitting) return - val callsign = upload.callsign.trim().uppercase(Locale.US) - val grid = upload.gridSquare.trim().uppercase(Locale.US) - when { - callsign.isBlank() -> { - _uiState.update { it.copy(upload = it.upload.copy(error = UPLOAD_ERROR_CALLSIGN_REQUIRED, message = null)) } - return - } - grid.isNotBlank() && !MAIDENHEAD_GRID.matches(grid) -> { - _uiState.update { it.copy(upload = it.upload.copy(error = UPLOAD_ERROR_GRID_INVALID, message = null)) } - return - } - } + val prepared = validateUpload(upload) ?: return - settingsRepo.setAmSatCallsign(callsign) - _uiState.update { it.copy(upload = it.upload.copy(isSubmitting = true, error = null, message = null)) } + settingsRepo.setAmSatCallsign(prepared.callsign) + _uiState.update { + it.copy( + upload = it.upload.copy( + callsign = prepared.callsign, + gridSquare = prepared.grid, + isSubmitting = true, + isConfirmingSubmit = false, + error = null, + message = null + ) + ) + } viewModelScope.launch { val result = amSatRepo.submitReport( AmSatReportSubmission( name = satelliteName, - report = upload.selectedReport, - callsign = callsign, - gridSquare = grid, + report = prepared.report, + callsign = prepared.callsign, + gridSquare = prepared.grid, reportedAtUtcMillis = System.currentTimeMillis() ) ) @@ -183,8 +203,8 @@ class SatStatusViewModel( it.copy( upload = it.upload.copy( isSubmitting = false, - callsign = callsign, - gridSquare = grid, + callsign = prepared.callsign, + gridSquare = prepared.grid, message = UPLOAD_MESSAGE_SUCCESS, error = null ) @@ -205,6 +225,42 @@ class SatStatusViewModel( } } + private fun validateUpload(upload: AmSatUploadUiState): PreparedUpload? { + val callsign = upload.callsign.trim().uppercase(Locale.US) + val grid = upload.gridSquare.trim().uppercase(Locale.US) + return when { + callsign.isBlank() -> { + _uiState.update { + it.copy( + upload = it.upload.copy( + isConfirmingSubmit = false, + error = UPLOAD_ERROR_CALLSIGN_REQUIRED, + message = null + ) + ) + } + null + } + grid.isNotBlank() && !MAIDENHEAD_GRID.matches(grid) -> { + _uiState.update { + it.copy( + upload = it.upload.copy( + isConfirmingSubmit = false, + error = UPLOAD_ERROR_GRID_INVALID, + message = null + ) + ) + } + null + } + else -> PreparedUpload( + report = upload.selectedReport, + callsign = callsign, + grid = grid + ) + } + } + private fun defaultGridSquare(): String { val grid = settingsRepo.stationPosition.value.qthLocator.trim().uppercase(Locale.US) return grid.takeUnless { it == "JJ00AA" }.orEmpty() @@ -219,4 +275,10 @@ class SatStatusViewModel( } } +private data class PreparedUpload( + val report: String, + val callsign: String, + val grid: String +) + private val MAIDENHEAD_GRID = Regex("^[A-R]{2}[0-9]{2}([A-X]{2})?$")