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
|
val positions: Map<OrbitalObject, GeoPos>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class MapAction {
|
sealed interface MapAction {
|
||||||
data object SelectPrev : MapAction()
|
data object SelectPrev : MapAction
|
||||||
data object SelectNext : MapAction()
|
data object SelectNext : MapAction
|
||||||
data class SelectItem(val item: OrbitalObject) : MapAction()
|
data class SelectItem(val item: OrbitalObject) : MapAction
|
||||||
data class SelectDefaultItem(val catnum: Int) : MapAction()
|
data class SelectDefaultItem(val catnum: Int) : MapAction
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MapData(
|
data class MapData(
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ private val allModes = listOf(
|
|||||||
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
|
"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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun PassesDialogPreview() {
|
private fun PassesDialogPreview() {
|
||||||
@@ -68,26 +70,27 @@ private fun PassesDialogPreview() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PassesDialog(hours: Int, elevation: Double, cancel: () -> Unit, accept: (Int, Double) -> Unit) {
|
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 elevationValueNew = remember { mutableDoubleStateOf(elevation) }
|
||||||
val onAccept = {
|
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) {
|
SharedDialog(title = stringResource(R.string.pass_filter_title), onCancel = cancel, onAccept = onAccept) {
|
||||||
SliderRow(
|
SliderRow(
|
||||||
title = stringResource(R.string.pass_filter_elev),
|
title = stringResource(R.string.pass_filter_elev),
|
||||||
value = elevationValueNew.doubleValue,
|
value = elevationValueNew.doubleValue,
|
||||||
valuePostfix = "°",
|
displayValue = "${elevationValueNew.doubleValue.toInt()}°",
|
||||||
valueResId = R.drawable.ic_elevation,
|
valueResId = R.drawable.ic_elevation,
|
||||||
valueRange = 0f..60f
|
valueRange = 0f..60f
|
||||||
) { elevationValueNew.doubleValue = it.toDouble() }
|
) { elevationValueNew.doubleValue = it.toDouble() }
|
||||||
SliderRow(
|
SliderRow(
|
||||||
title = stringResource(R.string.pass_filter_hours),
|
title = stringResource(R.string.pass_filter_hours),
|
||||||
value = hoursValue.intValue.toDouble(),
|
value = hoursIndex.intValue.toDouble(),
|
||||||
valuePostfix = "h",
|
displayValue = "${hourSteps[hoursIndex.intValue]}h",
|
||||||
valueResId = R.drawable.ic_clock,
|
valueResId = R.drawable.ic_clock,
|
||||||
valueRange = 1f..240f
|
valueRange = 0f..(hourSteps.size - 1).toFloat(),
|
||||||
) { hoursValue.intValue = it.toInt() }
|
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(
|
private fun SliderRow(
|
||||||
title: String,
|
title: String,
|
||||||
value: Double,
|
value: Double,
|
||||||
valuePostfix: String,
|
displayValue: String,
|
||||||
valueResId: Int,
|
valueResId: Int,
|
||||||
valueRange: ClosedFloatingPointRange<Float>,
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
steps: Int = 0,
|
||||||
onChange: (Float) -> Unit
|
onChange: (Float) -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -122,13 +126,13 @@ private fun SliderRow(
|
|||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "${value.toInt()}$valuePostfix",
|
text = displayValue,
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.primary
|
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
|
val shouldSeeWhatsNew: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class PassesAction {
|
sealed interface PassesAction {
|
||||||
data object DismissWhatsNew : PassesAction()
|
data object DismissWhatsNew : PassesAction
|
||||||
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction()
|
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction
|
||||||
data class FilterRadios(val modes: List<String>) : PassesAction()
|
data class FilterRadios(val modes: List<String>) : PassesAction
|
||||||
data object RefreshPasses : PassesAction()
|
data object RefreshPasses : PassesAction
|
||||||
data object TogglePassesDialog : PassesAction()
|
data object TogglePassesDialog : PassesAction
|
||||||
data object ToggleRadiosDialog : PassesAction()
|
data object ToggleRadiosDialog : PassesAction
|
||||||
}
|
}
|
||||||
+14
-6
@@ -34,6 +34,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -57,15 +58,22 @@ class PassesViewModel(
|
|||||||
val uiState: StateFlow<PassesState> = _uiState
|
val uiState: StateFlow<PassesState> = _uiState
|
||||||
|
|
||||||
init {
|
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 {
|
viewModelScope.launch {
|
||||||
var initialLoadDone = false
|
settingsRepo.selectedIds.collectLatest { selectedIds ->
|
||||||
satelliteRepo.passes.collectLatest { _ ->
|
_uiState.update { it.copy(isRefreshing = true) }
|
||||||
if (!initialLoadDone) {
|
if (selectedIds.isEmpty()) {
|
||||||
initialLoadDone = true
|
// No satellites selected — show indicator briefly, then stop
|
||||||
delay(1000) // Artificial delay to show the refreshing state on first load
|
delay(1000)
|
||||||
_uiState.update { it.copy(isRefreshing = false) }
|
_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
|
// Local tick loop — computes pass progress and countdown timer every second
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ data class RadarState(
|
|||||||
val selectedFrequency: Long? = null
|
val selectedFrequency: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class RadarAction {
|
sealed interface RadarAction {
|
||||||
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction()
|
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction
|
||||||
data class SelectTransmitter(val uuid: String) : RadarAction()
|
data class SelectTransmitter(val uuid: String) : RadarAction
|
||||||
}
|
}
|
||||||
@@ -174,6 +174,10 @@ class RadarViewModel(
|
|||||||
settingsRepo.rcSettings.value.frequencyState || settingsRepo.rcSettings.value.bluetoothFrequencyState
|
settingsRepo.rcSettings.value.frequencyState || settingsRepo.rcSettings.value.bluetoothFrequencyState
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
if (!isFreqEnabled) {
|
if (!isFreqEnabled) {
|
||||||
|
// Skip update if nothing changed
|
||||||
|
if (state.transmitters == transmitters && state.selectedTransmitterUuid == null && state.selectedFrequency == null) {
|
||||||
|
return@update state
|
||||||
|
}
|
||||||
return@update state.copy(
|
return@update state.copy(
|
||||||
transmitters = transmitters,
|
transmitters = transmitters,
|
||||||
selectedTransmitterUuid = null,
|
selectedTransmitterUuid = null,
|
||||||
@@ -191,6 +195,10 @@ class RadarViewModel(
|
|||||||
else -> null
|
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)
|
state.copy(transmitters = transmitters, selectedTransmitterUuid = selectedUuid, selectedFrequency = freq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-64
@@ -76,12 +76,16 @@ fun NavGraphBuilder.radioControlDestination(navigateUp: () -> Unit) {
|
|||||||
composable(route, args) {
|
composable(route, args) {
|
||||||
val viewModel = viewModel(RadioControlViewModel::class.java, factory = RadioControlViewModel.Factory)
|
val viewModel = viewModel(RadioControlViewModel::class.java, factory = RadioControlViewModel.Factory)
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
RadioControlScreen(uiState, navigateUp)
|
RadioControlScreen(uiState, viewModel::onAction, navigateUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Unit) {
|
private fun RadioControlScreen(
|
||||||
|
uiState: RadioControlState,
|
||||||
|
onAction: (RadioControlAction) -> Unit,
|
||||||
|
navigateUp: () -> Unit
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutPadding()
|
.layoutPadding()
|
||||||
@@ -115,21 +119,39 @@ private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Uni
|
|||||||
distance = uiState.distance
|
distance = uiState.distance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val selectedTransponder = uiState.transponders.find {
|
||||||
|
it.uuid == uiState.selectedTransponderUuid
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
item { TransponderSelector(uiState) }
|
item {
|
||||||
|
TransponderSelector(
|
||||||
val selectedTransponder = uiState.transponders.find {
|
transponders = uiState.transponders,
|
||||||
it.uuid == uiState.selectedTransponderUuid
|
selectedUuid = uiState.selectedTransponderUuid,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTransponder?.uplinkMode?.uppercase() == "FM") {
|
if (selectedTransponder?.uplinkMode?.uppercase() == "FM") {
|
||||||
item { CtcssSelector(uiState) }
|
item {
|
||||||
|
CtcssSelector(
|
||||||
|
ctcssTone = uiState.ctcssTone,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.txBaseFrequencyHz != null) {
|
if (uiState.txBaseFrequencyHz != null) {
|
||||||
item { FrequencyTuner(uiState) }
|
item {
|
||||||
|
FrequencyTuner(
|
||||||
|
txBaseFrequencyHz = uiState.txBaseFrequencyHz,
|
||||||
|
selectedTransponder = selectedTransponder,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.errorMessage?.let { msg ->
|
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
|
@Composable
|
||||||
private fun TransponderSelector(uiState: RadioControlState) {
|
private fun TransponderSelector(
|
||||||
if (uiState.transponders.isEmpty()) {
|
transponders: List<SatRadio>,
|
||||||
|
selectedUuid: String?,
|
||||||
|
onAction: (RadioControlAction) -> Unit
|
||||||
|
) {
|
||||||
|
if (transponders.isEmpty()) {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = "No transponders with uplink+downlink available",
|
text = "No transponders with uplink+downlink available",
|
||||||
@@ -222,11 +252,11 @@ private fun TransponderSelector(uiState: RadioControlState) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
uiState.transponders.forEach { radio ->
|
transponders.forEach { radio ->
|
||||||
TransponderItem(
|
TransponderItem(
|
||||||
radio = radio,
|
radio = radio,
|
||||||
isSelected = radio.uuid == uiState.selectedTransponderUuid,
|
isSelected = radio.uuid == selectedUuid,
|
||||||
onSelect = { uiState.sendAction(RadioControlAction.SelectTransponder(radio.uuid)) }
|
onSelect = { onAction(RadioControlAction.SelectTransponder(radio.uuid)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,21 +287,24 @@ private fun TransponderItem(radio: SatRadio, isSelected: Boolean, onSelect: () -
|
|||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun CtcssSelector(uiState: RadioControlState) {
|
private fun CtcssSelector(
|
||||||
|
ctcssTone: Double?,
|
||||||
|
onAction: (RadioControlAction) -> Unit
|
||||||
|
) {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) {
|
||||||
Text(text = "CTCSS Tone", color = MaterialTheme.colorScheme.primary)
|
Text(text = "CTCSS Tone", color = MaterialTheme.colorScheme.primary)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = uiState.ctcssTone == null,
|
selected = ctcssTone == null,
|
||||||
onClick = { uiState.sendAction(RadioControlAction.SetCtcssTone(null)) },
|
onClick = { onAction(RadioControlAction.SetCtcssTone(null)) },
|
||||||
label = { Text("Off") }
|
label = { Text("Off") }
|
||||||
)
|
)
|
||||||
RadioControlViewModel.CTCSS_TONES.forEach { tone ->
|
RadioControlViewModel.CTCSS_TONES.forEach { tone ->
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = uiState.ctcssTone == tone,
|
selected = ctcssTone == tone,
|
||||||
onClick = { uiState.sendAction(RadioControlAction.SetCtcssTone(tone)) },
|
onClick = { onAction(RadioControlAction.SetCtcssTone(tone)) },
|
||||||
label = { Text(String.format(Locale.ENGLISH, "%.1f", 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
|
@Composable
|
||||||
private fun FrequencyTuner(uiState: RadioControlState) {
|
private fun FrequencyTuner(
|
||||||
val freq = uiState.txBaseFrequencyHz ?: return
|
txBaseFrequencyHz: Long,
|
||||||
val transponder = uiState.transponders.find { it.uuid == uiState.selectedTransponderUuid }
|
selectedTransponder: SatRadio?,
|
||||||
|
onAction: (RadioControlAction) -> Unit
|
||||||
|
) {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = "TX Base Frequency", color = MaterialTheme.colorScheme.primary)
|
Text(text = "TX Base Frequency", color = MaterialTheme.colorScheme.primary)
|
||||||
if (transponder != null) {
|
if (selectedTransponder != null) {
|
||||||
val upLow = transponder.uplinkLow
|
val upLow = selectedTransponder.uplinkLow
|
||||||
val upHigh = transponder.uplinkHigh
|
val upHigh = selectedTransponder.uplinkHigh
|
||||||
val dnLow = transponder.downlinkLow
|
val dnLow = selectedTransponder.downlinkLow
|
||||||
val dnHigh = transponder.downlinkHigh
|
val dnHigh = selectedTransponder.downlinkHigh
|
||||||
if (upLow != null && upHigh != null && upLow != upHigh) {
|
if (upLow != null && upHigh != null && upLow != upHigh) {
|
||||||
Text(
|
Text(
|
||||||
text = "UP: ${RadioControlViewModel.formatFrequency(upLow)} - ${RadioControlViewModel.formatFrequency(upHigh)}",
|
text = "UP: ${RadioControlViewModel.formatFrequency(upLow)} - ${RadioControlViewModel.formatFrequency(upHigh)}",
|
||||||
@@ -312,68 +350,48 @@ private fun FrequencyTuner(uiState: RadioControlState) {
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${RadioControlViewModel.formatFrequency(freq)} MHz",
|
text = "${RadioControlViewModel.formatFrequency(txBaseFrequencyHz)} MHz",
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
CardButton(
|
FREQ_ADJUSTMENTS.forEach { (delta, label) ->
|
||||||
onClick = { uiState.sendAction(RadioControlAction.AdjustTxFrequency(-10_000)) },
|
CardButton(
|
||||||
text = "-10k",
|
onClick = { onAction(RadioControlAction.AdjustTxFrequency(delta)) },
|
||||||
modifier = Modifier.weight(1f)
|
text = label,
|
||||||
)
|
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ControlButtons(uiState: RadioControlState) {
|
private fun ControlButtons(
|
||||||
val isConnected = uiState.txPanel.isConnected || uiState.rxPanel.isConnected
|
isConnected: Boolean,
|
||||||
|
isTracking: Boolean,
|
||||||
|
onAction: (RadioControlAction) -> Unit
|
||||||
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { uiState.sendAction(RadioControlAction.ConnectRadios) },
|
onClick = { onAction(RadioControlAction.ConnectRadios) },
|
||||||
text = "Connect",
|
text = "Connect",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { uiState.sendAction(RadioControlAction.DisconnectRadios) },
|
onClick = { onAction(RadioControlAction.DisconnectRadios) },
|
||||||
text = "Disconnect",
|
text = "Disconnect",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { uiState.sendAction(RadioControlAction.ToggleTracking) },
|
onClick = { onAction(RadioControlAction.ToggleTracking) },
|
||||||
text = if (uiState.isTracking) "Stop Tracking" else "Start Tracking",
|
text = if (isTracking) "Stop Tracking" else "Start Tracking",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-10
@@ -42,16 +42,15 @@ data class RadioControlState(
|
|||||||
val txBaseFrequencyHz: Long?,
|
val txBaseFrequencyHz: Long?,
|
||||||
val ctcssTone: Double?,
|
val ctcssTone: Double?,
|
||||||
val isTracking: Boolean,
|
val isTracking: Boolean,
|
||||||
val errorMessage: String?,
|
val errorMessage: String?
|
||||||
val sendAction: (RadioControlAction) -> Unit
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class RadioControlAction {
|
sealed interface RadioControlAction {
|
||||||
data class SelectTransponder(val uuid: String) : RadioControlAction()
|
data class SelectTransponder(val uuid: String) : RadioControlAction
|
||||||
data class SetTxFrequency(val frequencyHz: Long) : RadioControlAction()
|
data class SetTxFrequency(val frequencyHz: Long) : RadioControlAction
|
||||||
data class AdjustTxFrequency(val deltaHz: Long) : RadioControlAction()
|
data class AdjustTxFrequency(val deltaHz: Long) : RadioControlAction
|
||||||
data class SetCtcssTone(val toneHz: Double?) : RadioControlAction()
|
data class SetCtcssTone(val toneHz: Double?) : RadioControlAction
|
||||||
data object ToggleTracking : RadioControlAction()
|
data object ToggleTracking : RadioControlAction
|
||||||
data object ConnectRadios : RadioControlAction()
|
data object ConnectRadios : RadioControlAction
|
||||||
data object DisconnectRadios : 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.IRadioTrackingService
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
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 com.rtbishop.look4sat.core.domain.utility.toTimerString
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
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 kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@@ -46,6 +49,7 @@ class RadioControlViewModel(
|
|||||||
|
|
||||||
private var currentPass: OrbitalPass? = null
|
private var currentPass: OrbitalPass? = null
|
||||||
private var transponders: List<SatRadio> = emptyList()
|
private var transponders: List<SatRadio> = emptyList()
|
||||||
|
private val stationPos = settingsRepo.stationPosition.value
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(
|
private val _uiState = MutableStateFlow(
|
||||||
RadioControlState(
|
RadioControlState(
|
||||||
@@ -62,8 +66,7 @@ class RadioControlViewModel(
|
|||||||
txBaseFrequencyHz = null,
|
txBaseFrequencyHz = null,
|
||||||
ctcssTone = null,
|
ctcssTone = null,
|
||||||
isTracking = false,
|
isTracking = false,
|
||||||
errorMessage = null,
|
errorMessage = null
|
||||||
sendAction = ::handleAction
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val uiState: StateFlow<RadioControlState> = _uiState
|
val uiState: StateFlow<RadioControlState> = _uiState
|
||||||
@@ -72,6 +75,7 @@ class RadioControlViewModel(
|
|||||||
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
||||||
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
||||||
|
|
||||||
|
// Resolve pass and load transponders
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val passes = satelliteRepo.passes.value
|
val passes = satelliteRepo.passes.value
|
||||||
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
||||||
@@ -90,29 +94,30 @@ class RadioControlViewModel(
|
|||||||
it.copy(selectedTransponderUuid = svcState.selectedTransponder?.uuid)
|
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 {
|
viewModelScope.launch {
|
||||||
trackingService.state.collect { svc ->
|
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 ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
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(
|
txPanel = RadioPanelState(
|
||||||
label = "TX (Uplink)",
|
label = "TX (Uplink)",
|
||||||
isConnected = svc.txConnected,
|
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) {
|
when (action) {
|
||||||
is RadioControlAction.SelectTransponder -> {
|
is RadioControlAction.SelectTransponder -> {
|
||||||
val transponder = transponders.find { it.uuid == action.uuid } ?: return
|
val transponder = transponders.find { it.uuid == action.uuid } ?: return
|
||||||
|
|||||||
+9
-9
@@ -28,13 +28,13 @@ data class SatellitesState(
|
|||||||
val typesList: List<String> = emptyList()
|
val typesList: List<String> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class SatellitesAction {
|
sealed interface SatellitesAction {
|
||||||
data object DismissWarning : SatellitesAction()
|
data object DismissWarning : SatellitesAction
|
||||||
data object SaveSelection : SatellitesAction()
|
data object SaveSelection : SatellitesAction
|
||||||
data class SearchFor(val query: String) : SatellitesAction()
|
data class SearchFor(val query: String) : SatellitesAction
|
||||||
data object SelectAll : SatellitesAction()
|
data object SelectAll : SatellitesAction
|
||||||
data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction()
|
data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction
|
||||||
data class SelectTypes(val types: List<String>) : SatellitesAction()
|
data class SelectTypes(val types: List<String>) : SatellitesAction
|
||||||
data object ToggleTypesDialog : SatellitesAction()
|
data object ToggleTypesDialog : SatellitesAction
|
||||||
data object UnselectAll : SatellitesAction()
|
data object UnselectAll : SatellitesAction
|
||||||
}
|
}
|
||||||
+103
-147
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.feature.settings
|
package com.rtbishop.look4sat.feature.settings
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -29,6 +31,7 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -257,14 +260,8 @@ fun NetworkOutputDialog(
|
|||||||
val (rotIp, rotPort) = splitAddress(rotatorAddress.value)
|
val (rotIp, rotPort) = splitAddress(rotatorAddress.value)
|
||||||
val (freqIp, freqPort) = splitAddress(frequencyAddress.value)
|
val (freqIp, freqPort) = splitAddress(frequencyAddress.value)
|
||||||
onSave(
|
onSave(
|
||||||
rotatorState.value,
|
rotatorState.value, rotIp, rotPort, rotatorFormat.value,
|
||||||
rotIp,
|
frequencyState.value, freqIp, freqPort, frequencyFormat.value
|
||||||
rotPort,
|
|
||||||
rotatorFormat.value,
|
|
||||||
frequencyState.value,
|
|
||||||
freqIp,
|
|
||||||
freqPort,
|
|
||||||
frequencyFormat.value
|
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -274,71 +271,29 @@ fun NetworkOutputDialog(
|
|||||||
onAccept = onAccept
|
onAccept = onAccept
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = padding)) {
|
Column(modifier = Modifier.padding(horizontal = padding)) {
|
||||||
Row(
|
OutputChannelSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
switchLabel = stringResource(R.string.prefs_net_rotator_switch),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
enabled = rotatorState.value,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onEnabledChange = { rotatorState.value = it },
|
||||||
) {
|
address = rotatorAddress.value,
|
||||||
Text(stringResource(R.string.prefs_net_rotator_switch))
|
onAddressChange = { rotatorAddress.value = it },
|
||||||
Switch(
|
addressLabel = stringResource(R.string.prefs_net_rotator_address_hint),
|
||||||
checked = rotatorState.value,
|
format = rotatorFormat.value,
|
||||||
onCheckedChange = { rotatorState.value = it }
|
onFormatChange = { rotatorFormat.value = it },
|
||||||
)
|
formatLabel = stringResource(R.string.prefs_net_rotator_format_hint)
|
||||||
}
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Row(
|
OutputChannelSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
switchLabel = stringResource(R.string.prefs_net_frequency_switch),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
enabled = frequencyState.value,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onEnabledChange = { frequencyState.value = it },
|
||||||
) {
|
address = frequencyAddress.value,
|
||||||
Text(stringResource(R.string.prefs_net_frequency_switch))
|
onAddressChange = { frequencyAddress.value = it },
|
||||||
Switch(
|
addressLabel = stringResource(R.string.prefs_net_frequency_address_hint),
|
||||||
checked = frequencyState.value,
|
format = frequencyFormat.value,
|
||||||
onCheckedChange = { frequencyState.value = it }
|
onFormatChange = { frequencyFormat.value = it },
|
||||||
)
|
formatLabel = stringResource(R.string.prefs_net_frequency_format_hint)
|
||||||
}
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,12 +353,8 @@ fun BluetoothOutputDialog(
|
|||||||
val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) }
|
val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) }
|
||||||
val onAccept = {
|
val onAccept = {
|
||||||
onSave(
|
onSave(
|
||||||
rotatorState.value,
|
rotatorState.value, rotatorAddress.value, rotatorFormat.value,
|
||||||
rotatorAddress.value,
|
frequencyState.value, frequencyAddress.value, frequencyFormat.value
|
||||||
rotatorFormat.value,
|
|
||||||
frequencyState.value,
|
|
||||||
frequencyAddress.value,
|
|
||||||
frequencyFormat.value
|
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@@ -413,75 +364,80 @@ fun BluetoothOutputDialog(
|
|||||||
onAccept = onAccept
|
onAccept = onAccept
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = padding)) {
|
Column(modifier = Modifier.padding(horizontal = padding)) {
|
||||||
Row(
|
OutputChannelSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
switchLabel = stringResource(R.string.prefs_bt_rotator_switch),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
enabled = rotatorState.value,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onEnabledChange = { rotatorState.value = it },
|
||||||
) {
|
address = rotatorAddress.value,
|
||||||
Text(stringResource(R.string.prefs_bt_rotator_switch))
|
onAddressChange = { rotatorAddress.value = it },
|
||||||
Switch(
|
addressLabel = stringResource(R.string.prefs_bt_rotator_device_hint),
|
||||||
checked = rotatorState.value,
|
format = rotatorFormat.value,
|
||||||
onCheckedChange = { rotatorState.value = it }
|
onFormatChange = { rotatorFormat.value = it },
|
||||||
)
|
formatLabel = stringResource(R.string.prefs_bt_rotator_output_hint)
|
||||||
}
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Row(
|
OutputChannelSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
switchLabel = stringResource(R.string.prefs_bt_frequency_switch),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
enabled = frequencyState.value,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onEnabledChange = { frequencyState.value = it },
|
||||||
) {
|
address = frequencyAddress.value,
|
||||||
Text(stringResource(R.string.prefs_bt_frequency_switch))
|
onAddressChange = { frequencyAddress.value = it },
|
||||||
Switch(
|
addressLabel = stringResource(R.string.prefs_bt_frequency_device_hint),
|
||||||
checked = frequencyState.value,
|
format = frequencyFormat.value,
|
||||||
onCheckedChange = { frequencyState.value = it }
|
onFormatChange = { frequencyFormat.value = it },
|
||||||
)
|
formatLabel = stringResource(R.string.prefs_bt_frequency_output_hint)
|
||||||
}
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
@Composable
|
||||||
fun RadioControlDialog(
|
fun RadioControlDialog(
|
||||||
initialSettings: RadioControlSettings,
|
initialSettings: RadioControlSettings,
|
||||||
@@ -497,12 +453,12 @@ fun RadioControlDialog(
|
|||||||
val rxAddress = rememberSaveable { mutableStateOf(initialSettings.rxRadioAddress) }
|
val rxAddress = rememberSaveable { mutableStateOf(initialSettings.rxRadioAddress) }
|
||||||
val txName = rememberSaveable { mutableStateOf(initialSettings.txRadioName) }
|
val txName = rememberSaveable { mutableStateOf(initialSettings.txRadioName) }
|
||||||
val rxName = rememberSaveable { mutableStateOf(initialSettings.rxRadioName) }
|
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 selectingFor = rememberSaveable { mutableStateOf("") } // "tx", "rx", or ""
|
||||||
|
|
||||||
val pairedDevices = remember {
|
val pairedDevices = remember {
|
||||||
try {
|
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()
|
manager.adapter?.bondedDevices?.map { Pair(it.name ?: "Unknown", it.address) } ?: emptyList()
|
||||||
} catch (_: SecurityException) {
|
} catch (_: SecurityException) {
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -518,7 +474,7 @@ fun RadioControlDialog(
|
|||||||
rxRadioAddress = rxAddress.value,
|
rxRadioAddress = rxAddress.value,
|
||||||
txRadioName = txName.value,
|
txRadioName = txName.value,
|
||||||
rxRadioName = rxName.value,
|
rxRadioName = rxName.value,
|
||||||
baudRate = baudRate.value
|
baudRate = baudRate.intValue
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -629,8 +585,8 @@ fun RadioControlDialog(
|
|||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
baudRates.forEach { rate ->
|
baudRates.forEach { rate ->
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = { baudRate.value = rate },
|
onClick = { baudRate.intValue = rate },
|
||||||
text = if (rate == baudRate.value) "[$rate]" else rate.toString(),
|
text = if (rate == baudRate.intValue) "[$rate]" else rate.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-160
@@ -42,9 +42,12 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -81,66 +84,44 @@ fun NavGraphBuilder.settingsDestination() {
|
|||||||
modelClass = SettingsViewModel::class.java,
|
modelClass = SettingsViewModel::class.java,
|
||||||
factory = SettingsViewModel.Factory
|
factory = SettingsViewModel.Factory
|
||||||
)
|
)
|
||||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
SettingsScreen(uiState)
|
SettingsScreen(uiState, viewModel::onAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(uiState: SettingsState) {
|
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
|
||||||
val locationContract = ActivityResultContracts.RequestMultiplePermissions()
|
val dialogs = rememberDialogVisibility()
|
||||||
val locationError = stringResource(R.string.prefs_loc_gps_error)
|
val permissions = rememberSettingsPermissions(
|
||||||
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
|
sendAction = onAction,
|
||||||
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
|
onBluetoothGranted = { dialogs.bluetooth = true }
|
||||||
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())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position settings
|
// Dialogs
|
||||||
val posSettings = uiState.positionSettings
|
if (dialogs.position) {
|
||||||
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
|
PositionDialog(
|
||||||
val setGeoPos = { lat: Double, lon: Double ->
|
uiState.positionSettings.stationPos.latitude,
|
||||||
uiState.sendAction(SettingsAction.SetGeoPosition(lat, lon))
|
uiState.positionSettings.stationPos.longitude,
|
||||||
|
dismiss = { dialogs.position = false },
|
||||||
|
save = { lat, lon -> onAction(SettingsAction.SetGeoPosition(lat, lon)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val setQthPos = { locator: String ->
|
if (dialogs.locator) {
|
||||||
uiState.sendAction(SettingsAction.SetQthPosition(locator))
|
LocatorDialog(
|
||||||
|
uiState.positionSettings.stationPos.qthLocator,
|
||||||
|
dismiss = { dialogs.locator = false },
|
||||||
|
save = { onAction(SettingsAction.SetQthPosition(it)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val dismissPos = { uiState.sendAction(SettingsAction.DismissPosMessages) }
|
if (dialogs.dataSources) {
|
||||||
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) {
|
|
||||||
DataSourcesDialog(
|
DataSourcesDialog(
|
||||||
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
|
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
|
||||||
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
|
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
|
||||||
tleUrl = uiState.dataSourcesSettings.tleUrl,
|
tleUrl = uiState.dataSourcesSettings.tleUrl,
|
||||||
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
|
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
|
||||||
onImportTle = { contentRequestForTle.launch("*/*") },
|
onImportTle = { permissions.launchTleImport(); dialogs.dataSources = false },
|
||||||
onImportTransceivers = { contentRequestForTransceivers.launch("*/*") },
|
onImportTransceivers = { permissions.launchTransceiverImport(); dialogs.dataSources = false },
|
||||||
onDismiss = dismissDataSourcesDialog,
|
onDismiss = { dialogs.dataSources = false },
|
||||||
onSave = { useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
|
onSave = { useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
|
||||||
val current = uiState.dataSourcesSettings
|
val current = uiState.dataSourcesSettings
|
||||||
val newSettings = current.copy(
|
val newSettings = current.copy(
|
||||||
@@ -149,112 +130,55 @@ private fun SettingsScreen(uiState: SettingsState) {
|
|||||||
useCustomTransceivers = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) useCustomTransceivers else current.useCustomTransceivers,
|
useCustomTransceivers = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) useCustomTransceivers else current.useCustomTransceivers,
|
||||||
transceiversUrl = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) transceiversUrl else current.transceiversUrl
|
transceiversUrl = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) transceiversUrl else current.transceiversUrl
|
||||||
)
|
)
|
||||||
if (newSettings != current) {
|
if (newSettings != current) onAction(SettingsAction.UpdateDataSources(newSettings))
|
||||||
uiState.sendDataSourcesAction(DataSourcesAction.Update(newSettings))
|
if (useCustomTle || useCustomTransceivers) onAction(SettingsAction.UpdateFromWeb)
|
||||||
}
|
|
||||||
if (useCustomTle || useCustomTransceivers) {
|
|
||||||
uiState.sendAction(SettingsAction.UpdateFromWeb)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (dialogs.network) {
|
||||||
// Data settings
|
NetworkOutputDialog(
|
||||||
val dataSettings = uiState.dataSettings
|
initialSettings = uiState.rcSettings,
|
||||||
val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
|
onDismiss = { dialogs.network = false },
|
||||||
val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
|
onSave = { rotState, rotAddr, rotPort, rotFmt, freqState, freqAddr, freqPort, freqFmt ->
|
||||||
|
onAction(
|
||||||
// RC settings
|
SettingsAction.UpdateRC(
|
||||||
val rcSettings = uiState.rcSettings
|
uiState.rcSettings.copy(
|
||||||
|
rotatorState = rotState, rotatorAddress = rotAddr,
|
||||||
// Radio control (FT-817) settings
|
rotatorPort = rotPort, rotatorFormat = rotFmt,
|
||||||
val radioControlDialogState = rememberSaveable { mutableStateOf(false) }
|
frequencyState = freqState, frequencyAddress = freqAddr,
|
||||||
val showRadioControlDialog = { radioControlDialogState.value = true }
|
frequencyPort = freqPort, frequencyFormat = freqFmt
|
||||||
val dismissRadioControlDialog = { radioControlDialogState.value = false }
|
)
|
||||||
if (radioControlDialogState.value) {
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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(
|
RadioControlDialog(
|
||||||
initialSettings = uiState.radioControlSettings,
|
initialSettings = uiState.radioControlSettings,
|
||||||
onDismiss = dismissRadioControlDialog,
|
onDismiss = { dialogs.radioControl = false },
|
||||||
onSave = { settings ->
|
onSave = { onAction(SettingsAction.UpdateRadioControl(it)) }
|
||||||
uiState.sendRadioControlAction(RadioControlSettingsAction.Update(settings))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network data output
|
// URLs for top bar
|
||||||
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
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val appUrl = stringResource(R.string.prefs_app_url)
|
val appUrl = stringResource(R.string.prefs_app_url)
|
||||||
val donateUrl = stringResource(R.string.prefs_donate_url)
|
val donateUrl = stringResource(R.string.prefs_donate_url)
|
||||||
@@ -302,13 +226,31 @@ private fun SettingsScreen(uiState: SettingsState) {
|
|||||||
modifier = Modifier.clip(MaterialTheme.shapes.medium)
|
modifier = Modifier.clip(MaterialTheme.shapes.medium)
|
||||||
) {
|
) {
|
||||||
item {
|
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) }) {
|
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() }
|
item { CardCredits() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,7 +271,7 @@ private fun LocationCard(
|
|||||||
showPosDialog: () -> Unit,
|
showPosDialog: () -> Unit,
|
||||||
showLocDialog: () -> Unit,
|
showLocDialog: () -> Unit,
|
||||||
dismissPosMessage: () -> Unit,
|
dismissPosMessage: () -> Unit,
|
||||||
sendSystemAction: (SystemAction) -> Unit
|
onAction: (SettingsAction) -> Unit
|
||||||
) {
|
) {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
@@ -344,7 +286,7 @@ private fun LocationCard(
|
|||||||
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
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))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(text = "Lat: ${settings.stationPos.latitude}°")
|
Text(text = "Lat: ${settings.stationPos.latitude}°")
|
||||||
@@ -376,7 +318,7 @@ private fun LocationCard(
|
|||||||
if (settings.messageResId != 0) {
|
if (settings.messageResId != 0) {
|
||||||
val errorString = stringResource(id = settings.messageResId)
|
val errorString = stringResource(id = settings.messageResId)
|
||||||
LaunchedEffect(key1 = settings.messageResId) {
|
LaunchedEffect(key1 = settings.messageResId) {
|
||||||
sendSystemAction(SystemAction.ShowToast(errorString))
|
onAction(SettingsAction.ShowToast(errorString))
|
||||||
dismissPosMessage()
|
dismissPosMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +351,7 @@ private fun DataCard(
|
|||||||
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
UpdateIndicator(isUpdating = settings.isUpdating, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(text = setUpdateTime(updateTime = settings.timestamp))
|
Text(text = formatUpdateTime(updateTime = settings.timestamp))
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(text = stringResource(R.string.prefs_data_entries, settings.entriesTotal))
|
Text(text = stringResource(R.string.prefs_data_entries, settings.entriesTotal))
|
||||||
@@ -491,7 +433,7 @@ private fun OtherCardPreview() = MainTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OtherCard(settings: OtherSettings, sendAction: (SettingsAction) -> Unit) {
|
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -503,16 +445,16 @@ private fun OtherCard(settings: OtherSettings, sendAction: (SettingsAction) -> U
|
|||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
SwitchRow(R.string.prefs_other_switch_utc, settings.stateOfUtc) {
|
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) {
|
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) {
|
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) {
|
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
|
@Composable
|
||||||
private fun setUpdateTime(updateTime: Long): String {
|
private fun formatUpdateTime(updateTime: Long): String {
|
||||||
val timePattern = stringResource(id = R.string.prefs_updated_time)
|
val timePattern = stringResource(id = R.string.prefs_updated_time)
|
||||||
val placeholder = stringResource(id = R.string.pass_time_placeholder)
|
val placeholder = stringResource(id = R.string.pass_time_placeholder)
|
||||||
val updateDate = remember(updateTime) {
|
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 otherSettings: OtherSettings,
|
||||||
val rcSettings: RCSettings,
|
val rcSettings: RCSettings,
|
||||||
val radioControlSettings: RadioControlSettings,
|
val radioControlSettings: RadioControlSettings,
|
||||||
val dataSourcesSettings: DataSourcesSettings,
|
val dataSourcesSettings: DataSourcesSettings
|
||||||
val sendAction: (SettingsAction) -> Unit,
|
|
||||||
val sendRCAction: (RCAction) -> Unit,
|
|
||||||
val sendRadioControlAction: (RadioControlSettingsAction) -> Unit,
|
|
||||||
val sendSystemAction: (SystemAction) -> Unit,
|
|
||||||
val sendDataSourcesAction: (DataSourcesAction) -> Unit
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class SettingsAction {
|
sealed interface SettingsAction {
|
||||||
data object SetGpsPosition : SettingsAction()
|
// Position
|
||||||
data class SetGeoPosition(val latitude: Double, val longitude: Double) : SettingsAction()
|
data object SetGpsPosition : SettingsAction
|
||||||
data class SetQthPosition(val locator: String) : SettingsAction()
|
data class SetGeoPosition(val latitude: Double, val longitude: Double) : SettingsAction
|
||||||
data object DismissPosMessages : SettingsAction()
|
data class SetQthPosition(val locator: String) : SettingsAction
|
||||||
data object UpdateFromWeb : SettingsAction()
|
data object DismissPosMessages : 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 class SystemAction {
|
// Data
|
||||||
data class ShowToast(val message: String) : SystemAction()
|
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 {
|
// Toggles
|
||||||
data class Update(val settings: RCSettings) : RCAction()
|
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 {
|
// Remote control
|
||||||
data class Update(val settings: DataSourcesSettings) : DataSourcesAction()
|
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||||
}
|
data class UpdateRadioControl(val settings: RadioControlSettings) : SettingsAction
|
||||||
|
|
||||||
sealed class RadioControlSettingsAction {
|
// Data sources
|
||||||
data class Update(val settings: RadioControlSettings) : RadioControlSettingsAction()
|
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,
|
otherSettings = settingsRepo.otherSettings.value,
|
||||||
rcSettings = settingsRepo.rcSettings.value,
|
rcSettings = settingsRepo.rcSettings.value,
|
||||||
radioControlSettings = settingsRepo.radioControlSettings.value,
|
radioControlSettings = settingsRepo.radioControlSettings.value,
|
||||||
dataSourcesSettings = settingsRepo.dataSourcesSettings.value,
|
dataSourcesSettings = settingsRepo.dataSourcesSettings.value
|
||||||
sendAction = ::handleAction,
|
|
||||||
sendRCAction = ::handleAction,
|
|
||||||
sendRadioControlAction = ::handleAction,
|
|
||||||
sendSystemAction = ::handleAction,
|
|
||||||
sendDataSourcesAction = ::handleAction
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,12 +99,14 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleAction(action: SettingsAction) {
|
fun onAction(action: SettingsAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
// Position
|
||||||
SettingsAction.SetGpsPosition -> setGpsPosition()
|
SettingsAction.SetGpsPosition -> setGpsPosition()
|
||||||
is SettingsAction.SetGeoPosition -> setGeoPosition(action.latitude, action.longitude)
|
is SettingsAction.SetGeoPosition -> setGeoPosition(action.latitude, action.longitude)
|
||||||
is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
|
is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
|
||||||
SettingsAction.DismissPosMessages -> dismissPosMessage()
|
SettingsAction.DismissPosMessages -> dismissPosMessage()
|
||||||
|
// Data
|
||||||
SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() }
|
SettingsAction.UpdateFromWeb -> runDataUpdate { databaseRepo.updateFromRemote() }
|
||||||
is SettingsAction.UpdateTLEFromFile -> runDataUpdate { databaseRepo.updateTLEFromFile(action.uri) }
|
is SettingsAction.UpdateTLEFromFile -> runDataUpdate { databaseRepo.updateTLEFromFile(action.uri) }
|
||||||
is SettingsAction.UpdateTransceiversFromFile -> runDataUpdate {
|
is SettingsAction.UpdateTransceiversFromFile -> runDataUpdate {
|
||||||
@@ -117,37 +114,19 @@ class SettingsViewModel(
|
|||||||
action.uri
|
action.uri
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() }
|
SettingsAction.ClearAllData -> viewModelScope.launch { databaseRepo.clearAllData() }
|
||||||
|
// Toggles
|
||||||
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
||||||
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
||||||
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
||||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||||
}
|
// Remote control & data sources
|
||||||
}
|
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
||||||
|
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||||
private fun handleAction(action: RCAction) {
|
is SettingsAction.UpdateDataSources -> settingsRepo.updateDataSourcesSettings(action.settings)
|
||||||
when (action) {
|
// System
|
||||||
is RCAction.Update -> settingsRepo.updateRCSettings(action.settings)
|
is SettingsAction.ShowToast -> showToast(action.message)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +167,7 @@ class SettingsViewModel(
|
|||||||
// region Data update helpers — consolidated from 3 near-identical functions
|
// 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.
|
* and resets isUpdating=false on failure.
|
||||||
*/
|
*/
|
||||||
private fun runDataUpdate(block: suspend () -> Unit) = viewModelScope.launch {
|
private fun runDataUpdate(block: suspend () -> Unit) = viewModelScope.launch {
|
||||||
|
|||||||
Reference in new issue
Block a user