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.
This commit is contained in:
atsunatsu committed 2026-08-10 17:03:45 +08:00
1 parent 8b5960282f
commit 4aada90b76
8 files changed
+70 -6

No files matched your search

@@ -37,6 +37,7 @@ import com.rtbishop.look4sat.core.domain.utility.round
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.json.JSONObject
class SettingsRepo( class SettingsRepo(
private val locationManager: LocationManager, private val locationManager: LocationManager,
@@ -417,5 +418,27 @@ class SettingsRepo(
baudRate = preferences.getInt(keyRadioBaudRate, 4800), baudRate = preferences.getInt(keyRadioBaudRate, 4800),
splitMode = preferences.getBoolean(keyRadioSplitMode, false) 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) }
}
} }
@@ -213,6 +213,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
} }
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
override fun getSatelliteOffset(catnum: Int): String = ""
override fun setSatelliteOffset(catnum: Int, offset: String) = Unit
} }
private fun defaultDataSourcesSettings(): DataSourcesSettings { private fun defaultDataSourcesSettings(): DataSourcesSettings {
@@ -167,6 +167,10 @@ class SelectionRepoTest {
override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
override fun getSatelliteOffset(catnum: Int): String = ""
override fun setSatelliteOffset(catnum: Int, offset: String) = Unit
} }
} }
@@ -75,4 +75,9 @@ interface ISettingsRepo {
val radioControlSettings: StateFlow<RadioControlSettings> val radioControlSettings: StateFlow<RadioControlSettings>
fun updateRadioControlSettings(settings: RadioControlSettings) fun updateRadioControlSettings(settings: RadioControlSettings)
//endregion //endregion
//region # Per-satellite calculator offset settings
fun getSatelliteOffset(catnum: Int): String
fun setSatelliteOffset(catnum: Int, offset: String)
//endregion
} }
@@ -198,7 +198,8 @@ private fun PagerCard(
transceivers = uiState.transceivers.transmitters, transceivers = uiState.transceivers.transmitters,
selectedUuid = uiState.transceivers.selectedUuid, selectedUuid = uiState.transceivers.selectedUuid,
orbitalPos = uiState.orbitalPos, orbitalPos = uiState.orbitalPos,
onAction = onAction onAction = onAction,
calculatorOffsetKHz = uiState.calculatorOffsetKHz
) )
RadarPage.Sstv -> SstvPage( RadarPage.Sstv -> SstvPage(
sstv = uiState.sstv, sstv = uiState.sstv,
@@ -63,7 +63,8 @@ data class RadarState(
val moonPosition: CelestialComputer.MoonPosition? = null, val moonPosition: CelestialComputer.MoonPosition? = null,
val transceivers: TransceiverSubState = TransceiverSubState(), val transceivers: TransceiverSubState = TransceiverSubState(),
val radioControl: RadioControlSubState = RadioControlSubState(), val radioControl: RadioControlSubState = RadioControlSubState(),
val sstv: SstvSubState = SstvSubState() val sstv: SstvSubState = SstvSubState(),
val calculatorOffsetKHz: String = ""
) )
enum class SstvStatus { Idle, Recording } enum class SstvStatus { Idle, Recording }
@@ -97,4 +98,7 @@ sealed interface RadarAction {
data object SstvReset : RadarAction data object SstvReset : RadarAction
data class SstvSelectMode(val modeName: String) : RadarAction data class SstvSelectMode(val modeName: String) : RadarAction
data class SstvPermissionResult(val granted: Boolean) : RadarAction data class SstvPermissionResult(val granted: Boolean) : RadarAction
// Calculator actions
data class ChangeCalculatorOffset(val offsetKHz: String) : RadarAction
} }
@@ -260,7 +260,17 @@ class RadarViewModel(
// Compute toggle state before the update so we don't read post-update value // Compute toggle state before the update so we don't read post-update value
val isTogglingOff = _uiState.value.transceivers.selectedUuid == action.uuid val isTogglingOff = _uiState.value.transceivers.selectedUuid == action.uuid
val newUuid = if (isTogglingOff) null else 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 // Only update the tracking service when selecting a different transponder to
// avoid resetting a user-adjusted TX base on re-expand // avoid resetting a user-adjusted TX base on re-expand
if (!isTogglingOff) { if (!isTogglingOff) {
@@ -309,6 +319,15 @@ class RadarViewModel(
sstvDecoder?.clearPixels() sstvDecoder?.clearPixels()
_uiState.update { it.copy(sstv = it.sstv.copy(currentFrame = null)) } _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) }
}
} }
} }
@@ -128,6 +128,7 @@ fun CalculatorPage(
selectedUuid: String?, selectedUuid: String?,
orbitalPos: OrbitalPos?, orbitalPos: OrbitalPos?,
onAction: (RadarAction) -> Unit, onAction: (RadarAction) -> Unit,
calculatorOffsetKHz: String = "",
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val calculatorTransceivers = remember(transceivers) { val calculatorTransceivers = remember(transceivers) {
@@ -180,6 +181,8 @@ fun CalculatorPage(
DopplerFrequencyCalculator( DopplerFrequencyCalculator(
transponder = selectedTransceiver, transponder = selectedTransceiver,
orbitalPos = orbitalPos, orbitalPos = orbitalPos,
offsetKHz = calculatorOffsetKHz,
onOffsetChange = { onAction(RadarAction.ChangeCalculatorOffset(it)) },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
@@ -521,6 +524,8 @@ private enum class EditedField { TX, PASSBAND, RX }
private fun DopplerFrequencyCalculator( private fun DopplerFrequencyCalculator(
transponder: SatRadio, transponder: SatRadio,
orbitalPos: OrbitalPos?, orbitalPos: OrbitalPos?,
offsetKHz: String = "",
onOffsetChange: (String) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return
@@ -529,7 +534,6 @@ private fun DopplerFrequencyCalculator(
var txFrequencyHz by remember { mutableStateOf(0L) } var txFrequencyHz by remember { mutableStateOf(0L) }
var rxFrequencyHz by remember { mutableStateOf(0L) } var rxFrequencyHz by remember { mutableStateOf(0L) }
var passbandPosition by remember { mutableStateOf(0.5f) } var passbandPosition by remember { mutableStateOf(0.5f) }
var offsetKHz by remember { mutableStateOf("") }
var stepSizeKHz by remember { mutableIntStateOf(1) } var stepSizeKHz by remember { mutableIntStateOf(1) }
val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L
@@ -712,7 +716,7 @@ private fun DopplerFrequencyCalculator(
) { ) {
BasicTextField( BasicTextField(
value = offsetKHz, value = offsetKHz,
onValueChange = { offsetKHz = it }, onValueChange = onOffsetChange,
singleLine = true, singleLine = true,
textStyle = TextStyle( textStyle = TextStyle(
fontSize = 16.sp, fontSize = 16.sp,