mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-10 14:27:22 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
375225f25b | ||
|
|
1a1ed0aaea | ||
|
|
0f34c40b11 | ||
|
|
85c39790ae | ||
|
|
e443ffe85d | ||
|
|
48fa384afd | ||
|
|
bf65d8d8cf |
No files matched your search
@@ -408,6 +408,7 @@ class SettingsRepo(
|
||||
private val keyWavelogSyncCursors = "wavelogSyncCursors"
|
||||
private val keyWavelogSyncUrl = "wavelogSyncUrl"
|
||||
private val keyLastWavelogSyncEpochMs = "wavelogLastSyncEpochMs"
|
||||
private val keyLastWavelogFullSyncEpochMs = "wavelogLastFullSyncEpochMs"
|
||||
|
||||
override fun getWavelogStations(): List<com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo> {
|
||||
val json = preferences.getString(keyWavelogStations, null).orEmpty()
|
||||
@@ -473,6 +474,12 @@ class SettingsRepo(
|
||||
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) =
|
||||
preferences.edit { putLong(keyLastWavelogSyncEpochMs, value) }
|
||||
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long =
|
||||
preferences.getLong(keyLastWavelogFullSyncEpochMs, 0L)
|
||||
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) =
|
||||
preferences.edit { putLong(keyLastWavelogFullSyncEpochMs, value) }
|
||||
//endregion
|
||||
|
||||
//region # Transceivers settings
|
||||
|
||||
@@ -486,6 +486,8 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
||||
override fun setWavelogSyncUrl(url: String) = Unit
|
||||
override fun getLastWavelogSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) = Unit
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) = Unit
|
||||
|
||||
override fun getWorkedGridQsos(): Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>> = emptyMap()
|
||||
|
||||
|
||||
+2
@@ -434,4 +434,6 @@ private class FakeSettingsRepoForSearch : ISettingsRepo {
|
||||
override fun setWavelogSyncUrl(url: String) = Unit
|
||||
override fun getLastWavelogSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) = Unit
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) = Unit
|
||||
}
|
||||
@@ -117,6 +117,8 @@ object AdifCodec {
|
||||
lotwConfirmed = values["LOTW_QSL_RCVD"].equals("Y", true),
|
||||
lotwReceived = values["LOTW_QSL_SENT"].equals("Y", true),
|
||||
lotwQslDate = values["LOTW_QSLRDATE"].orEmpty(),
|
||||
qslConfirmed = values["QSL_RCVD"].equals("Y", true),
|
||||
eqslConfirmed = values["EQSL_QSL_RCVD"].equals("Y", true),
|
||||
dxcc = values["DXCC"]?.toIntOrNull(),
|
||||
country = values["COUNTRY"].orEmpty(),
|
||||
cqZone = values["CQZ"]?.toIntOrNull()?.takeIf { it in 1..40 },
|
||||
|
||||
@@ -48,6 +48,12 @@ data class QsoRecord(
|
||||
val wavelogStation: String = "",
|
||||
val lotwReceived: Boolean = false,
|
||||
val lotwQslDate: String = "",
|
||||
/** Paper QSL received (ADIF QSL_RCVD=Y) — one of the routes Wavelog counts as
|
||||
* "confirmed". Sync-only: the logbook stores no paper-QSL column, so this survives
|
||||
* only inside the record a Wavelog pull produced (the grid rule reads it there). */
|
||||
val qslConfirmed: Boolean = false,
|
||||
/** eQSL confirmation received (ADIF EQSL_QSL_RCVD=Y). Sync-only, see [qslConfirmed]. */
|
||||
val eqslConfirmed: Boolean = false,
|
||||
/** Own operated grid set this QSO went out under — uploaded as ADIF MY_VUCC_GRIDS
|
||||
* (never VUCC_GRIDS, which belongs to the opposite station). Stamped at upload. */
|
||||
val vuccGrids: List<String> = emptyList(),
|
||||
|
||||
+5
@@ -141,6 +141,11 @@ interface ISettingsRepo {
|
||||
/** Epoch ms of the last successful Wavelog sync (0 when never synced). */
|
||||
fun getLastWavelogSyncEpochMs(): Long
|
||||
fun setLastWavelogSyncEpochMs(value: Long)
|
||||
/** Epoch ms of the last FULL Wavelog re-scan (0 when never). An incremental request is
|
||||
* promoted to a full pull once this is stale — Wavelog's pull filters by row id, so a
|
||||
* confirmation arriving for an already-pulled row is invisible to the incremental cursor. */
|
||||
fun getLastWavelogFullSyncEpochMs(): Long
|
||||
fun setLastWavelogFullSyncEpochMs(value: Long)
|
||||
//endregion
|
||||
|
||||
//region # LoTW confirmed-grids settings
|
||||
|
||||
+47
-11
@@ -51,10 +51,39 @@ internal fun isValidGrid4(grid: String): Boolean =
|
||||
grid.length == 4 && grid[0] in 'A'..'R' && grid[1] in 'A'..'R' &&
|
||||
grid[2] in '0'..'9' && grid[3] in '0'..'9'
|
||||
|
||||
/**
|
||||
* "Confirmed" in Wavelog's own sense. Its API's `qsl_filter` maps lotw / qsl / eqsl to
|
||||
* COL_LOTW_QSL_RCVD / COL_QSL_RCVD / COL_EQSL_QSL_RCVD = 'Y', and the ADIF export carries
|
||||
* those as LOTW_QSL_RCVD / QSL_RCVD / EQSL_QSL_RCVD.
|
||||
*/
|
||||
fun QsoRecord.isQslConfirmed(): Boolean = lotwConfirmed || qslConfirmed || eqslConfirmed
|
||||
|
||||
/**
|
||||
* How stale the last full pull may get before an incremental request is promoted to a full
|
||||
* one. Wavelog's pull API filters by row id only, so a confirmation that arrives for a row
|
||||
* pulled earlier (LoTW confirmations land on the same row, keeping its id) can never be seen
|
||||
* by the incremental cursor — the record was already fetched and is not fetched again. A full
|
||||
* pull re-reads every row with its current confirmation flags, which is the only way to pick
|
||||
* such confirmations up; it also rebuilds the stored grid set from scratch, so a square that
|
||||
* only ever existed because an unconfirmed QSO carried a looked-up grid drops out on its own.
|
||||
*/
|
||||
const val WAVELOG_FULL_RESCAN_INTERVAL_MS: Long = 3L * 24 * 60 * 60 * 1000
|
||||
|
||||
fun shouldPromoteWavelogFullSync(
|
||||
lastFullSyncEpochMs: Long,
|
||||
now: Long = System.currentTimeMillis()
|
||||
): Boolean = lastFullSyncEpochMs <= 0L || now - lastFullSyncEpochMs >= WAVELOG_FULL_RESCAN_INTERVAL_MS
|
||||
|
||||
/**
|
||||
* Derives the worked-grid set, the per-grid QSO detail and the roamed (own) grids from
|
||||
* records pulled by a Wavelog sync — mirroring what the LoTW report parser produces, so
|
||||
* both sync sources feed the map's highlight and its 台址 selector the same data shape.
|
||||
*
|
||||
* Only CONFIRMED contacts turn a square green: the LoTW side derives its grids from ARRL's
|
||||
* confirmation report, so a Wavelog record that merely carries the opposite station's grid
|
||||
* (Wavelog fills those in from a callsign lookup) must not count before it is confirmed.
|
||||
* The roamed set is the operator's own operating location, not the opposite station's
|
||||
* confirmation, so it keeps taking every satellite record with a valid own grid.
|
||||
*/
|
||||
fun buildWavelogGridData(records: List<QsoRecord>): WavelogGridData {
|
||||
val grids = linkedSetOf<String>()
|
||||
@@ -73,6 +102,7 @@ fun buildWavelogGridData(records: List<QsoRecord>): WavelogGridData {
|
||||
.filter(::isValidGrid4)
|
||||
.toSet()
|
||||
roamed += myGrids
|
||||
if (!record.isQslConfirmed()) return@forEach
|
||||
val qso = GridQso(
|
||||
call = record.theirCallsign,
|
||||
epochMs = record.startUtcMillis,
|
||||
@@ -100,14 +130,16 @@ fun buildWavelogGridData(records: List<QsoRecord>): WavelogGridData {
|
||||
/**
|
||||
* Applies a successful Wavelog sync to the stored map data and sync bookkeeping.
|
||||
*
|
||||
* Grids, per-grid QSO detail and roamed grids only ever GROW (union): both the
|
||||
* incremental and the full re-pull merge into the stored sets, matching how the LoTW
|
||||
* sync treats stale data — a full re-pull heals missing records but never deletes map
|
||||
* data the app already knows about. Cursors are replaced wholesale ([cursors] covers
|
||||
* every station just fetched), and the sync URL/epoch advance so the next run resumes
|
||||
* incrementally. Persistence runs on the IO dispatcher — building the per-grid QSO
|
||||
* JSON blocks the caller, and for large accounts that froze the main thread after a
|
||||
* manual sync.
|
||||
* An incremental pull MERGES into the stored sets (union: a late confirmation only ever
|
||||
* adds a square). A full pull REPLACES them — same contract as the LoTW full sync — so the
|
||||
* squares left behind by unconfirmed records that used to be counted disappear, and so do
|
||||
* rows the log no longer has. A full pull that lost a station is partial: it degrades to
|
||||
* the merge instead of wiping the squares that station contributed.
|
||||
*
|
||||
* Cursors are replaced wholesale ([cursors] covers every station just fetched), and the
|
||||
* sync URL/epoch advance so the next run resumes incrementally. Persistence runs on the IO
|
||||
* dispatcher — building the per-grid QSO JSON blocks the caller, and for large accounts that
|
||||
* froze the main thread after a manual sync.
|
||||
*
|
||||
* @return the resulting number of worked grids.
|
||||
*/
|
||||
@@ -116,18 +148,22 @@ suspend fun applyWavelogSyncResult(
|
||||
records: List<QsoRecord>,
|
||||
cursors: Map<String, Long>,
|
||||
url: String,
|
||||
mode: WavelogSyncMode,
|
||||
failedStations: Set<String> = emptySet(),
|
||||
now: Long = System.currentTimeMillis()
|
||||
): Int {
|
||||
val data = buildWavelogGridData(records)
|
||||
val replace = mode == WavelogSyncMode.Full && failedStations.isEmpty()
|
||||
val existingGrids = settingsRepo.getWorkedGrids()
|
||||
val existingQsos = settingsRepo.getWorkedGridQsos()
|
||||
val existingRoamed = settingsRepo.getRoamedGrids()
|
||||
val mergedGrids = existingGrids + data.grids
|
||||
val mergedQsos = mergeGridQsos(existingQsos, data.gridQsos)
|
||||
val mergedRoamed = existingRoamed + data.roamedGrids
|
||||
val mergedGrids = if (replace) data.grids else existingGrids + data.grids
|
||||
val mergedQsos = if (replace) data.gridQsos else mergeGridQsos(existingQsos, data.gridQsos)
|
||||
val mergedRoamed = if (replace) data.roamedGrids else existingRoamed + data.roamedGrids
|
||||
settingsRepo.setWavelogSyncCursors(cursors)
|
||||
settingsRepo.setWavelogSyncUrl(normalizeWavelogUrl(url))
|
||||
settingsRepo.setLastWavelogSyncEpochMs(now)
|
||||
if (replace) settingsRepo.setLastWavelogFullSyncEpochMs(now)
|
||||
withContext(Dispatchers.IO) {
|
||||
settingsRepo.setWorkedGrids(mergedGrids)
|
||||
settingsRepo.setWorkedGridQsos(mergedQsos)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.logbook
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.isQslConfirmed
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -124,6 +125,22 @@ class AdifCodecTest {
|
||||
assertTrue(adif.contains("<SAT_NAME:5>SO-50"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeReadsEveryConfirmationRouteWavelogCounts() {
|
||||
// Wavelog's qsl_filter treats lotw / qsl / eqsl alike; the pull must see all three,
|
||||
// and an unconfirmed record must stay unconfirmed.
|
||||
val adi = "<EOH><CALL:6>BG5JVM<QSO_DATE:8>20260916<TIME_ON:4>0745<LOTW_QSL_RCVD:1>Y<EOR>" +
|
||||
"<CALL:6>BG5JVN<QSO_DATE:8>20260916<TIME_ON:4>0746<QSL_RCVD:1>Y<EOR>" +
|
||||
"<CALL:6>BG5JVO<QSO_DATE:8>20260916<TIME_ON:4>0747<EQSL_QSL_RCVD:1>Y<EOR>" +
|
||||
"<CALL:6>BG5JVP<QSO_DATE:8>20260916<TIME_ON:4>0748<EOR>"
|
||||
val records = AdifCodec.decode(adi)
|
||||
assertTrue(records[0].isQslConfirmed())
|
||||
assertTrue(records[1].isQslConfirmed())
|
||||
assertTrue(records[2].isQslConfirmed())
|
||||
assertTrue(!records[3].isQslConfirmed())
|
||||
assertTrue(records[3].lotwConfirmed.not() && records[3].qslConfirmed.not() && records[3].eqslConfirmed.not())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodeKeepsOwnGridsOutOfTheOppositeVuccField() {
|
||||
// The reported defect: an operator's stamped own grid set must never land in
|
||||
|
||||
+2
@@ -284,6 +284,8 @@ class LoTWGridSyncTest {
|
||||
override fun setWavelogSyncUrl(url: String) { wavelogSyncUrl = url }
|
||||
override fun getLastWavelogSyncEpochMs(): Long = lastWavelogSyncEpochMs
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) { lastWavelogSyncEpochMs = value }
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) = Unit
|
||||
|
||||
override fun setSelectedIds(ids: List<Int>) = TODO()
|
||||
override fun setSelectedTypes(types: List<String>) = TODO()
|
||||
|
||||
+107
-4
@@ -35,6 +35,7 @@ import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@@ -124,6 +125,35 @@ class WavelogSyncTest {
|
||||
assertEquals(listOf("EN52"), qso.theirGrids)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gridDataSkipsUnconfirmedContactsButKeepsTheRoamedGrid() {
|
||||
val data = buildWavelogGridData(
|
||||
listOf(
|
||||
record(start = 1_000L, theirGrid = "EN52", myGrid = "OL62", confirmed = false),
|
||||
record(start = 2_000L, theirGrid = "EN53", myGrid = "OL63", confirmed = false)
|
||||
)
|
||||
)
|
||||
// Wavelog fills the opposite station's grid in from a callsign lookup; an unconfirmed
|
||||
// contact must not turn a square green even though the grid is there.
|
||||
assertTrue(data.grids.isEmpty())
|
||||
assertTrue(data.gridQsos.isEmpty())
|
||||
// Where the operator was is not something the opposite station confirms.
|
||||
assertEquals(setOf("OL62", "OL63"), data.roamedGrids)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gridDataAcceptsPaperAndEqslConfirmations() {
|
||||
val data = buildWavelogGridData(
|
||||
listOf(
|
||||
record(start = 1_000L, theirGrid = "EN52", confirmed = false, paper = true),
|
||||
record(start = 2_000L, theirGrid = "EN53", confirmed = false, eqsl = true)
|
||||
)
|
||||
)
|
||||
assertEquals(setOf("EN52", "EN53"), data.grids)
|
||||
assertEquals(1, data.gridQsos.getValue("EN52").size)
|
||||
assertEquals(1, data.gridQsos.getValue("EN53").size)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region # applyWavelogSyncResult
|
||||
@@ -138,6 +168,7 @@ class WavelogSyncTest {
|
||||
listOf(record(start = 1_000L, theirGrid = "EN52", myGrid = "OL62")),
|
||||
mapOf("1" to 500L),
|
||||
"http://a/wavelog/",
|
||||
WavelogSyncMode.Incremental,
|
||||
now = 42L
|
||||
)
|
||||
|
||||
@@ -147,6 +178,7 @@ class WavelogSyncTest {
|
||||
assertEquals(mapOf("1" to 500L), settings.getWavelogSyncCursors())
|
||||
assertEquals("http://a/wavelog", settings.getWavelogSyncUrl())
|
||||
assertEquals(42L, settings.getLastWavelogSyncEpochMs())
|
||||
assertEquals(0L, settings.getLastWavelogFullSyncEpochMs())
|
||||
assertEquals(2, count)
|
||||
}
|
||||
|
||||
@@ -156,14 +188,76 @@ class WavelogSyncTest {
|
||||
settings.setWorkedGrids(setOf("PM01"))
|
||||
settings.setRoamedGrids(setOf("OM60"))
|
||||
|
||||
applyWavelogSyncResult(settings, listOf(record(1_000L, "EN52", myGrid = "OL62")), mapOf("1" to 10L), "http://a")
|
||||
applyWavelogSyncResult(settings, listOf(record(2_000L, "EN53", myGrid = "OL63")), mapOf("1" to 20L), "http://a")
|
||||
applyWavelogSyncResult(
|
||||
settings, listOf(record(1_000L, "EN52", myGrid = "OL62")), mapOf("1" to 10L), "http://a",
|
||||
WavelogSyncMode.Incremental
|
||||
)
|
||||
applyWavelogSyncResult(
|
||||
settings, listOf(record(2_000L, "EN53", myGrid = "OL63")), mapOf("1" to 20L), "http://a",
|
||||
WavelogSyncMode.Incremental
|
||||
)
|
||||
|
||||
assertEquals(setOf("PM01", "EN52", "EN53"), settings.getWorkedGrids())
|
||||
assertEquals(setOf("OM60", "OL62", "OL63"), settings.getRoamedGrids())
|
||||
assertEquals(mapOf("1" to 20L), settings.getWavelogSyncCursors())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullSyncReplacesTheStoredSets() = runBlocking {
|
||||
val settings = FakeSettings()
|
||||
// A square kept from before the confirmation rule existed, plus one that is real now.
|
||||
settings.setWorkedGrids(setOf("PM01", "EN52"))
|
||||
settings.setWorkedGridQsos(mapOf("PM01" to emptyList()))
|
||||
settings.setRoamedGrids(setOf("OM60", "OL62"))
|
||||
|
||||
val count = applyWavelogSyncResult(
|
||||
settings,
|
||||
listOf(record(1_000L, "EN52", myGrid = "OL62")),
|
||||
mapOf("1" to 10L),
|
||||
"http://a",
|
||||
WavelogSyncMode.Full,
|
||||
now = 99L
|
||||
)
|
||||
|
||||
assertEquals(setOf("EN52"), settings.getWorkedGrids())
|
||||
assertEquals(setOf("EN52"), settings.getWorkedGridQsos().keys)
|
||||
assertEquals(setOf("OL62"), settings.getRoamedGrids())
|
||||
assertEquals(99L, settings.getLastWavelogFullSyncEpochMs())
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullSyncWithAFailedStationDegradesToMerge() = runBlocking {
|
||||
val settings = FakeSettings()
|
||||
settings.setWorkedGrids(setOf("PM01"))
|
||||
|
||||
applyWavelogSyncResult(
|
||||
settings,
|
||||
listOf(record(1_000L, "EN52")),
|
||||
mapOf("1" to 10L),
|
||||
"http://a",
|
||||
WavelogSyncMode.Full,
|
||||
failedStations = setOf("2"),
|
||||
now = 99L
|
||||
)
|
||||
|
||||
// Partial pull: never wipe what the station that failed contributed.
|
||||
assertEquals(setOf("PM01", "EN52"), settings.getWorkedGrids())
|
||||
assertEquals(0L, settings.getLastWavelogFullSyncEpochMs())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun incrementalRequestIsPromotedOnceTheLastFullScanIsStale() {
|
||||
val day = 24L * 60 * 60 * 1000
|
||||
assertTrue(shouldPromoteWavelogFullSync(lastFullSyncEpochMs = 0L, now = day))
|
||||
assertTrue(shouldPromoteWavelogFullSync(lastFullSyncEpochMs = 0L, now = 1000L * day))
|
||||
assertEquals(
|
||||
false,
|
||||
shouldPromoteWavelogFullSync(lastFullSyncEpochMs = 10 * day, now = 12 * day)
|
||||
)
|
||||
assertTrue(shouldPromoteWavelogFullSync(lastFullSyncEpochMs = 10 * day, now = 13 * day))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun record(
|
||||
@@ -171,7 +265,10 @@ class WavelogSyncTest {
|
||||
theirGrid: String = "",
|
||||
theirVucc: List<String> = emptyList(),
|
||||
myGrid: String = "OL62TI",
|
||||
sat: String = "SO-50"
|
||||
sat: String = "SO-50",
|
||||
confirmed: Boolean = true,
|
||||
paper: Boolean = false,
|
||||
eqsl: Boolean = false
|
||||
) = QsoRecord(
|
||||
startUtcMillis = start,
|
||||
theirCallsign = "BG5JVM",
|
||||
@@ -186,7 +283,10 @@ class WavelogSyncTest {
|
||||
mode = "FM",
|
||||
satelliteName = sat,
|
||||
propagationMode = if (sat.isNotBlank()) "SAT" else "",
|
||||
status = QsoStatus.COMPLETE
|
||||
status = QsoStatus.COMPLETE,
|
||||
lotwConfirmed = confirmed,
|
||||
qslConfirmed = paper,
|
||||
eqslConfirmed = eqsl
|
||||
)
|
||||
|
||||
private class FakeSettings : ISettingsRepo {
|
||||
@@ -196,6 +296,7 @@ class WavelogSyncTest {
|
||||
private var cursors: Map<String, Long> = emptyMap()
|
||||
private var syncUrl: String = ""
|
||||
private var lastSync: Long = 0L
|
||||
private var lastFullSync: Long = 0L
|
||||
|
||||
override fun getWorkedGrids(): Set<String> = workedGrids
|
||||
override fun setWorkedGrids(grids: Set<String>) { workedGrids = grids }
|
||||
@@ -209,6 +310,8 @@ class WavelogSyncTest {
|
||||
override fun setWavelogSyncUrl(url: String) { syncUrl = url }
|
||||
override fun getLastWavelogSyncEpochMs(): Long = lastSync
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) { lastSync = value }
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long = lastFullSync
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) { lastFullSync = value }
|
||||
|
||||
override val appVersionName: String get() = TODO()
|
||||
override val selectedIds: StateFlow<List<Int>> get() = TODO()
|
||||
|
||||
@@ -92,6 +92,10 @@
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">后退</string>
|
||||
<string name="radar_use_large">使用大雷达</string>
|
||||
<string name="radar_use_small">使用小雷达</string>
|
||||
<string name="radar_size_large_short">大</string>
|
||||
<string name="radar_size_small_short">小</string>
|
||||
<string name="radar_notify">通知</string>
|
||||
<string name="radar_az_text">方位角</string>
|
||||
<string name="radar_el_text">仰角</string>
|
||||
|
||||
@@ -82,6 +82,10 @@
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">后退</string>
|
||||
<string name="radar_use_large">使用大雷达</string>
|
||||
<string name="radar_use_small">使用小雷达</string>
|
||||
<string name="radar_size_large_short">大</string>
|
||||
<string name="radar_size_small_short">小</string>
|
||||
<string name="radar_notify">通知</string>
|
||||
<string name="radar_az_text">方位角</string>
|
||||
<string name="radar_el_text">仰角</string>
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Back</string>
|
||||
<string name="radar_use_large">Use large radar</string>
|
||||
<string name="radar_use_small">Use small radar</string>
|
||||
<string name="radar_size_large_short">L</string>
|
||||
<string name="radar_size_small_short">S</string>
|
||||
<string name="radar_notify">Notify</string>
|
||||
<string name="radar_az_text">Azimuth</string>
|
||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
||||
|
||||
@@ -159,4 +159,6 @@ class FakeSettingsRepo(
|
||||
override fun setWavelogSyncUrl(url: String) = Unit
|
||||
override fun getLastWavelogSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) = Unit
|
||||
override fun getLastWavelogFullSyncEpochMs(): Long = 0L
|
||||
override fun setLastWavelogFullSyncEpochMs(value: Long) = Unit
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
@@ -62,6 +63,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.keepScreenOn
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -284,6 +287,7 @@ private fun RadarScreen(
|
||||
// row, the radar keeps the largest square it can, the pager yields (reserved block when
|
||||
// the window is tall enough, overlaid tab strip + panel when it is not).
|
||||
val fillRadar = useFillRadarLayout(isVertical, maxWidth, maxHeight)
|
||||
var useLargeRadar by remember(fillRadar) { mutableStateOf(fillRadar) }
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
@@ -297,7 +301,15 @@ private fun RadarScreen(
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
|
||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||
}
|
||||
TopBar { NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc) }
|
||||
TopBar {
|
||||
NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc)
|
||||
if (fillRadar) {
|
||||
RadarSizeToggle(
|
||||
showingLargeRadar = useLargeRadar,
|
||||
onToggle = { useLargeRadar = !useLargeRadar }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TopBar {
|
||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||
@@ -307,7 +319,7 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
if (fillRadar) {
|
||||
if (fillRadar && useLargeRadar) {
|
||||
RadarFillArea(
|
||||
uiState = uiState,
|
||||
trackB = trackB,
|
||||
@@ -332,6 +344,32 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadarSizeToggle(showingLargeRadar: Boolean, onToggle: () -> Unit) {
|
||||
val label = stringResource(
|
||||
if (showingLargeRadar) R.string.radar_use_small else R.string.radar_use_large
|
||||
)
|
||||
ElevatedCard(
|
||||
modifier = Modifier.size(48.dp),
|
||||
onClick = onToggle
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.semantics { contentDescription = label },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (showingLargeRadar) R.string.radar_size_small_short
|
||||
else R.string.radar_size_large_short
|
||||
),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Radar area for split-screen / small windows. The radar takes the largest square the area
|
||||
* can hold so the circle spans the same share of the width it does full screen, and the
|
||||
|
||||
+23
-5
@@ -41,6 +41,7 @@ import com.rtbishop.look4sat.core.domain.repository.WavelogSyncFetch
|
||||
import com.rtbishop.look4sat.core.domain.repository.WavelogSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.applyWavelogSyncResult
|
||||
import com.rtbishop.look4sat.core.domain.repository.resolveWavelogSyncMode
|
||||
import com.rtbishop.look4sat.core.domain.repository.shouldPromoteWavelogFullSync
|
||||
import com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadOutcome
|
||||
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview
|
||||
@@ -390,6 +391,11 @@ class SettingsViewModel(
|
||||
return
|
||||
}
|
||||
val mode = resolveWavelogSyncMode(settingsRepo.getWavelogSyncUrl(), settings.url, requested)
|
||||
// Late confirmations can only be picked up by a full pull (Wavelog's incremental
|
||||
// cursor is a row id), so promote a stale incremental request to a full re-scan.
|
||||
val promoted = mode == WavelogSyncMode.Incremental &&
|
||||
shouldPromoteWavelogFullSync(settingsRepo.getLastWavelogFullSyncEpochMs())
|
||||
val actualMode = if (promoted) WavelogSyncMode.Full else mode
|
||||
_uiState.update { it.copy(wavelogSyncing = true, wavelogUploadMessage = null) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
@@ -398,7 +404,7 @@ class SettingsViewModel(
|
||||
settings.apiKey,
|
||||
stations,
|
||||
settingsRepo.getWavelogSyncCursors(),
|
||||
full = mode == WavelogSyncMode.Full
|
||||
full = actualMode == WavelogSyncMode.Full
|
||||
)
|
||||
if (fetch == null) {
|
||||
_uiState.update {
|
||||
@@ -413,13 +419,20 @@ class SettingsViewModel(
|
||||
}
|
||||
// Stations that failed keep their previous cursor; fetched ones advance.
|
||||
val cursors = settingsRepo.getWavelogSyncCursors() + fetch.cursors
|
||||
applyWavelogSyncResult(settingsRepo, fetch.records, cursors, settings.url)
|
||||
applyWavelogSyncResult(
|
||||
settingsRepo,
|
||||
fetch.records,
|
||||
cursors,
|
||||
settings.url,
|
||||
actualMode,
|
||||
failedStations = fetch.failedStations.toSet()
|
||||
)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
wavelogSyncing = false,
|
||||
workedGridsCount = settingsRepo.getWorkedGrids().size,
|
||||
wavelogLastSyncEpochMs = settingsRepo.getLastWavelogSyncEpochMs(),
|
||||
wavelogUploadMessage = wavelogSyncSummary(mode, fetch, merge, stations.size)
|
||||
wavelogUploadMessage = wavelogSyncSummary(actualMode, fetch, merge, stations.size, promoted)
|
||||
)
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
@@ -436,9 +449,14 @@ class SettingsViewModel(
|
||||
mode: WavelogSyncMode,
|
||||
fetch: WavelogSyncFetch,
|
||||
merge: com.rtbishop.look4sat.core.domain.logbook.AdifImportResult,
|
||||
totalStations: Int
|
||||
totalStations: Int,
|
||||
promoted: Boolean = false
|
||||
): String {
|
||||
val head = if (mode == WavelogSyncMode.Full) "Full Wavelog sync" else "Incremental Wavelog sync"
|
||||
val head = when {
|
||||
promoted -> "Full Wavelog sync (auto re-scan)"
|
||||
mode == WavelogSyncMode.Full -> "Full Wavelog sync"
|
||||
else -> "Incremental Wavelog sync"
|
||||
}
|
||||
val base = "$head — ${merge.imported} new, ${merge.skipped} already there"
|
||||
return if (fetch.failedStations.isEmpty()) base
|
||||
else "$base · ${fetch.failedStations.size}/$totalStations station(s) failed"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "583"
|
||||
appVersionCode = "585"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.8-ba7opf.11.8"
|
||||
appVersionName = "4.4.8-ba7opf.11.10"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user