From 4aada90b764727b8c69e1b47df5dc0a3f6520e85 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 10 Aug 2026 16:43:26 +0800 Subject: [PATCH] feat: remember per-satellite doppler offset in calculator Store the last entered offset (kHz) per satellite in SharedPreferences via SettingsRepo, and restore it when the linear transponder calculator is shown for that satellite. Follows the project's MVI/clean architecture: - ISettingsRepo: getSatelliteOffset / setSatelliteOffset - SettingsRepo: backed by SharedPreferences - RadarState: calculatorOffsetKHz field - RadarAction: ChangeCalculatorOffset - RadarViewModel: loads offset on SelectTransmitter, saves on change - CalculatorPage/DopplerFrequencyCalculator: receive offset via params instead of accessing SharedPreferences directly from the UI layer. --- .../core/data/repository/SettingsRepo.kt | 25 ++++++++++++++++++- .../core/data/repository/DatabaseRepoTest.kt | 4 +++ .../core/data/repository/SelectionRepoTest.kt | 4 +++ .../core/domain/repository/ISettingsRepo.kt | 5 ++++ .../look4sat/feature/radar/RadarScreen.kt | 3 ++- .../look4sat/feature/radar/RadarState.kt | 6 ++++- .../look4sat/feature/radar/RadarViewModel.kt | 21 +++++++++++++++- .../feature/radar/TransceiversPage.kt | 8 ++++-- 8 files changed, 70 insertions(+), 6 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 283142ac..5abc6d63 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -37,6 +37,7 @@ import com.rtbishop.look4sat.core.domain.utility.round import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import org.json.JSONObject class SettingsRepo( private val locationManager: LocationManager, @@ -417,5 +418,27 @@ class SettingsRepo( baudRate = preferences.getInt(keyRadioBaudRate, 4800), splitMode = preferences.getBoolean(keyRadioSplitMode, false) ) - //endregion + + private val keySatelliteOffsets = "satelliteOffsets" + + override fun getSatelliteOffset(catnum: Int): String { + val json = preferences.getString(keySatelliteOffsets, "{}") ?: "{}" + return try { + JSONObject(json).optString(catnum.toString(), "") + } catch (e: Exception) { + "" + } + } + + override fun setSatelliteOffset(catnum: Int, offset: String) { + val json = preferences.getString(keySatelliteOffsets, "{}") ?: "{}" + val updated = try { + val obj = JSONObject(json) + if (offset.isEmpty()) obj.remove(catnum.toString()) else obj.put(catnum.toString(), offset) + obj.toString() + } catch (e: Exception) { + """{"$catnum": "$offset"}""" + } + preferences.edit { putString(keySatelliteOffsets, updated) } + } } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt index a57c0ac8..e8b051fe 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -213,6 +213,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou } override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit + + override fun getSatelliteOffset(catnum: Int): String = "" + + override fun setSatelliteOffset(catnum: Int, offset: String) = Unit } private fun defaultDataSourcesSettings(): DataSourcesSettings { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoTest.kt index 9a240b46..8bb71983 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoTest.kt @@ -167,6 +167,10 @@ class SelectionRepoTest { override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit + + override fun getSatelliteOffset(catnum: Int): String = "" + + override fun setSatelliteOffset(catnum: Int, offset: String) = Unit } } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index 96d0283d..b1b11733 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -75,4 +75,9 @@ interface ISettingsRepo { val radioControlSettings: StateFlow fun updateRadioControlSettings(settings: RadioControlSettings) //endregion + + //region # Per-satellite calculator offset settings + fun getSatelliteOffset(catnum: Int): String + fun setSatelliteOffset(catnum: Int, offset: String) + //endregion } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index b94227ca..53b47670 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -198,7 +198,8 @@ private fun PagerCard( transceivers = uiState.transceivers.transmitters, selectedUuid = uiState.transceivers.selectedUuid, orbitalPos = uiState.orbitalPos, - onAction = onAction + onAction = onAction, + calculatorOffsetKHz = uiState.calculatorOffsetKHz ) RadarPage.Sstv -> SstvPage( sstv = uiState.sstv, diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt index 78aaadd9..0383b85d 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt @@ -63,7 +63,8 @@ data class RadarState( val moonPosition: CelestialComputer.MoonPosition? = null, val transceivers: TransceiverSubState = TransceiverSubState(), val radioControl: RadioControlSubState = RadioControlSubState(), - val sstv: SstvSubState = SstvSubState() + val sstv: SstvSubState = SstvSubState(), + val calculatorOffsetKHz: String = "" ) enum class SstvStatus { Idle, Recording } @@ -97,4 +98,7 @@ sealed interface RadarAction { data object SstvReset : RadarAction data class SstvSelectMode(val modeName: String) : RadarAction data class SstvPermissionResult(val granted: Boolean) : RadarAction + + // Calculator actions + data class ChangeCalculatorOffset(val offsetKHz: String) : RadarAction } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 6ddd57d2..55e4ec45 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -260,7 +260,17 @@ class RadarViewModel( // Compute toggle state before the update so we don't read post-update value val isTogglingOff = _uiState.value.transceivers.selectedUuid == action.uuid val newUuid = if (isTogglingOff) null else action.uuid - _uiState.update { it.copy(transceivers = it.transceivers.copy(selectedUuid = newUuid)) } + // Load the saved offset for the newly selected satellite + val offsetKHz = if (!isTogglingOff) { + transponders.find { it.uuid == action.uuid } + ?.catnum?.let { settingsRepo.getSatelliteOffset(it) } ?: "" + } else "" + _uiState.update { + it.copy( + transceivers = it.transceivers.copy(selectedUuid = newUuid), + calculatorOffsetKHz = offsetKHz + ) + } // Only update the tracking service when selecting a different transponder to // avoid resetting a user-adjusted TX base on re-expand if (!isTogglingOff) { @@ -309,6 +319,15 @@ class RadarViewModel( sstvDecoder?.clearPixels() _uiState.update { it.copy(sstv = it.sstv.copy(currentFrame = null)) } } + is RadarAction.ChangeCalculatorOffset -> { + val catnum = _uiState.value.transceivers.selectedUuid?.let { uuid -> + transponders.find { it.uuid == uuid }?.catnum + } + if (catnum != null) { + settingsRepo.setSatelliteOffset(catnum, action.offsetKHz) + } + _uiState.update { it.copy(calculatorOffsetKHz = action.offsetKHz) } + } } } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt index cea8114a..81969568 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/TransceiversPage.kt @@ -128,6 +128,7 @@ fun CalculatorPage( selectedUuid: String?, orbitalPos: OrbitalPos?, onAction: (RadarAction) -> Unit, + calculatorOffsetKHz: String = "", modifier: Modifier = Modifier ) { val calculatorTransceivers = remember(transceivers) { @@ -180,6 +181,8 @@ fun CalculatorPage( DopplerFrequencyCalculator( transponder = selectedTransceiver, orbitalPos = orbitalPos, + offsetKHz = calculatorOffsetKHz, + onOffsetChange = { onAction(RadarAction.ChangeCalculatorOffset(it)) }, modifier = Modifier.fillMaxWidth() ) } @@ -521,6 +524,8 @@ private enum class EditedField { TX, PASSBAND, RX } private fun DopplerFrequencyCalculator( transponder: SatRadio, orbitalPos: OrbitalPos?, + offsetKHz: String = "", + onOffsetChange: (String) -> Unit = {}, modifier: Modifier = Modifier ) { if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return @@ -529,7 +534,6 @@ private fun DopplerFrequencyCalculator( var txFrequencyHz by remember { mutableStateOf(0L) } var rxFrequencyHz by remember { mutableStateOf(0L) } var passbandPosition by remember { mutableStateOf(0.5f) } - var offsetKHz by remember { mutableStateOf("") } var stepSizeKHz by remember { mutableIntStateOf(1) } val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L @@ -712,7 +716,7 @@ private fun DopplerFrequencyCalculator( ) { BasicTextField( value = offsetKHz, - onValueChange = { offsetKHz = it }, + onValueChange = onOffsetChange, singleLine = true, textStyle = TextStyle( fontSize = 16.sp,