mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
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.
This commit is contained in:
1 parent
e606afb0f3
commit
238511aa78
7 files changed
+136
-120
No files matched your search
+3
@@ -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,
|
||||
|
||||
+6
-1
@@ -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())
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
+10
@@ -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)
|
||||
}
|
||||
}
|
||||
+94
-95
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+18
-19
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
Reference in new issue
Block a user