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})?$")