From 238511aa78ce7adcf40af807d8f00722314f3590 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Fri, 25 Sep 2026 23:03:20 +0800 Subject: [PATCH] fix: logbook mode display + bottom-sheet dialogs - displayMode no longer trusts a stale submode for non-MFSK modes: FM/CW/SSB records (including ones persisted with the old default submode=FT4) now show their real mode; only MODE=MFSK uses the FT4 submode. QsoRecord defaults changed mode=MFSK->FM, submode=FT4->''. - LoTW report bridge sets submode=FT4 for MODE=MFSK so confirmations still match local FT4 records. - Logbook and LoTW upload config dialogs now use SharedDialog (bottom-sheet), matching the LoTW confirmed-grids dialog style. --- .../core/domain/logbook/LoTWReportBridge.kt | 3 + .../domain/logbook/QsoConfirmationMerge.kt | 7 +- .../look4sat/core/domain/logbook/QsoModels.kt | 4 +- .../core/domain/logbook/LogbookMergeTest.kt | 10 + .../settings/LoTWUploadConfigDialog.kt | 189 +++++++++--------- .../feature/settings/LogbookDialog.kt | 37 ++-- gradle.properties | 6 +- 7 files changed, 136 insertions(+), 120 deletions(-) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/LoTWReportBridge.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/LoTWReportBridge.kt index 2ecbbe1a..99dd9f46 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/LoTWReportBridge.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/LoTWReportBridge.kt @@ -14,6 +14,9 @@ fun GridQso.toConfirmedRecord(accountCallsign: String): QsoRecord = QsoRecord( band = bandUp, rxBand = bandDown, mode = mode, + // On satellites, MODE=MFSK means FT4 (ADIF pairing); keep the sub-mode so + // confirmations match local FT4 records (mode=MFSK, submode=FT4). + submode = if (mode.equals("MFSK", true)) "FT4" else "", satelliteName = satName, propagationMode = "SAT", status = QsoStatus.COMPLETE, diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoConfirmationMerge.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoConfirmationMerge.kt index f4dd59ba..95a7bb65 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoConfirmationMerge.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoConfirmationMerge.kt @@ -44,7 +44,12 @@ fun QsoRecord.withConfirmation(confirmed: QsoRecord): QsoRecord = copy( ) val QsoRecord.displayMode: String - get() = submode.ifBlank { mode }.trim().uppercase(Locale.US) + get() { + // Satellite FT4 is MODE=MFSK + SUBMODE=FT4; any other mode keeps its own + // label even when a stale submode default ("FT4") was persisted. + val label = if (mode.equals("MFSK", true) && submode.isNotBlank()) submode else mode + return label.trim().uppercase(Locale.US) + } val QsoRecord.isSatellite: Boolean get() = propagationMode.equals("SAT", true) || (propagationMode.isBlank() && satelliteName.isNotBlank()) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoModels.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoModels.kt index 2c64504d..797b3f5c 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoModels.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/QsoModels.kt @@ -25,8 +25,8 @@ data class QsoRecord( val rxFrequencyHz: Long? = null, val band: String = "", val rxBand: String = "", - val mode: String = "MFSK", - val submode: String = "FT4", + val mode: String = "FM", + val submode: String = "", val satelliteName: String = "", val transponderName: String = "", val satelliteMode: String = "", diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/LogbookMergeTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/LogbookMergeTest.kt index 7338d200..6a38c38f 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/LogbookMergeTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/LogbookMergeTest.kt @@ -123,4 +123,14 @@ class LogbookMergeTest { assertEquals("FT4", ft4.displayMode) assertEquals("FM", local.displayMode) } + + @Test + fun displayMode_ignoresStaleSubmodeForOtherModes() { + // Regression: records persisted with the old default submode="FT4" must not + // show FT4 when the actual mode is FM/CW/SSB. + val staleFm = QsoRecord(startUtcMillis = 0, theirCallsign = "x", myCallsign = "y", mode = "FM", submode = "FT4") + val staleCw = QsoRecord(startUtcMillis = 0, theirCallsign = "x", myCallsign = "y", mode = "CW", submode = "FT4") + assertEquals("FM", staleFm.displayMode) + assertEquals("CW", staleCw.displayMode) + } } diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt index f3599c1c..3babef41 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LoTWUploadConfigDialog.kt @@ -21,15 +21,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -46,7 +43,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rtbishop.look4sat.core.domain.repository.LoTWCertificate import com.rtbishop.look4sat.core.domain.repository.LoTWStation +import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.R +import com.rtbishop.look4sat.core.presentation.SharedDialog @Composable fun LoTWUploadCard( @@ -103,102 +102,102 @@ fun LoTWUploadConfigDialog( } }.getOrNull() ?: uri.lastPathSegment.orEmpty() - AlertDialog( + SharedDialog( + title = stringResource(R.string.prefs_lotw_upload_title), onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.prefs_lotw_upload_title)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - if (busy) { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - CircularProgressIndicator(modifier = Modifier.height(20.dp), strokeWidth = 2.dp) - Text(stringResource(R.string.prefs_lotw_upload_busy), fontSize = 13.sp) - } - } - if (certificate == null) { - Text(stringResource(R.string.prefs_lotw_upload_cert_hint), fontSize = 13.sp) - Button( - onClick = { filePicker.launch(arrayOf("*/*")) }, - enabled = !busy, - modifier = Modifier.fillMaxWidth() - ) { Text(stringResource(R.string.prefs_lotw_upload_import), fontSize = 13.sp) } - selectedFile?.let { uri -> - Text( - text = displayName(uri), - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.primary - ) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text(stringResource(R.string.prefs_lotw_upload_password), fontSize = 13.sp) }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth() - ) - Button( - onClick = { - val bytes = runCatching { - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } - }.getOrNull() - if (bytes != null && bytes.isNotEmpty()) { - onImport(bytes, password.toCharArray()) - password = "" - selectedFile = null - } - }, - enabled = password.isNotBlank() && !busy, - modifier = Modifier.fillMaxWidth() - ) { Text(stringResource(R.string.prefs_lotw_upload_import_confirm), fontSize = 13.sp) } - } - } else { - Text(stringResource(R.string.prefs_lotw_upload_cert_info, certificate.callsign, certificate.dxcc, certificate.expires), fontSize = 13.sp) - OutlinedButton(onClick = onRemove, enabled = !busy) { - Text(stringResource(R.string.prefs_lotw_upload_remove), fontSize = 13.sp) - } - } - Text(stringResource(R.string.prefs_lotw_upload_station_title), style = MaterialTheme.typography.titleSmall) - // Grid field accepts a comma-separated multi-grid set (e.g. "OL62,OL72"). + onCancel = onDismiss, + onAccept = null + ) { + if (busy) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + CircularProgressIndicator(modifier = Modifier.height(20.dp), strokeWidth = 2.dp) + Text(stringResource(R.string.prefs_lotw_upload_busy), fontSize = 13.sp) + } + } + if (certificate == null) { + Text(stringResource(R.string.prefs_lotw_upload_cert_hint), fontSize = 13.sp) + CardButton( + onClick = { filePicker.launch(arrayOf("*/*")) }, + text = stringResource(R.string.prefs_lotw_upload_import), + isEnabled = !busy, + modifier = Modifier.fillMaxWidth() + ) + selectedFile?.let { uri -> + Text( + text = displayName(uri), + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary + ) OutlinedTextField( - value = grid, - onValueChange = { grid = it.uppercase() }, - label = { Text(stringResource(R.string.prefs_lotw_upload_grid), fontSize = 13.sp) }, + value = password, + onValueChange = { password = it }, + label = { Text(stringResource(R.string.prefs_lotw_upload_password), fontSize = 13.sp) }, singleLine = true, - keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(capitalization = KeyboardCapitalization.Characters), + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth() + ) + CardButton( + onClick = { + val bytes = runCatching { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + }.getOrNull() + if (bytes != null && bytes.isNotEmpty()) { + onImport(bytes, password.toCharArray()) + password = "" + selectedFile = null + } + }, + text = stringResource(R.string.prefs_lotw_upload_import_confirm), + isEnabled = password.isNotBlank() && !busy, modifier = Modifier.fillMaxWidth() ) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - OutlinedTextField( - value = cqZone, - onValueChange = { cqZone = it }, - label = { Text("CQZ", fontSize = 12.sp) }, - singleLine = true, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = ituZone, - onValueChange = { ituZone = it }, - label = { Text("ITUZ", fontSize = 12.sp) }, - singleLine = true, - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = iota, - onValueChange = { iota = it.uppercase() }, - label = { Text("IOTA", fontSize = 12.sp) }, - singleLine = true, - modifier = Modifier.weight(1f) - ) - } } - }, - confirmButton = { - TextButton( - onClick = { onSaveStation(LoTWStation(grid, cqZone, ituZone, "", "", iota)) }, - enabled = grid.isNotBlank() && !busy - ) { Text(stringResource(R.string.prefs_lotw_upload_save)) } - }, - dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.prefs_lotw_upload_close)) } } - ) + } else { + Text(stringResource(R.string.prefs_lotw_upload_cert_info, certificate.callsign, certificate.dxcc, certificate.expires), fontSize = 13.sp) + OutlinedButton(onClick = onRemove, enabled = !busy) { + Text(stringResource(R.string.prefs_lotw_upload_remove), fontSize = 13.sp) + } + } + Text(stringResource(R.string.prefs_lotw_upload_station_title), style = MaterialTheme.typography.titleSmall) + // Grid field accepts a comma-separated multi-grid set (e.g. "OL62,OL72"). + OutlinedTextField( + value = grid, + onValueChange = { grid = it.uppercase() }, + label = { Text(stringResource(R.string.prefs_lotw_upload_grid), fontSize = 13.sp) }, + singleLine = true, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(capitalization = KeyboardCapitalization.Characters), + modifier = Modifier.fillMaxWidth() + ) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + OutlinedTextField( + value = cqZone, + onValueChange = { cqZone = it }, + label = { Text("CQZ", fontSize = 12.sp) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = ituZone, + onValueChange = { ituZone = it }, + label = { Text("ITUZ", fontSize = 12.sp) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = iota, + onValueChange = { iota = it.uppercase() }, + label = { Text("IOTA", fontSize = 12.sp) }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + } + CardButton( + onClick = { onSaveStation(LoTWStation(grid, cqZone, ituZone, "", "", iota)) }, + text = stringResource(R.string.prefs_lotw_upload_save), + isEnabled = grid.isNotBlank() && !busy, + modifier = Modifier.fillMaxWidth() + ) + } } diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt index 17cf0574..5d96a899 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/LogbookDialog.kt @@ -14,17 +14,15 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -36,6 +34,7 @@ import androidx.compose.ui.unit.sp import com.rtbishop.look4sat.core.domain.logbook.QsoRecord import com.rtbishop.look4sat.core.domain.logbook.displayMode import com.rtbishop.look4sat.core.presentation.R +import com.rtbishop.look4sat.core.presentation.SharedDialog import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -65,25 +64,25 @@ fun LogbookDialog( onDismiss: () -> Unit, onDelete: (Long) -> Unit ) { - AlertDialog( + SharedDialog( + title = stringResource(R.string.prefs_logbook_title), onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.prefs_logbook_title)) }, - text = { - if (records.isEmpty()) { - Text(stringResource(R.string.prefs_logbook_empty), fontSize = 14.sp) - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - items(records, key = { it.id }) { record -> - LogbookRow(record, onDelete = { onDelete(record.id) }) - } + onCancel = onDismiss, + onAccept = null + ) { + if (records.isEmpty()) { + Text(stringResource(R.string.prefs_logbook_empty), fontSize = 14.sp) + } else { + LazyColumn( + modifier = Modifier.fillMaxWidth().heightIn(max = 420.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(records, key = { it.id }) { record -> + LogbookRow(record, onDelete = { onDelete(record.id) }) } } - }, - confirmButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.prefs_logbook_close)) } } - ) + } + } } @Composable diff --git a/gradle.properties b/gradle.properties index 6308f0e2..54de7fae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Configure your build environment: http://www.gradle.org/docs/current/userguide/build_environment.html org.gradle.caching=true -org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -org.gradle.parallel=true -org.gradle.workers.max=4 +org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.parallel=false +org.gradle.workers.max=2