mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 03:17:21 +00:00
feat(lotw): 漫游核验改用「当前定位 vs 证书台址」,移除记录网格核对
- 新增 LoTWPositionWarning + positionWarning(currentGrid, stationGrids):当前定位网格不在 证书台址网格集内 → 提示(4 位归一;任一侧缺失不报) - ISettingsRepo.getCurrentGrid():只读取「≤24h 系统最后已知定位」与「存储台址」中较新者, 不写任何设置(避开 setStationPosition 的写副作用) - 上传前核对弹窗(雷达 / 日志本两入口)改由 VM 在 prepare 后计算;雷达记录页新增提示行 (stationMismatch,点击=跳转改台址并预填当前格) - 移除旧「记录 vs 台址」核对:uploadGridWarning / LoTWGridWarning / preview.gridWarning 及 相关 UI / 字符串全删;LoTWPositionWarningDialog 重命名替代 LoTWGridWarningDialog - 测试:LoTWPositionWarningTest ×5 替代 LoTWGridWarningTest;domain 171 / data 116 / mutual 28 全绿
This commit is contained in:
1 parent
2e704ee1d3
commit
56a788138b
19 files changed
+222
-158
No files matched your search
@@ -30,7 +30,6 @@ import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWStationMeta
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadAudit
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview
|
||||
import com.rtbishop.look4sat.core.domain.repository.uploadGridWarning
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ensureActive
|
||||
@@ -287,9 +286,6 @@ class LoTWUploadRepository internal constructor(
|
||||
signing.location["MY_VUCC_GRIDS"]?.split(',')
|
||||
?.map(String::trim)?.filter(String::isNotBlank)?.let(::addAll)
|
||||
}.distinct()
|
||||
// Roaming guard: flag records whose own grids fall outside the station grids this
|
||||
// batch would be signed with (a station location never updated while roaming).
|
||||
val gridWarning = uploadGridWarning(contacts.map { it.record }, locationGrids)
|
||||
val preview = LoTWUploadPreview(
|
||||
UUID.randomUUID().toString(), signing.key.info.callsign, signing.key.info.dxcc, signing.location.getValue("GRIDSQUARE"),
|
||||
contacts.size, skipped,
|
||||
@@ -302,7 +298,6 @@ class LoTWUploadRepository internal constructor(
|
||||
duplicates,
|
||||
contacts.map { it.record.id },
|
||||
locationGrids,
|
||||
gridWarning,
|
||||
resubmit
|
||||
)
|
||||
if (contacts.isNotEmpty()) pending = Pending(
|
||||
|
||||
@@ -468,6 +468,34 @@ class SettingsRepo(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getCurrentGrid(): String? {
|
||||
val station = _stationPosition.value
|
||||
val now = System.currentTimeMillis()
|
||||
// Only trust a location fix that is both recent (≤ 24 h) and newer than the stored
|
||||
// station, so a stale fix can never override a position the operator just set.
|
||||
val fix = lastKnownGridFix()?.takeIf { now - it.second <= 24 * 3_600_000L }
|
||||
val chosen = if (fix != null && fix.second >= station.timestamp) fix.first else station.qthLocator
|
||||
return chosen.takeIf(String::isNotBlank)
|
||||
}
|
||||
|
||||
/** Last known GPS/NETWORK fix converted to a locator; read-only, null without permission. */
|
||||
private fun lastKnownGridFix(): Pair<String, Long>? {
|
||||
return try {
|
||||
val provider = when {
|
||||
LocationManagerCompat.hasProvider(locationManager, providerGps) -> providerGps
|
||||
LocationManagerCompat.hasProvider(locationManager, providerNet) -> providerNet
|
||||
else -> return null
|
||||
}
|
||||
val location = locationManager.getLastKnownLocation(provider) ?: return null
|
||||
val locator = positionToQth(location.latitude, location.longitude) ?: return null
|
||||
locator to location.time
|
||||
} catch (_: SecurityException) {
|
||||
null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStationPosition(): GeoPos {
|
||||
val latitude = (preferences.getString(keyStationLatitude, null) ?: "0.0").toDouble()
|
||||
val longitude = (preferences.getString(keyStationLongitude, null) ?: "0.0").toDouble()
|
||||
|
||||
@@ -271,6 +271,8 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
||||
|
||||
override fun setStationPosition(locator: String): Boolean = true
|
||||
|
||||
override fun getCurrentGrid(): String? = null
|
||||
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> = emptyList()
|
||||
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||
|
||||
+1
@@ -330,6 +330,7 @@ private class FakeSettingsRepoForSearch : ISettingsRepo {
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true
|
||||
override fun setStationPosition(): Boolean = true
|
||||
override fun setStationPosition(locator: String): Boolean = true
|
||||
override fun getCurrentGrid(): String? = null
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> =
|
||||
types.flatMap { typeIds[it].orEmpty() }.distinct()
|
||||
|
||||
|
||||
+15
-29
@@ -102,20 +102,17 @@ data class LoTWUploadPreview(
|
||||
* 2 on a line, 4 on a corner). Stamped onto the uploaded records so the logbook
|
||||
* can show which gridsquares the QSO went out under. */
|
||||
val grids: List<String> = emptyList(),
|
||||
/** Records whose own grids fall outside this batch's station location — the fingerprint
|
||||
* of uploading while roaming with a stale station location. Null when all covered. */
|
||||
val gridWarning: LoTWGridWarning? = null,
|
||||
/** True when this batch is a resubmit: already-uploaded/confirmed records were allowed
|
||||
* through so the corrected station location reaches LoTW as an update of the contact. */
|
||||
val resubmit: Boolean = false
|
||||
)
|
||||
|
||||
/** Pre-upload grid audit result: [count] records carry own grids outside the station grids. */
|
||||
data class LoTWGridWarning(
|
||||
val count: Int,
|
||||
/** Distinct own grids of the affected records (normalized, 4 characters). */
|
||||
val recordGrids: List<String>,
|
||||
/** Distinct station-location grids this batch would be signed with. */
|
||||
/** Pre-upload location check: the freshest known position grid is not covered by the
|
||||
* station grids an upload would be signed with. */
|
||||
data class LoTWPositionWarning(
|
||||
/** Normalized 4-character grid of the freshest known position. */
|
||||
val currentGrid: String,
|
||||
/** Distinct station-location grids the upload would be signed with. */
|
||||
val stationGrids: List<String>
|
||||
)
|
||||
|
||||
@@ -154,30 +151,19 @@ enum class LoTWProblem {
|
||||
}
|
||||
|
||||
/**
|
||||
* Records whose own grids (the set a previous upload stamped, else the logged grid) do not
|
||||
* appear at all in the station-location grids an upload goes out under — the signature of
|
||||
* uploading while roaming with a station location that was never updated. Records without
|
||||
* any grid are not flagged, and a partially covered set (boundary operations) is not
|
||||
* flagged either: only a fully disjoint pair, which is always a mistake, warns.
|
||||
* Roaming check run before upload: warns when the operator's freshest known position
|
||||
* (see [ISettingsRepo.getCurrentGrid]) lies outside every grid of the station location the
|
||||
* batch would be signed with — the signature of operating while roaming with a station
|
||||
* location that was never updated. An unknown position, missing station grids, or a covered
|
||||
* position (boundary operations included) produce no warning.
|
||||
*/
|
||||
fun uploadGridWarning(records: List<QsoRecord>, stationGrids: List<String>): LoTWGridWarning? {
|
||||
fun positionWarning(currentGrid: String?, stationGrids: List<String>): LoTWPositionWarning? {
|
||||
val current = currentGrid?.grid4() ?: return null
|
||||
val station = stationGrids.mapNotNull { it.grid4() }.toSet()
|
||||
if (station.isEmpty()) return null
|
||||
val affected = records.filter { record ->
|
||||
val own = record.ownGrids()
|
||||
own.isNotEmpty() && own.none { it in station }
|
||||
}
|
||||
if (affected.isEmpty()) return null
|
||||
return LoTWGridWarning(
|
||||
affected.size,
|
||||
affected.flatMap { it.ownGrids() }.distinct().sorted(),
|
||||
station.sorted()
|
||||
)
|
||||
if (current in station) return null
|
||||
return LoTWPositionWarning(current, station.sorted())
|
||||
}
|
||||
|
||||
/** The grid set a previous upload stamped onto the record, else the grid it was logged under. */
|
||||
private fun QsoRecord.ownGrids(): Set<String> =
|
||||
(vuccGrids.ifEmpty { listOf(myGrid) }).mapNotNull { it.grid4() }.toSet()
|
||||
|
||||
/** Normalized 4-character grid, null when the value is not a usable grid. */
|
||||
private fun String.grid4(): String? = trim().uppercase(Locale.US).take(4).takeIf { it.length >= 4 }
|
||||
+6
@@ -48,6 +48,12 @@ interface ISettingsRepo {
|
||||
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
|
||||
fun setStationPosition(): Boolean
|
||||
fun setStationPosition(locator: String): Boolean
|
||||
/**
|
||||
* Freshest known position grid for the roaming check, read-only: the newer of a fresh
|
||||
* (≤ 24 h) system location fix and the stored station position. Null only when neither
|
||||
* yields a usable locator. Never writes the station position.
|
||||
*/
|
||||
fun getCurrentGrid(): String?
|
||||
//endregion
|
||||
|
||||
//region # Database update settings
|
||||
|
||||
+1
@@ -276,6 +276,7 @@ class LoTWGridSyncTest {
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = TODO()
|
||||
override fun setStationPosition(): Boolean = TODO()
|
||||
override fun setStationPosition(locator: String): Boolean = TODO()
|
||||
override fun getCurrentGrid(): String? = TODO()
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> = TODO()
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) = TODO()
|
||||
override fun updateDatabaseState(state: DatabaseState) = TODO()
|
||||
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.logbook.QsoRecord
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class LoTWGridWarningTest {
|
||||
|
||||
private fun record(myGrid: String, vuccGrids: List<String> = emptyList()) = QsoRecord(
|
||||
startUtcMillis = 1_700_000_000_000L,
|
||||
theirCallsign = "BG5JSB",
|
||||
myCallsign = "BA7OPF",
|
||||
myGrid = myGrid,
|
||||
vuccGrids = vuccGrids,
|
||||
mode = "FM",
|
||||
satelliteName = "SO-50",
|
||||
propagationMode = "SAT"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun matchingGridsProduceNoWarning() {
|
||||
assertNull(uploadGridWarning(listOf(record("OM91")), listOf("OM91")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recordsLoggedOutsideTheStationAreCounted() {
|
||||
val warning = uploadGridWarning(
|
||||
listOf(record("OM91"), record("OM91"), record("OL62")),
|
||||
listOf("OL62")
|
||||
)
|
||||
assertEquals(2, warning?.count)
|
||||
assertEquals(listOf("OM91"), warning?.recordGrids)
|
||||
assertEquals(listOf("OL62"), warning?.stationGrids)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun boundaryStationCoversItsRecords() {
|
||||
val warning = uploadGridWarning(
|
||||
listOf(record("OL62", listOf("OL61", "OL62"))),
|
||||
listOf("OL61", "OL62", "OM60", "OM61")
|
||||
)
|
||||
assertNull(warning)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun partialOverlapIsNotFlagged() {
|
||||
// The record's set touches the station set — a boundary-bookkeeping difference,
|
||||
// not the roaming signature.
|
||||
assertNull(uploadGridWarning(listOf(record("OL62", listOf("OL62", "OL63"))), listOf("OL62")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recordsWithoutGridsOrStationAreNotFlagged() {
|
||||
assertNull(uploadGridWarning(listOf(record("")), listOf("OL62")))
|
||||
assertNull(uploadGridWarning(listOf(record("OM91")), emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stampedGridSetIsComparedInsteadOfTheLoggedGrid() {
|
||||
val warning = uploadGridWarning(
|
||||
listOf(record("OL62", listOf("OM91", "OM92", "PM01", "PM02"))),
|
||||
listOf("OL62")
|
||||
)
|
||||
assertEquals(1, warning?.count)
|
||||
assertEquals(listOf("OM91", "OM92", "PM01", "PM02"), warning?.recordGrids)
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class LoTWPositionWarningTest {
|
||||
|
||||
@Test
|
||||
fun coveredPositionProducesNoWarning() {
|
||||
assertNull(positionWarning("OL62aa", listOf("OL62")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun positionInsideAnyStationGridIsCovered() {
|
||||
assertNull(positionWarning("OL61", listOf("OL61", "OL62", "OM60", "OM61")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun positionOutsideTheStationWarns() {
|
||||
val warning = positionWarning("om91", listOf("OL62"))
|
||||
assertEquals("OM91", warning?.currentGrid)
|
||||
assertEquals(listOf("OL62"), warning?.stationGrids)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stationGridsAreNormalizedForComparison() {
|
||||
assertNull(positionWarning("OL62", listOf("ol62aa")))
|
||||
assertNull(positionWarning("ol62", listOf("OL62")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownPositionOrStationProducesNoWarning() {
|
||||
assertNull(positionWarning(null, listOf("OL62")))
|
||||
assertNull(positionWarning("", listOf("OL62")))
|
||||
assertNull(positionWarning("OM91", emptyList()))
|
||||
assertNull(positionWarning("OM91", listOf("")))
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -23,17 +23,17 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWGridWarning
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWPositionWarning
|
||||
|
||||
/**
|
||||
* Pre-upload grid check: the prepared batch holds records whose own grids fall outside the
|
||||
* station-location grids they would be signed with — the fingerprint of uploading while
|
||||
* Pre-upload position check: the operator's current position grid falls outside the station
|
||||
* location the prepared batch would be signed with — the fingerprint of operating while
|
||||
* roaming with a station location that was never updated. The operator either jumps to the
|
||||
* station location to fix it, or ignores the warning and proceeds to the normal preview.
|
||||
*/
|
||||
@Composable
|
||||
fun LoTWGridWarningDialog(
|
||||
warning: LoTWGridWarning,
|
||||
fun LoTWPositionWarningDialog(
|
||||
warning: LoTWPositionWarning,
|
||||
onFixStation: () -> Unit,
|
||||
onIgnore: () -> Unit
|
||||
) {
|
||||
@@ -46,8 +46,7 @@ fun LoTWGridWarningDialog(
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.lotw_upload_grid_mismatch,
|
||||
warning.count,
|
||||
gridsLabel(warning.recordGrids),
|
||||
warning.currentGrid,
|
||||
gridsLabel(warning.stationGrids)
|
||||
),
|
||||
fontSize = 14.sp
|
||||
@@ -204,7 +204,8 @@
|
||||
<string name="lotw_upload_grid_title">上传前核对</string>
|
||||
<string name="lotw_upload_grid_fix">修改台址</string>
|
||||
<string name="lotw_upload_grid_ignore">忽略</string>
|
||||
<string name="lotw_upload_grid_mismatch">有 %1$d 条记录的自己的网格不在上传台址内(记录:%2$s;台址:%3$s)——请确认台址是通联时实际所在的网格。</string>
|
||||
<string name="lotw_upload_grid_mismatch">当前定位 %1$s 不在证书台址网格 %2$s 内——如果正在此处操作,请先更新台址再上传。</string>
|
||||
<string name="lotw_upload_pos_hint">当前定位 %1$s 不在证书台址网格 %2$s 内 · 点此更新</string>
|
||||
<string name="prefs_lotw_upload_title">LoTW 上传证书</string>
|
||||
<string name="prefs_lotw_upload_configured">证书已导入 · 台址网格 %1$s</string>
|
||||
<string name="prefs_lotw_upload_not_configured">未配置 — 导入 TrustedQSL 证书后即可上传通联</string>
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
<string name="lotw_upload_grid_title">Upload check</string>
|
||||
<string name="lotw_upload_grid_fix">Fix station location</string>
|
||||
<string name="lotw_upload_grid_ignore">Ignore</string>
|
||||
<string name="lotw_upload_grid_mismatch">%1$d record(s) log a grid outside the upload station location (records: %2$s; station: %3$s). Verify the station location is where you actually operated.</string>
|
||||
<string name="lotw_upload_grid_mismatch">Current position %1$s is outside the certificate station grids %2$s — if you are operating here, update the station location before uploading.</string>
|
||||
<string name="lotw_upload_pos_hint">Current position %1$s is outside the station grids %2$s — tap to update</string>
|
||||
<string name="prefs_lotw_upload_title">LoTW upload certificate</string>
|
||||
<string name="prefs_lotw_upload_configured">Certificate imported · station grid %1$s</string>
|
||||
<string name="prefs_lotw_upload_not_configured">Not configured — import your TrustedQSL certificate to upload QSOs</string>
|
||||
|
||||
@@ -115,6 +115,7 @@ class FakeSettingsRepo(
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = TODO()
|
||||
override fun setStationPosition(): Boolean = TODO()
|
||||
override fun setStationPosition(locator: String): Boolean = TODO()
|
||||
override fun getCurrentGrid(): String? = TODO()
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> = TODO()
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) = TODO()
|
||||
override fun updateDatabaseState(state: DatabaseState) = TODO()
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -66,7 +67,7 @@ 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.LoTWPositionWarningDialog
|
||||
|
||||
@Composable
|
||||
fun LogPage(
|
||||
@@ -105,6 +106,12 @@ fun LogPage(
|
||||
if (catnum != 0) logViewModel.selectSatellite(catnum)
|
||||
}
|
||||
|
||||
// Refresh the roaming hint whenever the page re-enters composition (e.g. returning
|
||||
// from the station-location fix jump).
|
||||
LaunchedEffect(Unit) {
|
||||
logViewModel.refreshPositionHint()
|
||||
}
|
||||
|
||||
val selectedTxHz = remember(selectedRadio) { selectedRadio?.uplinkHz() }
|
||||
val selectedRxHz = remember(selectedRadio) { selectedRadio?.downlinkHz() }
|
||||
val repeaterTxHz = voiceRepeater?.uplinkHz()
|
||||
@@ -152,6 +159,20 @@ fun LogPage(
|
||||
enabled = logUiState.callsignInput.isNotBlank() && satName.isNotBlank()
|
||||
) { Text("Log") }
|
||||
}
|
||||
logUiState.stationMismatch?.let { mismatch ->
|
||||
Text(
|
||||
text = "⚠ " + stringResource(
|
||||
R.string.lotw_upload_pos_hint,
|
||||
mismatch.currentGrid,
|
||||
gridsLabel(mismatch.stationGrids)
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onFixGrid(listOf(mismatch.currentGrid)) }
|
||||
)
|
||||
}
|
||||
if (isLinear) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
listOf("CW", "SSB", "FT4").forEach { candidate ->
|
||||
@@ -222,19 +243,19 @@ fun LogPage(
|
||||
)
|
||||
}
|
||||
|
||||
logUiState.uploadGridWarning?.let { warning ->
|
||||
LoTWGridWarningDialog(
|
||||
logUiState.uploadPositionWarning?.let { warning ->
|
||||
LoTWPositionWarningDialog(
|
||||
warning = warning,
|
||||
onFixStation = {
|
||||
logViewModel.abandonForGridFix()
|
||||
onFixGrid(warning.recordGrids)
|
||||
onFixGrid(listOf(warning.currentGrid))
|
||||
},
|
||||
onIgnore = logViewModel::ignoreGridWarning
|
||||
onIgnore = logViewModel::ignorePositionWarning
|
||||
)
|
||||
}
|
||||
|
||||
// The preview only opens once the grid check is out of the way.
|
||||
if (logUiState.uploadGridWarning == null) {
|
||||
// The preview only opens once the position check is out of the way.
|
||||
if (logUiState.uploadPositionWarning == null) {
|
||||
logUiState.preview?.let { preview ->
|
||||
UploadPreviewDialog(
|
||||
preview = preview,
|
||||
|
||||
@@ -25,9 +25,10 @@ import com.rtbishop.look4sat.core.domain.repository.ILoTWUploadRepository
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWOperationException
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWPositionWarning
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWGridWarning
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
|
||||
import com.rtbishop.look4sat.core.domain.repository.positionWarning
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -67,9 +68,12 @@ data class LogUiState(
|
||||
/** ARRL satellite names (config.tq6) — the only names a record may be signed with. */
|
||||
val satelliteCatalog: List<String> = emptyList(),
|
||||
val preview: 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 uploadGridWarning: LoTWGridWarning? = null,
|
||||
/** Roaming guard: the current position grid is outside the station grids the prepared
|
||||
* batch would be signed with — shown as a dialog before the preview opens. */
|
||||
val uploadPositionWarning: LoTWPositionWarning? = null,
|
||||
/** Roaming hint on the log page: current position grid outside the station grids
|
||||
* (null when covered or unknown). Tapping it jumps to the station location. */
|
||||
val stationMismatch: LoTWPositionWarning? = null,
|
||||
/** User-facing upload / record message ("" when none). */
|
||||
val message: String = "",
|
||||
val postText: String? = null,
|
||||
@@ -108,6 +112,10 @@ class LogViewModel(
|
||||
)
|
||||
}
|
||||
}
|
||||
// The roaming hint tracks the freshest position data: refresh on init and every fix.
|
||||
viewModelScope.launch {
|
||||
settingsRepo.stationPosition.collect { refreshPositionHintNow() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCallsign(value: String) =
|
||||
@@ -268,7 +276,13 @@ class LogViewModel(
|
||||
// Only the records that actually made it into the TQ8 may be
|
||||
// marked uploaded later — never the whole candidate list.
|
||||
lastUploadedIds = preview.submittedIds
|
||||
_uiState.update { it.copy(busy = false, preview = preview, uploadGridWarning = preview.gridWarning) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
busy = false,
|
||||
preview = preview,
|
||||
uploadPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
|
||||
)
|
||||
}
|
||||
} catch (e: LoTWOperationException) {
|
||||
_uiState.update { it.copy(busy = false, message = "Upload unavailable: ${e.reason}") }
|
||||
} catch (_: Exception) {
|
||||
@@ -300,14 +314,32 @@ class LogViewModel(
|
||||
|
||||
fun dismissPreview() = _uiState.update { it.copy(preview = null) }
|
||||
|
||||
/** Operator chose "ignore" on the grid check: keep the prepared preview. */
|
||||
fun ignoreGridWarning() = _uiState.update { it.copy(uploadGridWarning = null) }
|
||||
/** Operator chose "ignore" on the position check: keep the prepared preview. */
|
||||
fun ignorePositionWarning() = _uiState.update { it.copy(uploadPositionWarning = null) }
|
||||
|
||||
/** Operator chose to fix the station grid first: drop the prepared preview and leave. */
|
||||
fun abandonForGridFix() {
|
||||
lotwUploadRepository.discardPreview()
|
||||
lastUploadedIds = emptyList()
|
||||
_uiState.update { it.copy(uploadGridWarning = null, preview = null) }
|
||||
_uiState.update { it.copy(uploadPositionWarning = null, preview = null) }
|
||||
}
|
||||
|
||||
/** Recompute the log-page roaming hint from the freshest station/position data. */
|
||||
fun refreshPositionHint() = viewModelScope.launch { refreshPositionHintNow() }
|
||||
|
||||
private suspend fun refreshPositionHintNow() {
|
||||
val station = runCatching { lotwUploadRepository.station() }.getOrNull()
|
||||
if (station == null) {
|
||||
_uiState.update { it.copy(stationMismatch = null) }
|
||||
return
|
||||
}
|
||||
val grids = station.grid.split(',').map(String::trim).filter(String::isNotBlank)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
stationMismatch = positionWarning(settingsRepo.getCurrentGrid(), grids),
|
||||
stationGrid = station.grid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMessage() = _uiState.update { it.copy(message = "") }
|
||||
|
||||
+10
-10
@@ -53,7 +53,7 @@ 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.LoTWPositionWarningDialog
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.SharedDialog
|
||||
import com.rtbishop.look4sat.core.presentation.SwipeController
|
||||
@@ -89,7 +89,7 @@ fun LogbookDialog(
|
||||
uploadBusy: Boolean,
|
||||
uploadMessage: String,
|
||||
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview?,
|
||||
gridWarning: com.rtbishop.look4sat.core.domain.repository.LoTWGridWarning? = null,
|
||||
positionWarning: com.rtbishop.look4sat.core.domain.repository.LoTWPositionWarning? = null,
|
||||
onDismiss: () -> Unit,
|
||||
onDelete: (Long) -> Unit,
|
||||
onEdit: (QsoRecord) -> Unit,
|
||||
@@ -97,7 +97,7 @@ fun LogbookDialog(
|
||||
onConfirmUpload: () -> Unit,
|
||||
onDismissPreview: () -> Unit,
|
||||
onDismissMessage: () -> Unit,
|
||||
onIgnoreGridWarning: () -> Unit = {},
|
||||
onIgnorePositionWarning: () -> Unit = {},
|
||||
onFixGrid: (List<String>) -> Unit = {},
|
||||
selectionMode: Boolean = false,
|
||||
selectedIds: Set<Long> = emptySet(),
|
||||
@@ -169,13 +169,13 @@ fun LogbookDialog(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (positionWarning != null) {
|
||||
// Roaming guard: the current position sits outside the station location this batch
|
||||
// would be signed with — confirm the station location before the preview opens.
|
||||
LoTWPositionWarningDialog(
|
||||
warning = positionWarning,
|
||||
onFixStation = { onFixGrid(listOf(positionWarning.currentGrid)) },
|
||||
onIgnore = onIgnorePositionWarning
|
||||
)
|
||||
} else if (preview != null) {
|
||||
LogbookUploadPreviewDialog(
|
||||
|
||||
+2
-2
@@ -292,7 +292,7 @@ private fun SettingsScreen(
|
||||
uploadBusy = uiState.logbookUploadBusy,
|
||||
uploadMessage = uiState.logbookUploadMessage,
|
||||
preview = uiState.logbookPreview,
|
||||
gridWarning = uiState.logbookGridWarning,
|
||||
positionWarning = uiState.logbookPositionWarning,
|
||||
onDismiss = {
|
||||
// Leaving the logbook resets the resubmit selection: re-entering starts
|
||||
// clean — the operator long-presses again to pick records.
|
||||
@@ -305,7 +305,7 @@ private fun SettingsScreen(
|
||||
onConfirmUpload = { onAction(SettingsAction.ConfirmLogbookUpload) },
|
||||
onDismissPreview = { onAction(SettingsAction.DismissLogbookPreview) },
|
||||
onDismissMessage = { onAction(SettingsAction.ClearLogbookMessage) },
|
||||
onIgnoreGridWarning = { onAction(SettingsAction.IgnoreLogbookGridWarning) },
|
||||
onIgnorePositionWarning = { onAction(SettingsAction.IgnoreLogbookPositionWarning) },
|
||||
onFixGrid = { grids ->
|
||||
onAction(SettingsAction.AbandonLogbookForGridFix)
|
||||
onAction(SettingsAction.ExitLogbookSelection)
|
||||
|
||||
+5
-5
@@ -85,9 +85,9 @@ 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,
|
||||
/** Roaming guard: the current position grid is outside the station grids the prepared
|
||||
* batch would be signed with — shown as a dialog before the preview opens. */
|
||||
val logbookPositionWarning: com.rtbishop.look4sat.core.domain.repository.LoTWPositionWarning? = null,
|
||||
val logbookUploadBusy: Boolean = false,
|
||||
/** User-facing upload message shown inside the logbook dialog ("" = none). */
|
||||
val logbookUploadMessage: String = "",
|
||||
@@ -167,8 +167,8 @@ 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 acknowledged the position mismatch and wants to upload anyway. */
|
||||
data object IgnoreLogbookPositionWarning : 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. */
|
||||
|
||||
+17
-6
@@ -34,6 +34,7 @@ import com.rtbishop.look4sat.core.domain.repository.applyLoTWGridResult
|
||||
import com.rtbishop.look4sat.core.domain.repository.lotwCursorApi
|
||||
import com.rtbishop.look4sat.core.domain.repository.lotwCursorEpochMs
|
||||
import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.positionWarning
|
||||
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
||||
@@ -235,7 +236,7 @@ class SettingsViewModel(
|
||||
SettingsAction.PrepareLogbookUpload -> prepareLogbookUpload()
|
||||
SettingsAction.ConfirmLogbookUpload -> confirmLogbookUpload()
|
||||
SettingsAction.DismissLogbookPreview -> dismissLogbookPreview()
|
||||
SettingsAction.IgnoreLogbookGridWarning -> ignoreLogbookGridWarning()
|
||||
SettingsAction.IgnoreLogbookPositionWarning -> ignoreLogbookPositionWarning()
|
||||
SettingsAction.AbandonLogbookForGridFix -> abandonLogbookForGridFix()
|
||||
is SettingsAction.StartLogbookSelection -> startLogbookSelection(action.id)
|
||||
is SettingsAction.ToggleLogbookSelection -> toggleLogbookSelection(action.id)
|
||||
@@ -404,7 +405,13 @@ 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, logbookGridWarning = preview.gridWarning) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
logbookUploadBusy = false,
|
||||
logbookPreview = preview,
|
||||
logbookPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
|
||||
)
|
||||
}
|
||||
} catch (e: com.rtbishop.look4sat.core.domain.repository.LoTWOperationException) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload unavailable: ${e.reason}") }
|
||||
} catch (_: Exception) {
|
||||
@@ -445,14 +452,14 @@ class SettingsViewModel(
|
||||
|
||||
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 "ignore" on the position check: keep the prepared preview. */
|
||||
private fun ignoreLogbookPositionWarning() = _uiState.update { it.copy(logbookPositionWarning = 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) }
|
||||
_uiState.update { it.copy(logbookPositionWarning = null, logbookPreview = null) }
|
||||
}
|
||||
|
||||
/** Long-press entry: selection mode with the pressed record checked. */
|
||||
@@ -496,7 +503,11 @@ class SettingsViewModel(
|
||||
}
|
||||
lastLogbookUploadIds = preview.submittedIds
|
||||
_uiState.update {
|
||||
it.copy(logbookUploadBusy = false, logbookPreview = preview, logbookGridWarning = preview.gridWarning)
|
||||
it.copy(
|
||||
logbookUploadBusy = false,
|
||||
logbookPreview = preview,
|
||||
logbookPositionWarning = positionWarning(settingsRepo.getCurrentGrid(), preview.grids)
|
||||
)
|
||||
}
|
||||
} catch (e: com.rtbishop.look4sat.core.domain.repository.LoTWOperationException) {
|
||||
_uiState.update { it.copy(logbookUploadBusy = false, logbookUploadMessage = "Upload unavailable: ${e.reason}") }
|
||||
|
||||
Reference in new issue
Block a user