Compare commits

...
Author SHA1 Message Date
atsunatsu 375225f25b build: release version 4.4.8-ba7opf.11.10 (585) 2026-10-10 21:03:24 +08:00
atsunatsu 1a1ed0aaea Merge pull request #7 from Iu-yang1/feature/radar-window-toggle-upstream-pr
feat(radar): toggle radar size in small windows (+ missing layout.size import fix)
2026-10-10 20:52:41 +08:00
atsunatsu 0f34c40b11 fix(radar): add missing layout.size import for the radar size toggle
PR #7 calls Modifier.size(48.dp) in RadarScreen.kt but never imported androidx.compose.foundation.layout.size, so :feature:radar:compileDebugKotlin failed with 'Unresolved reference: size' (verified by running the build).
2026-10-10 20:51:38 +08:00
Iu_yang1 85c39790ae fix(radar): match current split-window sizing behavior 2026-10-10 20:30:29 +08:00
Iu_yang1 e443ffe85d feat(radar): toggle radar size in small windows 2026-10-10 20:26:10 +08:00
atsunatsu 48fa384afd build: release version 4.4.8-ba7opf.11.9 (584) 2026-10-10 18:42:58 +08:00
atsunatsu bf65d8d8cf fix(wavelog): 只把已确认通联算进网格(未确认但有网格的不算)
Wavelog 拉取原先把所有带对方网格的卫星通联都算成方格,包括未确认的通联(网格只是
Wavelog 按呼号查出来的)。而 LoTW 侧网格取自 ARRL 确认报告,两侧口径不一致。
现在要求确认:LOTW_QSL_RCVD / QSL_RCVD / EQSL_QSL_RCVD 任一为 Y(= Wavelog 自己
qsl_filter 的三条途径),AdifCodec 补解析后两个字段;roamed 台址不受影响。
存量清理:全量同步改为替换(与 LoTW 全量同口径,台站失败时降级为并入);迟到确认
因 Wavelog 只能按行 id 增量拉取而不可见,增量请求在上次全量重扫超过 3 天后自动升级
为全量。新增 WavelogSyncTest 5 例与 AdifCodecTest 1 例。
2026-10-10 18:42:53 +08:00
17 changed files with 276 additions and 24 deletions

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()
@@ -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(),
@@ -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
@@ -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
@@ -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()
@@ -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
@@ -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"
+2 -2
View File
@@ -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