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
+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