mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added several tweaks to Settings and RadioControl screens
This commit is contained in:
1 parent
4a3a01729b
commit
a94f95874b
14 files changed
+514
-505
No files matched your search
@@ -33,11 +33,11 @@ data class MapState(
|
||||
val positions: Map<OrbitalObject, GeoPos>? = null
|
||||
)
|
||||
|
||||
sealed class MapAction {
|
||||
data object SelectPrev : MapAction()
|
||||
data object SelectNext : MapAction()
|
||||
data class SelectItem(val item: OrbitalObject) : MapAction()
|
||||
data class SelectDefaultItem(val catnum: Int) : MapAction()
|
||||
sealed interface MapAction {
|
||||
data object SelectPrev : MapAction
|
||||
data object SelectNext : MapAction
|
||||
data class SelectItem(val item: OrbitalObject) : MapAction
|
||||
data class SelectDefaultItem(val catnum: Int) : MapAction
|
||||
}
|
||||
|
||||
data class MapData(
|
||||
|
||||
@@ -60,6 +60,8 @@ private val allModes = listOf(
|
||||
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
|
||||
)
|
||||
|
||||
private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PassesDialogPreview() {
|
||||
@@ -68,26 +70,27 @@ private fun PassesDialogPreview() {
|
||||
|
||||
@Composable
|
||||
internal fun PassesDialog(hours: Int, elevation: Double, cancel: () -> Unit, accept: (Int, Double) -> Unit) {
|
||||
val hoursValue = remember { mutableIntStateOf(hours) }
|
||||
val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) }
|
||||
val elevationValueNew = remember { mutableDoubleStateOf(elevation) }
|
||||
val onAccept = {
|
||||
accept(hoursValue.intValue, elevationValueNew.doubleValue).also { cancel() }
|
||||
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue).also { cancel() }
|
||||
}
|
||||
SharedDialog(title = stringResource(R.string.pass_filter_title), onCancel = cancel, onAccept = onAccept) {
|
||||
SliderRow(
|
||||
title = stringResource(R.string.pass_filter_elev),
|
||||
value = elevationValueNew.doubleValue,
|
||||
valuePostfix = "°",
|
||||
displayValue = "${elevationValueNew.doubleValue.toInt()}°",
|
||||
valueResId = R.drawable.ic_elevation,
|
||||
valueRange = 0f..60f
|
||||
) { elevationValueNew.doubleValue = it.toDouble() }
|
||||
SliderRow(
|
||||
title = stringResource(R.string.pass_filter_hours),
|
||||
value = hoursValue.intValue.toDouble(),
|
||||
valuePostfix = "h",
|
||||
value = hoursIndex.intValue.toDouble(),
|
||||
displayValue = "${hourSteps[hoursIndex.intValue]}h",
|
||||
valueResId = R.drawable.ic_clock,
|
||||
valueRange = 1f..240f
|
||||
) { hoursValue.intValue = it.toInt() }
|
||||
valueRange = 0f..(hourSteps.size - 1).toFloat(),
|
||||
steps = hourSteps.size - 2
|
||||
) { hoursIndex.intValue = it.toInt().coerceIn(0, hourSteps.size - 1) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +98,10 @@ internal fun PassesDialog(hours: Int, elevation: Double, cancel: () -> Unit, acc
|
||||
private fun SliderRow(
|
||||
title: String,
|
||||
value: Double,
|
||||
valuePostfix: String,
|
||||
displayValue: String,
|
||||
valueResId: Int,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
steps: Int = 0,
|
||||
onChange: (Float) -> Unit
|
||||
) {
|
||||
Column(
|
||||
@@ -122,13 +126,13 @@ private fun SliderRow(
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${value.toInt()}$valuePostfix",
|
||||
text = displayValue,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange)
|
||||
Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange, steps = steps)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ data class PassesState(
|
||||
val shouldSeeWhatsNew: Boolean = false
|
||||
)
|
||||
|
||||
sealed class PassesAction {
|
||||
data object DismissWhatsNew : PassesAction()
|
||||
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction()
|
||||
data class FilterRadios(val modes: List<String>) : PassesAction()
|
||||
data object RefreshPasses : PassesAction()
|
||||
data object TogglePassesDialog : PassesAction()
|
||||
data object ToggleRadiosDialog : PassesAction()
|
||||
sealed interface PassesAction {
|
||||
data object DismissWhatsNew : PassesAction
|
||||
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction
|
||||
data class FilterRadios(val modes: List<String>) : PassesAction
|
||||
data object RefreshPasses : PassesAction
|
||||
data object TogglePassesDialog : PassesAction
|
||||
data object ToggleRadiosDialog : PassesAction
|
||||
}
|
||||
+14
-6
@@ -34,6 +34,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -57,15 +58,22 @@ class PassesViewModel(
|
||||
val uiState: StateFlow<PassesState> = _uiState
|
||||
|
||||
init {
|
||||
// React to raw pass list changes — stops the refreshing indicator on first emission
|
||||
// Show refreshing indicator whenever the selected satellites list changes
|
||||
viewModelScope.launch {
|
||||
var initialLoadDone = false
|
||||
satelliteRepo.passes.collectLatest { _ ->
|
||||
if (!initialLoadDone) {
|
||||
initialLoadDone = true
|
||||
delay(1000) // Artificial delay to show the refreshing state on first load
|
||||
settingsRepo.selectedIds.collectLatest { selectedIds ->
|
||||
_uiState.update { it.copy(isRefreshing = true) }
|
||||
if (selectedIds.isEmpty()) {
|
||||
// No satellites selected — show indicator briefly, then stop
|
||||
delay(1000)
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
// For non-empty selections, the passes collector below will clear isRefreshing
|
||||
}
|
||||
}
|
||||
// Stop refreshing whenever new passes arrive (from initial load, selection change, or filter)
|
||||
viewModelScope.launch {
|
||||
satelliteRepo.passes.drop(1).collect { _ ->
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
}
|
||||
// Local tick loop — computes pass progress and countdown timer every second
|
||||
|
||||
@@ -36,7 +36,7 @@ data class RadarState(
|
||||
val selectedFrequency: Long? = null
|
||||
)
|
||||
|
||||
sealed class RadarAction {
|
||||
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction()
|
||||
data class SelectTransmitter(val uuid: String) : RadarAction()
|
||||
sealed interface RadarAction {
|
||||
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction
|
||||
data class SelectTransmitter(val uuid: String) : RadarAction
|
||||
}
|
||||
@@ -174,6 +174,10 @@ class RadarViewModel(
|
||||
settingsRepo.rcSettings.value.frequencyState || settingsRepo.rcSettings.value.bluetoothFrequencyState
|
||||
_uiState.update { state ->
|
||||
if (!isFreqEnabled) {
|
||||
// Skip update if nothing changed
|
||||
if (state.transmitters == transmitters && state.selectedTransmitterUuid == null && state.selectedFrequency == null) {
|
||||
return@update state
|
||||
}
|
||||
return@update state.copy(
|
||||
transmitters = transmitters,
|
||||
selectedTransmitterUuid = null,
|
||||
@@ -191,6 +195,10 @@ class RadarViewModel(
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
// Skip update if nothing changed
|
||||
if (state.transmitters == transmitters && state.selectedTransmitterUuid == selectedUuid && state.selectedFrequency == freq) {
|
||||
return@update state
|
||||
}
|
||||
state.copy(transmitters = transmitters, selectedTransmitterUuid = selectedUuid, selectedFrequency = freq)
|
||||
}
|
||||
}
|
||||
|
||||
+82
-64
@@ -76,12 +76,16 @@ fun NavGraphBuilder.radioControlDestination(navigateUp: () -> Unit) {
|
||||
composable(route, args) {
|
||||
val viewModel = viewModel(RadioControlViewModel::class.java, factory = RadioControlViewModel.Factory)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
RadioControlScreen(uiState, navigateUp)
|
||||
RadioControlScreen(uiState, viewModel::onAction, navigateUp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Unit) {
|
||||
private fun RadioControlScreen(
|
||||
uiState: RadioControlState,
|
||||
onAction: (RadioControlAction) -> Unit,
|
||||
navigateUp: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.layoutPadding()
|
||||
@@ -115,21 +119,39 @@ private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Uni
|
||||
distance = uiState.distance
|
||||
)
|
||||
|
||||
val selectedTransponder = uiState.transponders.find {
|
||||
it.uuid == uiState.selectedTransponderUuid
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
item { TransponderSelector(uiState) }
|
||||
|
||||
val selectedTransponder = uiState.transponders.find {
|
||||
it.uuid == uiState.selectedTransponderUuid
|
||||
item {
|
||||
TransponderSelector(
|
||||
transponders = uiState.transponders,
|
||||
selectedUuid = uiState.selectedTransponderUuid,
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedTransponder?.uplinkMode?.uppercase() == "FM") {
|
||||
item { CtcssSelector(uiState) }
|
||||
item {
|
||||
CtcssSelector(
|
||||
ctcssTone = uiState.ctcssTone,
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.txBaseFrequencyHz != null) {
|
||||
item { FrequencyTuner(uiState) }
|
||||
item {
|
||||
FrequencyTuner(
|
||||
txBaseFrequencyHz = uiState.txBaseFrequencyHz,
|
||||
selectedTransponder = selectedTransponder,
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
uiState.errorMessage?.let { msg ->
|
||||
@@ -143,7 +165,11 @@ private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Uni
|
||||
}
|
||||
}
|
||||
|
||||
ControlButtons(uiState)
|
||||
ControlButtons(
|
||||
isConnected = uiState.txPanel.isConnected || uiState.rxPanel.isConnected,
|
||||
isTracking = uiState.isTracking,
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +234,12 @@ private fun PositionRow(azimuth: String, elevation: String, distance: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransponderSelector(uiState: RadioControlState) {
|
||||
if (uiState.transponders.isEmpty()) {
|
||||
private fun TransponderSelector(
|
||||
transponders: List<SatRadio>,
|
||||
selectedUuid: String?,
|
||||
onAction: (RadioControlAction) -> Unit
|
||||
) {
|
||||
if (transponders.isEmpty()) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "No transponders with uplink+downlink available",
|
||||
@@ -222,11 +252,11 @@ private fun TransponderSelector(uiState: RadioControlState) {
|
||||
return
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
uiState.transponders.forEach { radio ->
|
||||
transponders.forEach { radio ->
|
||||
TransponderItem(
|
||||
radio = radio,
|
||||
isSelected = radio.uuid == uiState.selectedTransponderUuid,
|
||||
onSelect = { uiState.sendAction(RadioControlAction.SelectTransponder(radio.uuid)) }
|
||||
isSelected = radio.uuid == selectedUuid,
|
||||
onSelect = { onAction(RadioControlAction.SelectTransponder(radio.uuid)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -257,21 +287,24 @@ private fun TransponderItem(radio: SatRadio, isSelected: Boolean, onSelect: () -
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun CtcssSelector(uiState: RadioControlState) {
|
||||
private fun CtcssSelector(
|
||||
ctcssTone: Double?,
|
||||
onAction: (RadioControlAction) -> Unit
|
||||
) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) {
|
||||
Text(text = "CTCSS Tone", color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
FilterChip(
|
||||
selected = uiState.ctcssTone == null,
|
||||
onClick = { uiState.sendAction(RadioControlAction.SetCtcssTone(null)) },
|
||||
selected = ctcssTone == null,
|
||||
onClick = { onAction(RadioControlAction.SetCtcssTone(null)) },
|
||||
label = { Text("Off") }
|
||||
)
|
||||
RadioControlViewModel.CTCSS_TONES.forEach { tone ->
|
||||
FilterChip(
|
||||
selected = uiState.ctcssTone == tone,
|
||||
onClick = { uiState.sendAction(RadioControlAction.SetCtcssTone(tone)) },
|
||||
selected = ctcssTone == tone,
|
||||
onClick = { onAction(RadioControlAction.SetCtcssTone(tone)) },
|
||||
label = { Text(String.format(Locale.ENGLISH, "%.1f", tone)) }
|
||||
)
|
||||
}
|
||||
@@ -280,21 +313,26 @@ private fun CtcssSelector(uiState: RadioControlState) {
|
||||
}
|
||||
}
|
||||
|
||||
private val FREQ_ADJUSTMENTS =
|
||||
listOf(-10_000L to "-10k", -1_000L to "-1k", -100L to "-100", 100L to "+100", 1_000L to "+1k", 10_000L to "+10k")
|
||||
|
||||
@Composable
|
||||
private fun FrequencyTuner(uiState: RadioControlState) {
|
||||
val freq = uiState.txBaseFrequencyHz ?: return
|
||||
val transponder = uiState.transponders.find { it.uuid == uiState.selectedTransponderUuid }
|
||||
private fun FrequencyTuner(
|
||||
txBaseFrequencyHz: Long,
|
||||
selectedTransponder: SatRadio?,
|
||||
onAction: (RadioControlAction) -> Unit
|
||||
) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
Text(text = "TX Base Frequency", color = MaterialTheme.colorScheme.primary)
|
||||
if (transponder != null) {
|
||||
val upLow = transponder.uplinkLow
|
||||
val upHigh = transponder.uplinkHigh
|
||||
val dnLow = transponder.downlinkLow
|
||||
val dnHigh = transponder.downlinkHigh
|
||||
if (selectedTransponder != null) {
|
||||
val upLow = selectedTransponder.uplinkLow
|
||||
val upHigh = selectedTransponder.uplinkHigh
|
||||
val dnLow = selectedTransponder.downlinkLow
|
||||
val dnHigh = selectedTransponder.downlinkHigh
|
||||
if (upLow != null && upHigh != null && upLow != upHigh) {
|
||||
Text(
|
||||
text = "UP: ${RadioControlViewModel.formatFrequency(upLow)} - ${RadioControlViewModel.formatFrequency(upHigh)}",
|
||||
@@ -312,68 +350,48 @@ private fun FrequencyTuner(uiState: RadioControlState) {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${RadioControlViewModel.formatFrequency(freq)} MHz",
|
||||
text = "${RadioControlViewModel.formatFrequency(txBaseFrequencyHz)} MHz",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(-10_000)) },
|
||||
text = "-10k",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(-1_000)) },
|
||||
text = "-1k",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(-100)) },
|
||||
text = "-100",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(100)) },
|
||||
text = "+100",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(1_000)) },
|
||||
text = "+1k",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(10_000)) },
|
||||
text = "+10k",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
FREQ_ADJUSTMENTS.forEach { (delta, label) ->
|
||||
CardButton(
|
||||
onClick = { onAction(RadioControlAction.AdjustTxFrequency(delta)) },
|
||||
text = label,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlButtons(uiState: RadioControlState) {
|
||||
val isConnected = uiState.txPanel.isConnected || uiState.rxPanel.isConnected
|
||||
private fun ControlButtons(
|
||||
isConnected: Boolean,
|
||||
isTracking: Boolean,
|
||||
onAction: (RadioControlAction) -> Unit
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (!isConnected) {
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.ConnectRadios) },
|
||||
onClick = { onAction(RadioControlAction.ConnectRadios) },
|
||||
text = "Connect",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
} else {
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.DisconnectRadios) },
|
||||
onClick = { onAction(RadioControlAction.DisconnectRadios) },
|
||||
text = "Disconnect",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
CardButton(
|
||||
onClick = { uiState.sendAction(RadioControlAction.ToggleTracking) },
|
||||
text = if (uiState.isTracking) "Stop Tracking" else "Start Tracking",
|
||||
onClick = { onAction(RadioControlAction.ToggleTracking) },
|
||||
text = if (isTracking) "Stop Tracking" else "Start Tracking",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
+9
-10
@@ -42,16 +42,15 @@ data class RadioControlState(
|
||||
val txBaseFrequencyHz: Long?,
|
||||
val ctcssTone: Double?,
|
||||
val isTracking: Boolean,
|
||||
val errorMessage: String?,
|
||||
val sendAction: (RadioControlAction) -> Unit
|
||||
val errorMessage: String?
|
||||
)
|
||||
|
||||
sealed class RadioControlAction {
|
||||
data class SelectTransponder(val uuid: String) : RadioControlAction()
|
||||
data class SetTxFrequency(val frequencyHz: Long) : RadioControlAction()
|
||||
data class AdjustTxFrequency(val deltaHz: Long) : RadioControlAction()
|
||||
data class SetCtcssTone(val toneHz: Double?) : RadioControlAction()
|
||||
data object ToggleTracking : RadioControlAction()
|
||||
data object ConnectRadios : RadioControlAction()
|
||||
data object DisconnectRadios : RadioControlAction()
|
||||
sealed interface RadioControlAction {
|
||||
data class SelectTransponder(val uuid: String) : RadioControlAction
|
||||
data class SetTxFrequency(val frequencyHz: Long) : RadioControlAction
|
||||
data class AdjustTxFrequency(val deltaHz: Long) : RadioControlAction
|
||||
data class SetCtcssTone(val toneHz: Double?) : RadioControlAction
|
||||
data object ToggleTracking : RadioControlAction
|
||||
data object ConnectRadios : RadioControlAction
|
||||
data object DisconnectRadios : RadioControlAction
|
||||
}
|
||||
+32
-19
@@ -30,10 +30,13 @@ import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.IRadioTrackingService
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
import com.rtbishop.look4sat.core.domain.utility.toTimerString
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@@ -46,6 +49,7 @@ class RadioControlViewModel(
|
||||
|
||||
private var currentPass: OrbitalPass? = null
|
||||
private var transponders: List<SatRadio> = emptyList()
|
||||
private val stationPos = settingsRepo.stationPosition.value
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
RadioControlState(
|
||||
@@ -62,8 +66,7 @@ class RadioControlViewModel(
|
||||
txBaseFrequencyHz = null,
|
||||
ctcssTone = null,
|
||||
isTracking = false,
|
||||
errorMessage = null,
|
||||
sendAction = ::handleAction
|
||||
errorMessage = null
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<RadioControlState> = _uiState
|
||||
@@ -72,6 +75,7 @@ class RadioControlViewModel(
|
||||
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
||||
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
||||
|
||||
// Resolve pass and load transponders
|
||||
viewModelScope.launch {
|
||||
val passes = satelliteRepo.passes.value
|
||||
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
||||
@@ -90,29 +94,30 @@ class RadioControlViewModel(
|
||||
it.copy(selectedTransponderUuid = svcState.selectedTransponder?.uuid)
|
||||
}
|
||||
}
|
||||
// Tick loop — timer and satellite position updates every second
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow)
|
||||
val (timeStr, isAos) = computeTimer(satPass.isDeepSpace, satPass.aosTime, satPass.losTime, timeNow)
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
currentTime = timeStr,
|
||||
isCurrentTimeAos = isAos,
|
||||
azimuth = String.format(Locale.ENGLISH, "%.1f", pos.azimuth.toDegrees()),
|
||||
elevation = String.format(Locale.ENGLISH, "%.1f", pos.elevation.toDegrees()),
|
||||
distance = String.format(Locale.ENGLISH, "%.0f", pos.distance)
|
||||
)
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Observe service state and map to UI state
|
||||
// Observe service state for radio-specific updates (panels, frequencies, tracking status)
|
||||
viewModelScope.launch {
|
||||
trackingService.state.collect { svc ->
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val pass = currentPass
|
||||
val timeStr = when {
|
||||
pass == null -> "00:00:00"
|
||||
pass.isDeepSpace -> 0L.toTimerString()
|
||||
pass.aosTime > timeNow -> pass.aosTime.minus(timeNow).toTimerString()
|
||||
else -> pass.losTime.minus(timeNow).toTimerString()
|
||||
}
|
||||
val isAos = pass != null && !pass.isDeepSpace && pass.aosTime > timeNow
|
||||
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
currentTime = timeStr,
|
||||
isCurrentTimeAos = isAos,
|
||||
azimuth = String.format(Locale.ENGLISH, "%.1f", svc.azimuth),
|
||||
elevation = String.format(Locale.ENGLISH, "%.1f", svc.elevation),
|
||||
distance = String.format(Locale.ENGLISH, "%.0f", svc.distance),
|
||||
txPanel = RadioPanelState(
|
||||
label = "TX (Uplink)",
|
||||
isConnected = svc.txConnected,
|
||||
@@ -138,7 +143,15 @@ class RadioControlViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: RadioControlAction) {
|
||||
private fun computeTimer(isDeepSpace: Boolean, aosTime: Long, losTime: Long, timeNow: Long): Pair<String, Boolean> {
|
||||
return when {
|
||||
isDeepSpace -> 0L.toTimerString() to false
|
||||
aosTime > timeNow -> (aosTime - timeNow).toTimerString() to true
|
||||
else -> (losTime - timeNow).toTimerString() to false
|
||||
}
|
||||
}
|
||||
|
||||
fun onAction(action: RadioControlAction) {
|
||||
when (action) {
|
||||
is RadioControlAction.SelectTransponder -> {
|
||||
val transponder = transponders.find { it.uuid == action.uuid } ?: return
|
||||
|
||||
+9
-9
@@ -28,13 +28,13 @@ data class SatellitesState(
|
||||
val typesList: List<String> = emptyList()
|
||||
)
|
||||
|
||||
sealed class SatellitesAction {
|
||||
data object DismissWarning : SatellitesAction()
|
||||
data object SaveSelection : SatellitesAction()
|
||||
data class SearchFor(val query: String) : SatellitesAction()
|
||||
data object SelectAll : SatellitesAction()
|
||||
data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction()
|
||||
data class SelectTypes(val types: List<String>) : SatellitesAction()
|
||||
data object ToggleTypesDialog : SatellitesAction()
|
||||
data object UnselectAll : SatellitesAction()
|
||||
sealed interface SatellitesAction {
|
||||
data object DismissWarning : SatellitesAction
|
||||
data object SaveSelection : SatellitesAction
|
||||
data class SearchFor(val query: String) : SatellitesAction
|
||||
data object SelectAll : SatellitesAction
|
||||
data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction
|
||||
data class SelectTypes(val types: List<String>) : SatellitesAction
|
||||
data object ToggleTypesDialog : SatellitesAction
|
||||
data object UnselectAll : SatellitesAction
|
||||
}
|
||||
+103
-147
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.settings
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -29,6 +31,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -257,14 +260,8 @@ fun NetworkOutputDialog(
|
||||
val (rotIp, rotPort) = splitAddress(rotatorAddress.value)
|
||||
val (freqIp, freqPort) = splitAddress(frequencyAddress.value)
|
||||
onSave(
|
||||
rotatorState.value,
|
||||
rotIp,
|
||||
rotPort,
|
||||
rotatorFormat.value,
|
||||
frequencyState.value,
|
||||
freqIp,
|
||||
freqPort,
|
||||
frequencyFormat.value
|
||||
rotatorState.value, rotIp, rotPort, rotatorFormat.value,
|
||||
frequencyState.value, freqIp, freqPort, frequencyFormat.value
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
@@ -274,71 +271,29 @@ fun NetworkOutputDialog(
|
||||
onAccept = onAccept
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = padding)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.prefs_net_rotator_switch))
|
||||
Switch(
|
||||
checked = rotatorState.value,
|
||||
onCheckedChange = { rotatorState.value = it }
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = rotatorAddress.value,
|
||||
onValueChange = { rotatorAddress.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_net_rotator_address_hint)) },
|
||||
modifier = Modifier.weight(0.6f),
|
||||
enabled = rotatorState.value
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = rotatorFormat.value,
|
||||
onValueChange = { rotatorFormat.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_net_rotator_format_hint)) },
|
||||
modifier = Modifier.weight(0.4f),
|
||||
enabled = rotatorState.value
|
||||
)
|
||||
}
|
||||
OutputChannelSection(
|
||||
switchLabel = stringResource(R.string.prefs_net_rotator_switch),
|
||||
enabled = rotatorState.value,
|
||||
onEnabledChange = { rotatorState.value = it },
|
||||
address = rotatorAddress.value,
|
||||
onAddressChange = { rotatorAddress.value = it },
|
||||
addressLabel = stringResource(R.string.prefs_net_rotator_address_hint),
|
||||
format = rotatorFormat.value,
|
||||
onFormatChange = { rotatorFormat.value = it },
|
||||
formatLabel = stringResource(R.string.prefs_net_rotator_format_hint)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.prefs_net_frequency_switch))
|
||||
Switch(
|
||||
checked = frequencyState.value,
|
||||
onCheckedChange = { frequencyState.value = it }
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = frequencyAddress.value,
|
||||
onValueChange = { frequencyAddress.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_net_frequency_address_hint)) },
|
||||
modifier = Modifier.weight(0.6f),
|
||||
enabled = frequencyState.value
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = frequencyFormat.value,
|
||||
onValueChange = { frequencyFormat.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_net_frequency_format_hint)) },
|
||||
modifier = Modifier.weight(0.4f),
|
||||
enabled = frequencyState.value
|
||||
)
|
||||
}
|
||||
OutputChannelSection(
|
||||
switchLabel = stringResource(R.string.prefs_net_frequency_switch),
|
||||
enabled = frequencyState.value,
|
||||
onEnabledChange = { frequencyState.value = it },
|
||||
address = frequencyAddress.value,
|
||||
onAddressChange = { frequencyAddress.value = it },
|
||||
addressLabel = stringResource(R.string.prefs_net_frequency_address_hint),
|
||||
format = frequencyFormat.value,
|
||||
onFormatChange = { frequencyFormat.value = it },
|
||||
formatLabel = stringResource(R.string.prefs_net_frequency_format_hint)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,12 +353,8 @@ fun BluetoothOutputDialog(
|
||||
val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) }
|
||||
val onAccept = {
|
||||
onSave(
|
||||
rotatorState.value,
|
||||
rotatorAddress.value,
|
||||
rotatorFormat.value,
|
||||
frequencyState.value,
|
||||
frequencyAddress.value,
|
||||
frequencyFormat.value
|
||||
rotatorState.value, rotatorAddress.value, rotatorFormat.value,
|
||||
frequencyState.value, frequencyAddress.value, frequencyFormat.value
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
@@ -413,75 +364,80 @@ fun BluetoothOutputDialog(
|
||||
onAccept = onAccept
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = padding)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.prefs_bt_rotator_switch))
|
||||
Switch(
|
||||
checked = rotatorState.value,
|
||||
onCheckedChange = { rotatorState.value = it }
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = rotatorAddress.value,
|
||||
onValueChange = { rotatorAddress.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_bt_rotator_device_hint)) },
|
||||
modifier = Modifier.weight(0.6f),
|
||||
enabled = rotatorState.value
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = rotatorFormat.value,
|
||||
onValueChange = { rotatorFormat.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_bt_rotator_output_hint)) },
|
||||
modifier = Modifier.weight(0.4f),
|
||||
enabled = rotatorState.value
|
||||
)
|
||||
}
|
||||
OutputChannelSection(
|
||||
switchLabel = stringResource(R.string.prefs_bt_rotator_switch),
|
||||
enabled = rotatorState.value,
|
||||
onEnabledChange = { rotatorState.value = it },
|
||||
address = rotatorAddress.value,
|
||||
onAddressChange = { rotatorAddress.value = it },
|
||||
addressLabel = stringResource(R.string.prefs_bt_rotator_device_hint),
|
||||
format = rotatorFormat.value,
|
||||
onFormatChange = { rotatorFormat.value = it },
|
||||
formatLabel = stringResource(R.string.prefs_bt_rotator_output_hint)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.prefs_bt_frequency_switch))
|
||||
Switch(
|
||||
checked = frequencyState.value,
|
||||
onCheckedChange = { frequencyState.value = it }
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = frequencyAddress.value,
|
||||
onValueChange = { frequencyAddress.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_bt_frequency_device_hint)) },
|
||||
modifier = Modifier.weight(0.6f),
|
||||
enabled = frequencyState.value
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = frequencyFormat.value,
|
||||
onValueChange = { frequencyFormat.value = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prefs_bt_frequency_output_hint)) },
|
||||
modifier = Modifier.weight(0.4f),
|
||||
enabled = frequencyState.value
|
||||
)
|
||||
}
|
||||
OutputChannelSection(
|
||||
switchLabel = stringResource(R.string.prefs_bt_frequency_switch),
|
||||
enabled = frequencyState.value,
|
||||
onEnabledChange = { frequencyState.value = it },
|
||||
address = frequencyAddress.value,
|
||||
onAddressChange = { frequencyAddress.value = it },
|
||||
addressLabel = stringResource(R.string.prefs_bt_frequency_device_hint),
|
||||
format = frequencyFormat.value,
|
||||
onFormatChange = { frequencyFormat.value = it },
|
||||
formatLabel = stringResource(R.string.prefs_bt_frequency_output_hint)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable section for a switch-toggled output channel with address and format fields.
|
||||
* Used by both Network and Bluetooth output dialogs.
|
||||
*/
|
||||
@Composable
|
||||
private fun OutputChannelSection(
|
||||
switchLabel: String,
|
||||
enabled: Boolean,
|
||||
onEnabledChange: (Boolean) -> Unit,
|
||||
address: String,
|
||||
onAddressChange: (String) -> Unit,
|
||||
addressLabel: String,
|
||||
format: String,
|
||||
onFormatChange: (String) -> Unit,
|
||||
formatLabel: String
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(switchLabel)
|
||||
Switch(checked = enabled, onCheckedChange = onEnabledChange)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = address,
|
||||
onValueChange = onAddressChange,
|
||||
singleLine = true,
|
||||
label = { Text(addressLabel) },
|
||||
modifier = Modifier.weight(0.6f),
|
||||
enabled = enabled
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = format,
|
||||
onValueChange = onFormatChange,
|
||||
singleLine = true,
|
||||
label = { Text(formatLabel) },
|
||||
modifier = Modifier.weight(0.4f),
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioControlDialog(
|
||||
initialSettings: RadioControlSettings,
|
||||
@@ -497,12 +453,12 @@ fun RadioControlDialog(
|
||||
val rxAddress = rememberSaveable { mutableStateOf(initialSettings.rxRadioAddress) }
|
||||
val txName = rememberSaveable { mutableStateOf(initialSettings.txRadioName) }
|
||||
val rxName = rememberSaveable { mutableStateOf(initialSettings.rxRadioName) }
|
||||
val baudRate = rememberSaveable { mutableStateOf(initialSettings.baudRate) }
|
||||
val baudRate = rememberSaveable { mutableIntStateOf(initialSettings.baudRate) }
|
||||
val selectingFor = rememberSaveable { mutableStateOf("") } // "tx", "rx", or ""
|
||||
|
||||
val pairedDevices = remember {
|
||||
try {
|
||||
val manager = context.getSystemService(android.content.Context.BLUETOOTH_SERVICE) as android.bluetooth.BluetoothManager
|
||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
manager.adapter?.bondedDevices?.map { Pair(it.name ?: "Unknown", it.address) } ?: emptyList()
|
||||
} catch (_: SecurityException) {
|
||||
emptyList()
|
||||
@@ -518,7 +474,7 @@ fun RadioControlDialog(
|
||||
rxRadioAddress = rxAddress.value,
|
||||
txRadioName = txName.value,
|
||||
rxRadioName = rxName.value,
|
||||
baudRate = baudRate.value
|
||||
baudRate = baudRate.intValue
|
||||
)
|
||||
)
|
||||
onDismiss()
|
||||
@@ -629,8 +585,8 @@ fun RadioControlDialog(
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
baudRates.forEach { rate ->
|
||||
CardButton(
|
||||
onClick = { baudRate.value = rate },
|
||||
text = if (rate == baudRate.value) "[$rate]" else rate.toString(),
|
||||
onClick = { baudRate.intValue = rate },
|
||||
text = if (rate == baudRate.intValue) "[$rate]" else rate.toString(),
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
|
||||
+190
-160
@@ -42,9 +42,12 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -81,66 +84,44 @@ fun NavGraphBuilder.settingsDestination() {
|
||||
modelClass = SettingsViewModel::class.java,
|
||||
factory = SettingsViewModel.Factory
|
||||
)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
SettingsScreen(uiState)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsScreen(uiState: SettingsState) {
|
||||
val locationContract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
val locationError = stringResource(R.string.prefs_loc_gps_error)
|
||||
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
|
||||
val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions ->
|
||||
when {
|
||||
permissions[locationPermFine] == true -> uiState.sendAction(SettingsAction.SetGpsPosition)
|
||||
permissions[locationPermCoarse] == true -> uiState.sendAction(SettingsAction.SetGpsPosition)
|
||||
else -> uiState.sendSystemAction(SystemAction.ShowToast(locationError))
|
||||
}
|
||||
}
|
||||
val contentContract = ActivityResultContracts.GetContent()
|
||||
val contentRequestForTle = rememberLauncherForActivityResult(contentContract) { uri ->
|
||||
uri?.let { uiState.sendAction(SettingsAction.UpdateTLEFromFile(uri.toString())) }
|
||||
}
|
||||
val contentRequestForTransceivers = rememberLauncherForActivityResult(contentContract) { uri ->
|
||||
uri?.let { uiState.sendAction(SettingsAction.UpdateTransceiversFromFile(uri.toString())) }
|
||||
}
|
||||
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
|
||||
val dialogs = rememberDialogVisibility()
|
||||
val permissions = rememberSettingsPermissions(
|
||||
sendAction = onAction,
|
||||
onBluetoothGranted = { dialogs.bluetooth = true }
|
||||
)
|
||||
|
||||
// Position settings
|
||||
val posSettings = uiState.positionSettings
|
||||
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
|
||||
val setGeoPos = { lat: Double, lon: Double ->
|
||||
uiState.sendAction(SettingsAction.SetGeoPosition(lat, lon))
|
||||
// Dialogs
|
||||
if (dialogs.position) {
|
||||
PositionDialog(
|
||||
uiState.positionSettings.stationPos.latitude,
|
||||
uiState.positionSettings.stationPos.longitude,
|
||||
dismiss = { dialogs.position = false },
|
||||
save = { lat, lon -> onAction(SettingsAction.SetGeoPosition(lat, lon)) }
|
||||
)
|
||||
}
|
||||
val setQthPos = { locator: String ->
|
||||
uiState.sendAction(SettingsAction.SetQthPosition(locator))
|
||||
if (dialogs.locator) {
|
||||
LocatorDialog(
|
||||
uiState.positionSettings.stationPos.qthLocator,
|
||||
dismiss = { dialogs.locator = false },
|
||||
save = { onAction(SettingsAction.SetQthPosition(it)) }
|
||||
)
|
||||
}
|
||||
val dismissPos = { uiState.sendAction(SettingsAction.DismissPosMessages) }
|
||||
val posDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val showPosDialog = { posDialogState.value = posDialogState.value.not() }
|
||||
if (posDialogState.value) {
|
||||
PositionDialog(posSettings.stationPos.latitude, posSettings.stationPos.longitude, showPosDialog, setGeoPos)
|
||||
}
|
||||
val locDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val showLocDialog = { locDialogState.value = locDialogState.value.not() }
|
||||
if (locDialogState.value) {
|
||||
LocatorDialog(posSettings.stationPos.qthLocator, showLocDialog, setQthPos)
|
||||
}
|
||||
|
||||
// Data sources dialog
|
||||
val dataSourcesDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val showDataSourcesDialog = { dataSourcesDialogState.value = true }
|
||||
val dismissDataSourcesDialog = { dataSourcesDialogState.value = false }
|
||||
if (dataSourcesDialogState.value) {
|
||||
if (dialogs.dataSources) {
|
||||
DataSourcesDialog(
|
||||
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
|
||||
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
|
||||
tleUrl = uiState.dataSourcesSettings.tleUrl,
|
||||
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
|
||||
onImportTle = { contentRequestForTle.launch("*/*") },
|
||||
onImportTransceivers = { contentRequestForTransceivers.launch("*/*") },
|
||||
onDismiss = dismissDataSourcesDialog,
|
||||
onImportTle = { permissions.launchTleImport(); dialogs.dataSources = false },
|
||||
onImportTransceivers = { permissions.launchTransceiverImport(); dialogs.dataSources = false },
|
||||
onDismiss = { dialogs.dataSources = false },
|
||||
onSave = { useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
|
||||
val current = uiState.dataSourcesSettings
|
||||
val newSettings = current.copy(
|
||||
@@ -149,112 +130,55 @@ private fun SettingsScreen(uiState: SettingsState) {
|
||||
useCustomTransceivers = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) useCustomTransceivers else current.useCustomTransceivers,
|
||||
transceiversUrl = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) transceiversUrl else current.transceiversUrl
|
||||
)
|
||||
if (newSettings != current) {
|
||||
uiState.sendDataSourcesAction(DataSourcesAction.Update(newSettings))
|
||||
}
|
||||
if (useCustomTle || useCustomTransceivers) {
|
||||
uiState.sendAction(SettingsAction.UpdateFromWeb)
|
||||
}
|
||||
if (newSettings != current) onAction(SettingsAction.UpdateDataSources(newSettings))
|
||||
if (useCustomTle || useCustomTransceivers) onAction(SettingsAction.UpdateFromWeb)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Data settings
|
||||
val dataSettings = uiState.dataSettings
|
||||
val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
|
||||
val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
|
||||
|
||||
// RC settings
|
||||
val rcSettings = uiState.rcSettings
|
||||
|
||||
// Radio control (FT-817) settings
|
||||
val radioControlDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val showRadioControlDialog = { radioControlDialogState.value = true }
|
||||
val dismissRadioControlDialog = { radioControlDialogState.value = false }
|
||||
if (radioControlDialogState.value) {
|
||||
if (dialogs.network) {
|
||||
NetworkOutputDialog(
|
||||
initialSettings = uiState.rcSettings,
|
||||
onDismiss = { dialogs.network = false },
|
||||
onSave = { rotState, rotAddr, rotPort, rotFmt, freqState, freqAddr, freqPort, freqFmt ->
|
||||
onAction(
|
||||
SettingsAction.UpdateRC(
|
||||
uiState.rcSettings.copy(
|
||||
rotatorState = rotState, rotatorAddress = rotAddr,
|
||||
rotatorPort = rotPort, rotatorFormat = rotFmt,
|
||||
frequencyState = freqState, frequencyAddress = freqAddr,
|
||||
frequencyPort = freqPort, frequencyFormat = freqFmt
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (dialogs.bluetooth) {
|
||||
BluetoothOutputDialog(
|
||||
initialSettings = uiState.rcSettings,
|
||||
onDismiss = { dialogs.bluetooth = false },
|
||||
onSave = { rotState, rotAddr, rotFmt, freqState, freqAddr, freqFmt ->
|
||||
onAction(
|
||||
SettingsAction.UpdateRC(
|
||||
uiState.rcSettings.copy(
|
||||
bluetoothRotatorState = rotState, bluetoothRotatorAddress = rotAddr,
|
||||
bluetoothRotatorFormat = rotFmt, bluetoothFrequencyState = freqState,
|
||||
bluetoothFrequencyAddress = freqAddr, bluetoothFrequencyFormat = freqFmt
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (dialogs.radioControl) {
|
||||
RadioControlDialog(
|
||||
initialSettings = uiState.radioControlSettings,
|
||||
onDismiss = dismissRadioControlDialog,
|
||||
onSave = { settings ->
|
||||
uiState.sendRadioControlAction(RadioControlSettingsAction.Update(settings))
|
||||
}
|
||||
onDismiss = { dialogs.radioControl = false },
|
||||
onSave = { onAction(SettingsAction.UpdateRadioControl(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
// Network data output
|
||||
val networkDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val showNetworkDialog = { networkDialogState.value = true }
|
||||
val dismissNetworkDialog = { networkDialogState.value = false }
|
||||
if (networkDialogState.value) {
|
||||
NetworkOutputDialog(
|
||||
initialSettings = rcSettings,
|
||||
onDismiss = dismissNetworkDialog,
|
||||
onSave = { rotatorState, rotatorAddress, rotatorPort, rotatorFormat,
|
||||
frequencyState, frequencyAddress, frequencyPort, frequencyFormat ->
|
||||
uiState.sendRCAction(
|
||||
RCAction.Update(
|
||||
rcSettings.copy(
|
||||
rotatorState = rotatorState,
|
||||
rotatorAddress = rotatorAddress,
|
||||
rotatorPort = rotatorPort,
|
||||
rotatorFormat = rotatorFormat,
|
||||
frequencyState = frequencyState,
|
||||
frequencyAddress = frequencyAddress,
|
||||
frequencyPort = frequencyPort,
|
||||
frequencyFormat = frequencyFormat
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Bluetooth data output
|
||||
val bluetoothDialogState = rememberSaveable { mutableStateOf(false) }
|
||||
val bluetoothContract = ActivityResultContracts.RequestPermission()
|
||||
val bluetoothError = stringResource(R.string.prefs_bt_perm_error)
|
||||
val bluetoothPerm = when {
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
|
||||
else -> Manifest.permission.BLUETOOTH_CONNECT
|
||||
}
|
||||
val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
|
||||
if (!isGranted)
|
||||
{
|
||||
uiState.sendRCAction(
|
||||
RCAction.Update(
|
||||
rcSettings.copy(bluetoothRotatorState = false, bluetoothFrequencyState = false)
|
||||
)
|
||||
)
|
||||
uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError))
|
||||
}
|
||||
else { bluetoothDialogState.value = true }
|
||||
}
|
||||
val showBluetoothDialog = { bluetoothRequest.launch(bluetoothPerm) }
|
||||
val dismissBluetoothDialog = { bluetoothDialogState.value = false }
|
||||
if (bluetoothDialogState.value) {
|
||||
BluetoothOutputDialog(
|
||||
initialSettings = rcSettings,
|
||||
onDismiss = dismissBluetoothDialog,
|
||||
onSave = { rotatorState, rotatorAddress, rotatorFormat,
|
||||
frequencyState, frequencyAddress, frequencyFormat ->
|
||||
uiState.sendRCAction(
|
||||
RCAction.Update(
|
||||
rcSettings.copy(
|
||||
bluetoothRotatorState = rotatorState,
|
||||
bluetoothRotatorAddress = rotatorAddress,
|
||||
bluetoothRotatorFormat = rotatorFormat,
|
||||
bluetoothFrequencyState = frequencyState,
|
||||
bluetoothFrequencyAddress = frequencyAddress,
|
||||
bluetoothFrequencyFormat = frequencyFormat
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Other settings
|
||||
val otherSettings = uiState.otherSettings
|
||||
// URLs for top bar
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val appUrl = stringResource(R.string.prefs_app_url)
|
||||
val donateUrl = stringResource(R.string.prefs_donate_url)
|
||||
@@ -302,13 +226,31 @@ private fun SettingsScreen(uiState: SettingsState) {
|
||||
modifier = Modifier.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
item {
|
||||
LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction)
|
||||
LocationCard(
|
||||
settings = uiState.positionSettings,
|
||||
setGpsPos = permissions.launchLocation,
|
||||
showPosDialog = { dialogs.position = true },
|
||||
showLocDialog = { dialogs.locator = true },
|
||||
dismissPosMessage = { onAction(SettingsAction.DismissPosMessages) },
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
item {
|
||||
DataCard(
|
||||
settings = uiState.dataSettings,
|
||||
updateFromWeb = { onAction(SettingsAction.UpdateFromWeb) },
|
||||
clearAllData = { onAction(SettingsAction.ClearAllData) },
|
||||
showDataSourcesDialog = { dialogs.dataSources = true }
|
||||
)
|
||||
}
|
||||
item { DataCard(dataSettings, updateFromWeb, clearAllData, showDataSourcesDialog) }
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
OutputCard({ showNetworkDialog() }, { showBluetoothDialog() }, { showRadioControlDialog() })
|
||||
OutputCard(
|
||||
onNetworkClick = { dialogs.network = true },
|
||||
onBluetoothClick = permissions.launchBluetooth,
|
||||
onRadioControlClick = { dialogs.radioControl = true }
|
||||
)
|
||||
}
|
||||
item { OtherCard(otherSettings, uiState.sendAction) }
|
||||
item { OtherCard(uiState.otherSettings, onAction) }
|
||||
item { CardCredits() }
|
||||
}
|
||||
}
|
||||
@@ -329,7 +271,7 @@ private fun LocationCard(
|
||||
showPosDialog: () -> Unit,
|
||||
showLocDialog: () -> Unit,
|
||||
dismissPosMessage: () -> Unit,
|
||||
sendSystemAction: (SystemAction) -> Unit
|
||||
onAction: (SettingsAction) -> Unit
|
||||
) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
@@ -344,7 +286,7 @@ private fun LocationCard(
|
||||
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = setUpdateTime(updateTime = settings.stationPos.timestamp))
|
||||
Text(text = formatUpdateTime(updateTime = settings.stationPos.timestamp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Lat: ${settings.stationPos.latitude}°")
|
||||
@@ -376,7 +318,7 @@ private fun LocationCard(
|
||||
if (settings.messageResId != 0) {
|
||||
val errorString = stringResource(id = settings.messageResId)
|
||||
LaunchedEffect(key1 = settings.messageResId) {
|
||||
sendSystemAction(SystemAction.ShowToast(errorString))
|
||||
onAction(SettingsAction.ShowToast(errorString))
|
||||
dismissPosMessage()
|
||||
}
|
||||
}
|
||||
@@ -409,7 +351,7 @@ private fun DataCard(
|
||||
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = setUpdateTime(updateTime = settings.timestamp))
|
||||
Text(text = formatUpdateTime(updateTime = settings.timestamp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.prefs_data_entries, settings.entriesTotal))
|
||||
@@ -491,7 +433,7 @@ private fun OtherCardPreview() = MainTheme {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OtherCard(settings: OtherSettings, sendAction: (SettingsAction) -> Unit) {
|
||||
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -503,16 +445,16 @@ private fun OtherCard(settings: OtherSettings, sendAction: (SettingsAction) -> U
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
SwitchRow(R.string.prefs_other_switch_utc, settings.stateOfUtc) {
|
||||
sendAction(SettingsAction.ToggleUtc(it))
|
||||
onAction(SettingsAction.ToggleUtc(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
|
||||
sendAction(SettingsAction.ToggleUpdate(it))
|
||||
onAction(SettingsAction.ToggleUpdate(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
|
||||
sendAction(SettingsAction.ToggleSweep(it))
|
||||
onAction(SettingsAction.ToggleSweep(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_sensors, settings.stateOfSensors) {
|
||||
sendAction(SettingsAction.ToggleSensor(it))
|
||||
onAction(SettingsAction.ToggleSensor(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,7 +473,7 @@ private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boole
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun setUpdateTime(updateTime: Long): String {
|
||||
private fun formatUpdateTime(updateTime: Long): String {
|
||||
val timePattern = stringResource(id = R.string.prefs_updated_time)
|
||||
val placeholder = stringResource(id = R.string.pass_time_placeholder)
|
||||
val updateDate = remember(updateTime) {
|
||||
@@ -638,3 +580,91 @@ private fun BotCard(onClick: () -> Unit, resId: Int, text: String, modifier: Mod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Dialog visibility state holder
|
||||
|
||||
@Stable
|
||||
private class DialogVisibility {
|
||||
var position by mutableStateOf(false)
|
||||
var locator by mutableStateOf(false)
|
||||
var dataSources by mutableStateOf(false)
|
||||
var network by mutableStateOf(false)
|
||||
var bluetooth by mutableStateOf(false)
|
||||
var radioControl by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberDialogVisibility(): DialogVisibility {
|
||||
return rememberSaveable(saver = run {
|
||||
androidx.compose.runtime.saveable.Saver(
|
||||
save = {
|
||||
listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl)
|
||||
},
|
||||
restore = {
|
||||
DialogVisibility().apply {
|
||||
position = it[0]; locator = it[1]; dataSources = it[2]
|
||||
network = it[3]; bluetooth = it[4]; radioControl = it[5]
|
||||
}
|
||||
}
|
||||
)
|
||||
}) { DialogVisibility() }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Permission launchers holder
|
||||
|
||||
@Stable
|
||||
private class SettingsPermissions(
|
||||
val launchLocation: () -> Unit,
|
||||
val launchTleImport: () -> Unit,
|
||||
val launchTransceiverImport: () -> Unit,
|
||||
val launchBluetooth: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun rememberSettingsPermissions(
|
||||
sendAction: (SettingsAction) -> Unit,
|
||||
onBluetoothGranted: () -> Unit
|
||||
): SettingsPermissions {
|
||||
val locationError = stringResource(R.string.prefs_loc_gps_error)
|
||||
val locationRequest = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
val fine = permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true
|
||||
val coarse = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true
|
||||
if (fine || coarse) sendAction(SettingsAction.SetGpsPosition)
|
||||
else sendAction(SettingsAction.ShowToast(locationError))
|
||||
}
|
||||
|
||||
val tleRequest = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { sendAction(SettingsAction.UpdateTLEFromFile(it.toString())) }
|
||||
}
|
||||
|
||||
val transceiversRequest = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { sendAction(SettingsAction.UpdateTransceiversFromFile(it.toString())) }
|
||||
}
|
||||
|
||||
val bluetoothError = stringResource(R.string.prefs_bt_perm_error)
|
||||
val bluetoothPerm = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)
|
||||
Manifest.permission.BLUETOOTH else Manifest.permission.BLUETOOTH_CONNECT
|
||||
val bluetoothRequest = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) onBluetoothGranted()
|
||||
else sendAction(SettingsAction.ShowToast(bluetoothError))
|
||||
}
|
||||
|
||||
return remember {
|
||||
SettingsPermissions(
|
||||
launchLocation = {
|
||||
locationRequest.launch(
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
)
|
||||
},
|
||||
launchTleImport = { tleRequest.launch("*/*") },
|
||||
launchTransceiverImport = { transceiversRequest.launch("*/*") },
|
||||
launchBluetooth = { bluetoothRequest.launch(bluetoothPerm) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
+26
-32
@@ -38,42 +38,36 @@ data class SettingsState(
|
||||
val otherSettings: OtherSettings,
|
||||
val rcSettings: RCSettings,
|
||||
val radioControlSettings: RadioControlSettings,
|
||||
val dataSourcesSettings: DataSourcesSettings,
|
||||
val sendAction: (SettingsAction) -> Unit,
|
||||
val sendRCAction: (RCAction) -> Unit,
|
||||
val sendRadioControlAction: (RadioControlSettingsAction) -> Unit,
|
||||
val sendSystemAction: (SystemAction) -> Unit,
|
||||
val sendDataSourcesAction: (DataSourcesAction) -> Unit
|
||||
val dataSourcesSettings: DataSourcesSettings
|
||||
)
|
||||
|
||||
sealed class SettingsAction {
|
||||
data object SetGpsPosition : SettingsAction()
|
||||
data class SetGeoPosition(val latitude: Double, val longitude: Double) : SettingsAction()
|
||||
data class SetQthPosition(val locator: String) : SettingsAction()
|
||||
data object DismissPosMessages : SettingsAction()
|
||||
data object UpdateFromWeb : SettingsAction()
|
||||
data class UpdateTLEFromFile(val uri: String) : SettingsAction()
|
||||
data class UpdateTransceiversFromFile(val uri: String) : SettingsAction()
|
||||
data object ClearAllData : SettingsAction()
|
||||
data class ToggleUtc(val value: Boolean) : SettingsAction()
|
||||
data class ToggleUpdate(val value: Boolean) : SettingsAction()
|
||||
data class ToggleSweep(val value: Boolean) : SettingsAction()
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction()
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction()
|
||||
}
|
||||
sealed interface SettingsAction {
|
||||
// Position
|
||||
data object SetGpsPosition : SettingsAction
|
||||
data class SetGeoPosition(val latitude: Double, val longitude: Double) : SettingsAction
|
||||
data class SetQthPosition(val locator: String) : SettingsAction
|
||||
data object DismissPosMessages : SettingsAction
|
||||
|
||||
sealed class SystemAction {
|
||||
data class ShowToast(val message: String) : SystemAction()
|
||||
}
|
||||
// Data
|
||||
data object UpdateFromWeb : SettingsAction
|
||||
data class UpdateTLEFromFile(val uri: String) : SettingsAction
|
||||
data class UpdateTransceiversFromFile(val uri: String) : SettingsAction
|
||||
data object ClearAllData : SettingsAction
|
||||
|
||||
sealed class RCAction {
|
||||
data class Update(val settings: RCSettings) : RCAction()
|
||||
}
|
||||
// Toggles
|
||||
data class ToggleUtc(val value: Boolean) : SettingsAction
|
||||
data class ToggleUpdate(val value: Boolean) : SettingsAction
|
||||
data class ToggleSweep(val value: Boolean) : SettingsAction
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||
|
||||
sealed class DataSourcesAction {
|
||||
data class Update(val settings: DataSourcesSettings) : DataSourcesAction()
|
||||
}
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
data class UpdateRadioControl(val settings: RadioControlSettings) : SettingsAction
|
||||
|
||||
sealed class RadioControlSettingsAction {
|
||||
data class Update(val settings: RadioControlSettings) : RadioControlSettingsAction()
|
||||
// Data sources
|
||||
data class UpdateDataSources(val settings: DataSourcesSettings) : SettingsAction
|
||||
|
||||
// System
|
||||
data class ShowToast(val message: String) : SettingsAction
|
||||
}
|
||||
+12
-33
@@ -48,12 +48,7 @@ class SettingsViewModel(
|
||||
otherSettings = settingsRepo.otherSettings.value,
|
||||
rcSettings = settingsRepo.rcSettings.value,
|
||||
radioControlSettings = settingsRepo.radioControlSettings.value,
|
||||
dataSourcesSettings = settingsRepo.dataSourcesSettings.value,
|
||||
sendAction = ::handleAction,
|
||||
sendRCAction = ::handleAction,
|
||||
sendRadioControlAction = ::handleAction,
|
||||
sendSystemAction = ::handleAction,
|
||||
sendDataSourcesAction = ::handleAction
|
||||
dataSourcesSettings = settingsRepo.dataSourcesSettings.value
|
||||
)
|
||||
)
|
||||
|
||||
@@ -104,12 +99,14 @@ class SettingsViewModel(
|
||||
}
|
||||
|
||||
|
||||
private fun handleAction(action: SettingsAction) {
|
||||
fun onAction(action: SettingsAction) {
|
||||
when (action) {
|
||||
// Position
|
||||
SettingsAction.SetGpsPosition -> setGpsPosition()
|
||||
is SettingsAction.SetGeoPosition -> setGeoPosition(action.latitude, action.longitude)
|
||||
is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
|
||||
SettingsAction.DismissPosMessages -> dismissPosMessage()
|
||||
// Data
|
||||
SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() }
|
||||
is SettingsAction.UpdateTLEFromFile -> runDataUpdate { databaseRepo.updateTLEFromFile(action.uri) }
|
||||
is SettingsAction.UpdateTransceiversFromFile -> runDataUpdate {
|
||||
@@ -117,37 +114,19 @@ class SettingsViewModel(
|
||||
action.uri
|
||||
)
|
||||
}
|
||||
|
||||
SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() }
|
||||
// Toggles
|
||||
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
||||
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
||||
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: RCAction) {
|
||||
when (action) {
|
||||
is RCAction.Update -> settingsRepo.updateRCSettings(action.settings)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: SystemAction) {
|
||||
when (action) {
|
||||
is SystemAction.ShowToast -> showToast(action.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: DataSourcesAction) {
|
||||
when (action) {
|
||||
is DataSourcesAction.Update -> settingsRepo.updateDataSourcesSettings(action.settings)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(action: RadioControlSettingsAction) {
|
||||
when (action) {
|
||||
is RadioControlSettingsAction.Update -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||
// Remote control & data sources
|
||||
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
||||
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||
is SettingsAction.UpdateDataSources -> settingsRepo.updateDataSourcesSettings(action.settings)
|
||||
// System
|
||||
is SettingsAction.ShowToast -> showToast(action.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +167,7 @@ class SettingsViewModel(
|
||||
// region Data update helpers — consolidated from 3 near-identical functions
|
||||
|
||||
/**
|
||||
* Common data update logic. Sets isUpdating=true, runs the suspend [block],
|
||||
* Common data update logic. Sets isUpdating=true, runs the suspending [block],
|
||||
* and resets isUpdating=false on failure.
|
||||
*/
|
||||
private fun runDataUpdate(block: suspend () -> Unit) = viewModelScope.launch {
|
||||
|
||||
Reference in new issue
Block a user