mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
742 lines
30 KiB
Kotlin
742 lines
30 KiB
Kotlin
package com.geeksville.mesh.ui
|
|
|
|
import android.app.Activity
|
|
import android.content.Intent
|
|
import android.os.Bundle
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.annotation.StringRes
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.wrapContentSize
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material.AlertDialog
|
|
import androidx.compose.material.Button
|
|
import androidx.compose.material.Card
|
|
import androidx.compose.material.ContentAlpha
|
|
import androidx.compose.material.Icon
|
|
import androidx.compose.material.IconButton
|
|
import androidx.compose.material.MaterialTheme
|
|
import androidx.compose.material.Scaffold
|
|
import androidx.compose.material.Text
|
|
import androidx.compose.material.TextButton
|
|
import androidx.compose.material.TopAppBar
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowRight
|
|
import androidx.compose.material.icons.twotone.Warning
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.platform.ComposeView
|
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.os.bundleOf
|
|
import androidx.fragment.app.FragmentManager
|
|
import androidx.fragment.app.viewModels
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import androidx.navigation.NavHostController
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.composable
|
|
import androidx.navigation.compose.rememberNavController
|
|
import com.geeksville.mesh.Position
|
|
import com.geeksville.mesh.R
|
|
import com.geeksville.mesh.android.Logging
|
|
import com.geeksville.mesh.config
|
|
import com.geeksville.mesh.database.entity.NodeEntity
|
|
import com.geeksville.mesh.model.Channel
|
|
import com.geeksville.mesh.model.RadioConfigViewModel
|
|
import com.geeksville.mesh.moduleConfig
|
|
import com.geeksville.mesh.service.MeshService.ConnectionState
|
|
import com.geeksville.mesh.ui.components.PreferenceCategory
|
|
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.AudioConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.BluetoothConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.CannedMessageConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.ChannelSettingsItemList
|
|
import com.geeksville.mesh.ui.components.config.DetectionSensorConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.DeviceConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.DisplayConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
|
|
import com.geeksville.mesh.ui.components.config.ExternalNotificationConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.LoRaConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.MQTTConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.NeighborInfoConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.NetworkConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.PacketResponseStateDialog
|
|
import com.geeksville.mesh.ui.components.config.PaxcounterConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.PositionConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.PowerConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.RangeTestConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.RemoteHardwareConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.SecurityConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.SerialConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.StoreForwardConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.TelemetryConfigItemList
|
|
import com.geeksville.mesh.ui.components.config.UserConfigItemList
|
|
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
|
|
internal fun FragmentManager.navigateToRadioConfig(destNum: Int? = null) {
|
|
val radioConfigFragment = DeviceSettingsFragment().apply {
|
|
arguments = bundleOf("destNum" to destNum)
|
|
}
|
|
beginTransaction()
|
|
.replace(R.id.mainActivityLayout, radioConfigFragment)
|
|
.addToBackStack(null)
|
|
.commit()
|
|
}
|
|
|
|
@AndroidEntryPoint
|
|
class DeviceSettingsFragment : ScreenFragment("Radio Configuration"), Logging {
|
|
|
|
private val model: RadioConfigViewModel by viewModels()
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View {
|
|
val destNum = arguments?.getInt("destNum")
|
|
model.setDestNum(destNum)
|
|
|
|
return ComposeView(requireContext()).apply {
|
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
|
setContent {
|
|
val node by model.destNode.collectAsStateWithLifecycle()
|
|
|
|
AppCompatTheme {
|
|
val navController: NavHostController = rememberNavController()
|
|
// Get current back stack entry
|
|
// val backStackEntry by navController.currentBackStackEntryAsState()
|
|
// Get the name of the current screen
|
|
// val currentScreen = backStackEntry?.destination?.route?.let { route ->
|
|
// NavRoute.entries.find { it.name == route }?.title
|
|
// }
|
|
|
|
Scaffold(
|
|
topBar = {
|
|
MeshAppBar(
|
|
currentScreen = node?.user?.longName
|
|
?: stringResource(R.string.unknown_username),
|
|
// canNavigateBack = navController.previousBackStackEntry != null,
|
|
// navigateUp = { navController.navigateUp() },
|
|
canNavigateBack = true,
|
|
navigateUp = {
|
|
if (navController.previousBackStackEntry != null) {
|
|
navController.navigateUp()
|
|
} else {
|
|
parentFragmentManager.popBackStack()
|
|
}
|
|
},
|
|
)
|
|
}
|
|
) { innerPadding ->
|
|
RadioConfigNavHost(
|
|
node = node,
|
|
viewModel = model,
|
|
navController = navController,
|
|
modifier = Modifier.padding(innerPadding),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum class AdminRoute(@StringRes val title: Int) {
|
|
REBOOT(R.string.reboot),
|
|
SHUTDOWN(R.string.shutdown),
|
|
FACTORY_RESET(R.string.factory_reset),
|
|
NODEDB_RESET(R.string.nodedb_reset),
|
|
}
|
|
|
|
// Config (configType = AdminProtos.AdminMessage.ConfigType)
|
|
enum class ConfigRoute(val title: String, val configType: Int = 0) {
|
|
USER("User"),
|
|
CHANNELS("Channels"),
|
|
DEVICE("Device", 0),
|
|
POSITION("Position", 1),
|
|
POWER("Power", 2),
|
|
NETWORK("Network", 3),
|
|
DISPLAY("Display", 4),
|
|
LORA("LoRa", 5),
|
|
BLUETOOTH("Bluetooth", 6),
|
|
SECURITY("Security", configType = 7),
|
|
}
|
|
|
|
// ModuleConfig (configType = AdminProtos.AdminMessage.ModuleConfigType)
|
|
enum class ModuleRoute(val title: String, val configType: Int = 0) {
|
|
MQTT("MQTT", 0),
|
|
SERIAL("Serial", 1),
|
|
EXTERNAL_NOTIFICATION("External Notification", 2),
|
|
STORE_FORWARD("Store & Forward", 3),
|
|
RANGE_TEST("Range Test", 4),
|
|
TELEMETRY("Telemetry", 5),
|
|
CANNED_MESSAGE("Canned Message", 6),
|
|
AUDIO("Audio", 7),
|
|
REMOTE_HARDWARE("Remote Hardware", 8),
|
|
NEIGHBOR_INFO("Neighbor Info", 9),
|
|
AMBIENT_LIGHTING("Ambient Lighting", 10),
|
|
DETECTION_SENSOR("Detection Sensor", 11),
|
|
PAXCOUNTER("Paxcounter", 12),
|
|
}
|
|
|
|
/**
|
|
* Generic sealed class defines each possible state of a response.
|
|
*/
|
|
sealed class ResponseState<out T> {
|
|
data object Empty : ResponseState<Nothing>()
|
|
data class Loading(var total: Int = 1, var completed: Int = 0) : ResponseState<Nothing>()
|
|
data class Success<T>(val result: T) : ResponseState<T>()
|
|
data class Error(val error: String) : ResponseState<Nothing>()
|
|
|
|
fun isWaiting() = this !is Empty
|
|
}
|
|
|
|
@Composable
|
|
private fun MeshAppBar(
|
|
currentScreen: String,
|
|
canNavigateBack: Boolean,
|
|
navigateUp: () -> Unit,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
TopAppBar(
|
|
title = { Text(currentScreen) },
|
|
modifier = modifier,
|
|
navigationIcon = {
|
|
if (canNavigateBack) {
|
|
IconButton(onClick = navigateUp) {
|
|
Icon(
|
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
null,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
|
@Composable
|
|
fun RadioConfigNavHost(
|
|
node: NodeEntity?,
|
|
viewModel: RadioConfigViewModel = hiltViewModel(),
|
|
navController: NavHostController = rememberNavController(),
|
|
modifier: Modifier,
|
|
) {
|
|
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
|
val connected = connectionState == ConnectionState.CONNECTED && node != null
|
|
|
|
val destNum = node?.num ?: 0
|
|
val isLocal = destNum == viewModel.myNodeNum
|
|
|
|
val radioConfigState by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
|
|
|
val deviceProfile by viewModel.deviceProfile.collectAsStateWithLifecycle()
|
|
val isWaiting = radioConfigState.responseState.isWaiting()
|
|
var showEditDeviceProfileDialog by remember { mutableStateOf(false) }
|
|
|
|
val importConfigLauncher = rememberLauncherForActivityResult(
|
|
ActivityResultContracts.StartActivityForResult()
|
|
) {
|
|
if (it.resultCode == Activity.RESULT_OK) {
|
|
showEditDeviceProfileDialog = true
|
|
it.data?.data?.let { uri -> viewModel.importProfile(uri) }
|
|
}
|
|
}
|
|
|
|
val exportConfigLauncher = rememberLauncherForActivityResult(
|
|
ActivityResultContracts.StartActivityForResult()
|
|
) {
|
|
if (it.resultCode == Activity.RESULT_OK) {
|
|
it.data?.data?.let { uri -> viewModel.exportProfile(uri) }
|
|
}
|
|
}
|
|
|
|
if (showEditDeviceProfileDialog) EditDeviceProfileDialog(
|
|
title = if (deviceProfile != null) "Import configuration" else "Export configuration",
|
|
deviceProfile = deviceProfile ?: viewModel.currentDeviceProfile,
|
|
onConfirm = {
|
|
showEditDeviceProfileDialog = false
|
|
if (deviceProfile != null) {
|
|
viewModel.installProfile(it)
|
|
} else {
|
|
viewModel.setDeviceProfile(it)
|
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "application/*"
|
|
putExtra(Intent.EXTRA_TITLE, "${destNum.toUInt()}.cfg")
|
|
}
|
|
exportConfigLauncher.launch(intent)
|
|
}
|
|
},
|
|
onDismiss = {
|
|
showEditDeviceProfileDialog = false
|
|
viewModel.setDeviceProfile(null)
|
|
}
|
|
)
|
|
|
|
if (isWaiting) PacketResponseStateDialog(
|
|
radioConfigState.responseState,
|
|
onDismiss = {
|
|
showEditDeviceProfileDialog = false
|
|
viewModel.clearPacketResponse()
|
|
},
|
|
onComplete = {
|
|
val route = radioConfigState.route
|
|
if (ConfigRoute.entries.any { it.name == route } ||
|
|
ModuleRoute.entries.any { it.name == route }) {
|
|
navController.navigate(route)
|
|
viewModel.clearPacketResponse()
|
|
}
|
|
}
|
|
)
|
|
|
|
NavHost(
|
|
navController = navController,
|
|
startDestination = "home",
|
|
modifier = modifier,
|
|
) {
|
|
composable("home") {
|
|
RadioConfigItemList(
|
|
enabled = connected && !isWaiting,
|
|
isLocal = isLocal,
|
|
onRouteClick = { route ->
|
|
viewModel.setResponseStateLoading(route)
|
|
},
|
|
onImport = {
|
|
viewModel.clearPacketResponse()
|
|
viewModel.setDeviceProfile(null)
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "application/*"
|
|
}
|
|
importConfigLauncher.launch(intent)
|
|
},
|
|
onExport = {
|
|
viewModel.clearPacketResponse()
|
|
viewModel.setDeviceProfile(null)
|
|
showEditDeviceProfileDialog = true
|
|
},
|
|
)
|
|
}
|
|
composable(ConfigRoute.USER.name) {
|
|
UserConfigItemList(
|
|
userConfig = radioConfigState.userConfig,
|
|
enabled = connected,
|
|
onSaveClicked = { userInput ->
|
|
viewModel.setRemoteOwner(destNum, userInput)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.CHANNELS.name) {
|
|
ChannelSettingsItemList(
|
|
settingsList = radioConfigState.channelList,
|
|
modemPresetName = Channel(loraConfig = radioConfigState.radioConfig.lora).name,
|
|
enabled = connected,
|
|
maxChannels = viewModel.maxChannels,
|
|
onPositiveClicked = { channelListInput ->
|
|
viewModel.updateChannels(destNum, channelListInput, radioConfigState.channelList)
|
|
},
|
|
)
|
|
}
|
|
composable(ConfigRoute.DEVICE.name) {
|
|
DeviceConfigItemList(
|
|
deviceConfig = radioConfigState.radioConfig.device,
|
|
enabled = connected,
|
|
onSaveClicked = { deviceInput ->
|
|
val config = config { device = deviceInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.POSITION.name) {
|
|
val currentPosition = Position(
|
|
latitude = node?.latitude ?: 0.0,
|
|
longitude = node?.longitude ?: 0.0,
|
|
altitude = node?.position?.altitude ?: 0,
|
|
time = 1, // ignore time for fixed_position
|
|
)
|
|
PositionConfigItemList(
|
|
location = currentPosition,
|
|
positionConfig = radioConfigState.radioConfig.position,
|
|
enabled = connected,
|
|
onSaveClicked = { locationInput, positionInput ->
|
|
if (positionInput.fixedPosition) {
|
|
if (locationInput != currentPosition) {
|
|
viewModel.setFixedPosition(destNum, locationInput)
|
|
}
|
|
} else {
|
|
if (radioConfigState.radioConfig.position.fixedPosition) {
|
|
// fixed position changed from enabled to disabled
|
|
viewModel.removeFixedPosition(destNum)
|
|
}
|
|
}
|
|
val config = config { position = positionInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.POWER.name) {
|
|
PowerConfigItemList(
|
|
powerConfig = radioConfigState.radioConfig.power,
|
|
enabled = connected,
|
|
onSaveClicked = { powerInput ->
|
|
val config = config { power = powerInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.NETWORK.name) {
|
|
NetworkConfigItemList(
|
|
networkConfig = radioConfigState.radioConfig.network,
|
|
enabled = connected,
|
|
onSaveClicked = { networkInput ->
|
|
val config = config { network = networkInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.DISPLAY.name) {
|
|
DisplayConfigItemList(
|
|
displayConfig = radioConfigState.radioConfig.display,
|
|
enabled = connected,
|
|
onSaveClicked = { displayInput ->
|
|
val config = config { display = displayInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.LORA.name) {
|
|
LoRaConfigItemList(
|
|
loraConfig = radioConfigState.radioConfig.lora,
|
|
primarySettings = radioConfigState.channelList.getOrNull(0) ?: return@composable,
|
|
enabled = connected,
|
|
onSaveClicked = { loraInput ->
|
|
val config = config { lora = loraInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
},
|
|
hasPaFan = viewModel.hasPaFan,
|
|
)
|
|
}
|
|
composable(ConfigRoute.BLUETOOTH.name) {
|
|
BluetoothConfigItemList(
|
|
bluetoothConfig = radioConfigState.radioConfig.bluetooth,
|
|
enabled = connected,
|
|
onSaveClicked = { bluetoothInput ->
|
|
val config = config { bluetooth = bluetoothInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ConfigRoute.SECURITY.name) {
|
|
SecurityConfigItemList(
|
|
securityConfig = radioConfigState.radioConfig.security,
|
|
enabled = connected,
|
|
onConfirm = { securityInput ->
|
|
val config = config { security = securityInput }
|
|
viewModel.setRemoteConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.MQTT.name) {
|
|
MQTTConfigItemList(
|
|
mqttConfig = radioConfigState.moduleConfig.mqtt,
|
|
enabled = connected,
|
|
onSaveClicked = { mqttInput ->
|
|
val config = moduleConfig { mqtt = mqttInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.SERIAL.name) {
|
|
SerialConfigItemList(
|
|
serialConfig = radioConfigState.moduleConfig.serial,
|
|
enabled = connected,
|
|
onSaveClicked = { serialInput ->
|
|
val config = moduleConfig { serial = serialInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.EXTERNAL_NOTIFICATION.name) {
|
|
ExternalNotificationConfigItemList(
|
|
ringtone = radioConfigState.ringtone,
|
|
extNotificationConfig = radioConfigState.moduleConfig.externalNotification,
|
|
enabled = connected,
|
|
onSaveClicked = { ringtoneInput, extNotificationInput ->
|
|
if (ringtoneInput != radioConfigState.ringtone) {
|
|
viewModel.setRingtone(destNum, ringtoneInput)
|
|
}
|
|
if (extNotificationInput != radioConfigState.moduleConfig.externalNotification) {
|
|
val config = moduleConfig { externalNotification = extNotificationInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.STORE_FORWARD.name) {
|
|
StoreForwardConfigItemList(
|
|
storeForwardConfig = radioConfigState.moduleConfig.storeForward,
|
|
enabled = connected,
|
|
onSaveClicked = { storeForwardInput ->
|
|
val config = moduleConfig { storeForward = storeForwardInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.RANGE_TEST.name) {
|
|
RangeTestConfigItemList(
|
|
rangeTestConfig = radioConfigState.moduleConfig.rangeTest,
|
|
enabled = connected,
|
|
onSaveClicked = { rangeTestInput ->
|
|
val config = moduleConfig { rangeTest = rangeTestInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.TELEMETRY.name) {
|
|
TelemetryConfigItemList(
|
|
telemetryConfig = radioConfigState.moduleConfig.telemetry,
|
|
enabled = connected,
|
|
onSaveClicked = { telemetryInput ->
|
|
val config = moduleConfig { telemetry = telemetryInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.CANNED_MESSAGE.name) {
|
|
CannedMessageConfigItemList(
|
|
messages = radioConfigState.cannedMessageMessages,
|
|
cannedMessageConfig = radioConfigState.moduleConfig.cannedMessage,
|
|
enabled = connected,
|
|
onSaveClicked = { messagesInput, cannedMessageInput ->
|
|
if (messagesInput != radioConfigState.cannedMessageMessages) {
|
|
viewModel.setCannedMessages(destNum, messagesInput)
|
|
}
|
|
if (cannedMessageInput != radioConfigState.moduleConfig.cannedMessage) {
|
|
val config = moduleConfig { cannedMessage = cannedMessageInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.AUDIO.name) {
|
|
AudioConfigItemList(
|
|
audioConfig = radioConfigState.moduleConfig.audio,
|
|
enabled = connected,
|
|
onSaveClicked = { audioInput ->
|
|
val config = moduleConfig { audio = audioInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.REMOTE_HARDWARE.name) {
|
|
RemoteHardwareConfigItemList(
|
|
remoteHardwareConfig = radioConfigState.moduleConfig.remoteHardware,
|
|
enabled = connected,
|
|
onSaveClicked = { remoteHardwareInput ->
|
|
val config = moduleConfig { remoteHardware = remoteHardwareInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.NEIGHBOR_INFO.name) {
|
|
NeighborInfoConfigItemList(
|
|
neighborInfoConfig = radioConfigState.moduleConfig.neighborInfo,
|
|
enabled = connected,
|
|
onSaveClicked = { neighborInfoInput ->
|
|
val config = moduleConfig { neighborInfo = neighborInfoInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.AMBIENT_LIGHTING.name) {
|
|
AmbientLightingConfigItemList(
|
|
ambientLightingConfig = radioConfigState.moduleConfig.ambientLighting,
|
|
enabled = connected,
|
|
onSaveClicked = { ambientLightingInput ->
|
|
val config = moduleConfig { ambientLighting = ambientLightingInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.DETECTION_SENSOR.name) {
|
|
DetectionSensorConfigItemList(
|
|
detectionSensorConfig = radioConfigState.moduleConfig.detectionSensor,
|
|
enabled = connected,
|
|
onSaveClicked = { detectionSensorInput ->
|
|
val config = moduleConfig { detectionSensor = detectionSensorInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
composable(ModuleRoute.PAXCOUNTER.name) {
|
|
PaxcounterConfigItemList(
|
|
paxcounterConfig = radioConfigState.moduleConfig.paxcounter,
|
|
enabled = connected,
|
|
onSaveClicked = { paxcounterConfigInput ->
|
|
val config = moduleConfig { paxcounter = paxcounterConfigInput }
|
|
viewModel.setModuleConfig(destNum, config)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun NavCard(
|
|
title: String,
|
|
enabled: Boolean,
|
|
onClick: () -> Unit
|
|
) {
|
|
val color = if (enabled) {
|
|
MaterialTheme.colors.onSurface
|
|
} else {
|
|
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
|
|
}
|
|
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 2.dp)
|
|
.clickable(enabled = enabled) { onClick() },
|
|
elevation = 4.dp
|
|
) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 12.dp)
|
|
) {
|
|
Text(
|
|
text = title,
|
|
style = MaterialTheme.typography.body1,
|
|
color = color,
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
Icon(
|
|
Icons.AutoMirrored.TwoTone.KeyboardArrowRight, "trailingIcon",
|
|
modifier = Modifier.wrapContentSize(),
|
|
tint = color,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) {
|
|
var showDialog by remember { mutableStateOf(false) }
|
|
if (showDialog) AlertDialog(
|
|
onDismissRequest = {},
|
|
shape = RoundedCornerShape(16.dp),
|
|
backgroundColor = MaterialTheme.colors.background,
|
|
title = {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.Center,
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.TwoTone.Warning,
|
|
contentDescription = "warning",
|
|
modifier = Modifier.padding(end = 8.dp)
|
|
)
|
|
Text(
|
|
text = "${stringResource(title)}?\n")
|
|
Icon(
|
|
imageVector = Icons.TwoTone.Warning,
|
|
contentDescription = "warning",
|
|
modifier = Modifier.padding(start = 8.dp)
|
|
)
|
|
}
|
|
},
|
|
buttons = {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
) {
|
|
TextButton(
|
|
modifier = Modifier.weight(1f),
|
|
onClick = { showDialog = false },
|
|
) { Text(stringResource(R.string.cancel)) }
|
|
Button(
|
|
modifier = Modifier.weight(1f),
|
|
onClick = {
|
|
showDialog = false
|
|
onClick()
|
|
},
|
|
) { Text(stringResource(R.string.send)) }
|
|
}
|
|
}
|
|
)
|
|
|
|
Column {
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Button(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(48.dp),
|
|
enabled = enabled,
|
|
onClick = { showDialog = true },
|
|
) { Text(text = stringResource(title)) }
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RadioConfigItemList(
|
|
enabled: Boolean = true,
|
|
isLocal: Boolean = true,
|
|
modifier: Modifier = Modifier,
|
|
onRouteClick: (Enum<*>) -> Unit = {},
|
|
onImport: () -> Unit = {},
|
|
onExport: () -> Unit = {},
|
|
) {
|
|
LazyColumn(
|
|
modifier = modifier,
|
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
) {
|
|
item { PreferenceCategory(stringResource(R.string.device_settings)) }
|
|
items(ConfigRoute.entries) { NavCard(it.title, enabled = enabled) { onRouteClick(it) } }
|
|
|
|
item { PreferenceCategory(stringResource(R.string.module_settings)) }
|
|
items(ModuleRoute.entries) { NavCard(it.title, enabled = enabled) { onRouteClick(it) } }
|
|
|
|
if (isLocal) {
|
|
item { PreferenceCategory("Import / Export") }
|
|
item { NavCard("Import configuration", enabled = enabled) { onImport() } }
|
|
item { NavCard("Export configuration", enabled = enabled) { onExport() } }
|
|
}
|
|
|
|
items(AdminRoute.entries) { NavButton(it.title, enabled) { onRouteClick(it) } }
|
|
}
|
|
}
|
|
|
|
@Preview(showBackground = true)
|
|
@Composable
|
|
private fun RadioSettingsScreenPreview() {
|
|
RadioConfigItemList()
|
|
}
|