refactor: move RadioConfig files to separate package

This commit is contained in:
andrekir 2025-01-09 20:01:21 -03:00 committed by Andre K
parent 7794c08190
commit ad9a3a5e49
39 changed files with 501 additions and 357 deletions

View file

@ -104,12 +104,12 @@ import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.components.AdaptiveTwoPane
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.config.ChannelCard
import com.geeksville.mesh.ui.components.config.ChannelSelection
import com.geeksville.mesh.ui.components.config.EditChannelDialog
import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState
import com.geeksville.mesh.ui.radioconfig.components.ChannelCard
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
import com.geeksville.mesh.ui.radioconfig.components.EditChannelDialog
import com.geeksville.mesh.ui.theme.AppTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.journeyapps.barcodescanner.ScanContract

View file

@ -21,36 +21,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.DataUsage
import androidx.compose.material.icons.filled.DisplaySettings
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material.icons.filled.SettingsRemote
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
@ -64,11 +38,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.navigation.addRadioConfigSection
import com.geeksville.mesh.ui.components.BaseScaffold
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
@ -76,31 +49,8 @@ import com.geeksville.mesh.ui.components.NodeMapScreen
import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.components.config.AudioConfigScreen
import com.geeksville.mesh.ui.components.config.BluetoothConfigScreen
import com.geeksville.mesh.ui.components.config.CannedMessageConfigScreen
import com.geeksville.mesh.ui.components.config.ChannelConfigScreen
import com.geeksville.mesh.ui.components.config.DetectionSensorConfigScreen
import com.geeksville.mesh.ui.components.config.DeviceConfigScreen
import com.geeksville.mesh.ui.components.config.DisplayConfigScreen
import com.geeksville.mesh.ui.components.config.ExternalNotificationConfigScreen
import com.geeksville.mesh.ui.components.config.LoRaConfigScreen
import com.geeksville.mesh.ui.components.config.MQTTConfigScreen
import com.geeksville.mesh.ui.components.config.NeighborInfoConfigScreen
import com.geeksville.mesh.ui.components.config.NetworkConfigScreen
import com.geeksville.mesh.ui.components.config.PaxcounterConfigScreen
import com.geeksville.mesh.ui.components.config.PositionConfigScreen
import com.geeksville.mesh.ui.components.config.PowerConfigScreen
import com.geeksville.mesh.ui.components.config.RangeTestConfigScreen
import com.geeksville.mesh.ui.components.config.RemoteHardwareConfigScreen
import com.geeksville.mesh.ui.components.config.SecurityConfigScreen
import com.geeksville.mesh.ui.components.config.SerialConfigScreen
import com.geeksville.mesh.ui.components.config.StoreForwardConfigScreen
import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen
import com.geeksville.mesh.ui.components.config.UserConfigScreen
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.UiText
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.serialization.Serializable
@ -164,13 +114,6 @@ class NavGraphFragment : ScreenFragment("NavGraph"), Logging {
}
}
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),
}
sealed interface Route {
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@ -180,7 +123,7 @@ sealed interface Route {
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@Serializable data object User : Route
@Serializable data object Channels : Route
@Serializable data object ChannelConfig : Route
@Serializable data object Device : Route
@Serializable data object Position : Route
@Serializable data object Power : Route
@ -214,74 +157,6 @@ sealed interface Route {
@Serializable data object TracerouteLog : Route
}
// Config (type = AdminProtos.AdminMessage.ConfigType)
enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
USER("User", Route.User, Icons.Default.Person, 0),
CHANNELS("Channels", Route.Channels, Icons.AutoMirrored.Default.List, 0),
DEVICE("Device", Route.Device, Icons.Default.Router, 0),
POSITION("Position", Route.Position, Icons.Default.LocationOn, 1),
POWER("Power", Route.Power, Icons.Default.Power, 2),
NETWORK("Network", Route.Network, Icons.Default.Wifi, 3),
DISPLAY("Display", Route.Display, Icons.Default.DisplaySettings, 4),
LORA("LoRa", Route.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY("Security", Route.Security, Icons.Default.Security, type = 7),
;
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
when {
metadata == null -> true
it == BLUETOOTH -> metadata.hasBluetooth
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
else -> true // Include all other routes by default
}
}
}
}
// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType)
enum class ModuleRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
MQTT("MQTT", Route.MQTT, Icons.Default.Cloud, 0),
SERIAL("Serial", Route.Serial, Icons.Default.Usb, 1),
EXT_NOTIFICATION("External Notification", Route.ExtNotification, Icons.Default.Notifications, 2),
STORE_FORWARD("Store & Forward", Route.StoreForward, Icons.AutoMirrored.Default.Forward, 3),
RANGE_TEST("Range Test", Route.RangeTest, Icons.Default.Speed, 4),
TELEMETRY("Telemetry", Route.Telemetry, Icons.Default.DataUsage, 5),
CANNED_MESSAGE("Canned Message", Route.CannedMessage, Icons.AutoMirrored.Default.Message, 6),
AUDIO("Audio", Route.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
REMOTE_HARDWARE("Remote Hardware", Route.RemoteHardware, Icons.Default.SettingsRemote, 8),
NEIGHBOR_INFO("Neighbor Info", Route.NeighborInfo, Icons.Default.People, 9),
AMBIENT_LIGHTING("Ambient Lighting", Route.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER("Paxcounter", Route.Paxcounter, Icons.Default.PermScanWifi, 12),
;
val bitfield: Int get() = 1 shl ordinal
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> = entries.filter {
when (metadata) {
null -> true
else -> metadata.excludedModules and it.bitfield == 0
}
}
}
}
/**
* 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: UiText) : ResponseState<Nothing>()
fun isWaiting() = this !is Empty
}
@Suppress("LongMethod")
@Composable
fun NavGraph(
navController: NavHostController = rememberNavController(),
@ -320,101 +195,7 @@ fun NavGraph(
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
TracerouteLogScreen(hiltViewModel<MetricsViewModel>(parentEntry))
}
composable<Route.RadioConfig> {
RadioConfigScreen { navController.navigate(route = it) }
}
composable<Route.User> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Channels> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Device> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DeviceConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Position> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PositionConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Power> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PowerConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Network> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NetworkConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Display> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DisplayConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.LoRa> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
LoRaConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Bluetooth> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
BluetoothConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Security> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SecurityConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.MQTT> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
MQTTConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Serial> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
SerialConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.ExtNotification> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ExternalNotificationConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.StoreForward> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
StoreForwardConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.RangeTest> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RangeTestConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Telemetry> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
TelemetryConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.CannedMessage> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
CannedMessageConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Audio> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AudioConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.RemoteHardware> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
RemoteHardwareConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.NeighborInfo> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
NeighborInfoConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.AmbientLighting> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
AmbientLightingConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.DetectionSensor> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
DetectionSensorConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Paxcounter> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PaxcounterConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
addRadioConfigSection(navController)
composable<Route.Share> { backStackEntry ->
val message = backStackEntry.toRoute<Route.Share>().message
ShareScreen(

View file

@ -97,6 +97,7 @@ import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.radioconfig.NavCard
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.DistanceUnit
import com.geeksville.mesh.util.formatAgo

View file

@ -54,7 +54,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.config.ChannelSelection
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
/**
* Enables the user to select which channels to accept after scanning a QR code.

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
import androidx.annotation.StringRes
import com.geeksville.mesh.R
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),
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.DisplaySettings
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.ui.graphics.vector.ImageVector
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.ui.Route
@Suppress("MagicNumber")
// Config (type = AdminProtos.AdminMessage.ConfigType)
enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
USER("User", Route.User, Icons.Default.Person, 0),
CHANNELS("Channels", Route.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE("Device", Route.Device, Icons.Default.Router, 0),
POSITION("Position", Route.Position, Icons.Default.LocationOn, 1),
POWER("Power", Route.Power, Icons.Default.Power, 2),
NETWORK("Network", Route.Network, Icons.Default.Wifi, 3),
DISPLAY("Display", Route.Display, Icons.Default.DisplaySettings, 4),
LORA("LoRa", Route.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY("Security", Route.Security, Icons.Default.Security, 7),
;
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
when {
metadata == null -> true
it == BLUETOOTH -> metadata.hasBluetooth
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
else -> true // Include all other routes by default
}
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.DataUsage
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material.icons.filled.SettingsRemote
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb
import androidx.compose.ui.graphics.vector.ImageVector
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.ui.Route
@Suppress("MagicNumber")
// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType)
enum class ModuleRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
MQTT("MQTT", Route.MQTT, Icons.Default.Cloud, 0),
SERIAL("Serial", Route.Serial, Icons.Default.Usb, 1),
EXT_NOTIFICATION("External Notification", Route.ExtNotification, Icons.Default.Notifications, 2),
STORE_FORWARD("Store & Forward", Route.StoreForward, Icons.AutoMirrored.Default.Forward, 3),
RANGE_TEST("Range Test", Route.RangeTest, Icons.Default.Speed, 4),
TELEMETRY("Telemetry", Route.Telemetry, Icons.Default.DataUsage, 5),
CANNED_MESSAGE("Canned Message", Route.CannedMessage, Icons.AutoMirrored.Default.Message, 6),
AUDIO("Audio", Route.Audio, Icons.AutoMirrored.Default.VolumeUp, 7),
REMOTE_HARDWARE("Remote Hardware", Route.RemoteHardware, Icons.Default.SettingsRemote, 8),
NEIGHBOR_INFO("Neighbor Info", Route.NeighborInfo, Icons.Default.People, 9),
AMBIENT_LIGHTING("Ambient Lighting", Route.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER("Paxcounter", Route.Paxcounter, Icons.Default.PermScanWifi, 12),
;
val bitfield: Int get() = 1 shl ordinal
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> = entries.filter {
when (metadata) {
null -> true
else -> metadata.excludedModules and it.bitfield == 0
}
}
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui
package com.geeksville.mesh.ui.radioconfig
import android.app.Activity
import android.content.Intent
@ -65,13 +65,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.R
import com.geeksville.mesh.model.RadioConfigState
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.Route
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
import com.geeksville.mesh.ui.components.config.PacketResponseStateDialog
import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
private fun getNavRouteFrom(routeName: String): Any? {
private fun getNavRouteFrom(routeName: String): Route? {
return ConfigRoute.entries.find { it.name == routeName }?.route
?: ModuleRoute.entries.find { it.name == routeName }?.route
}
@ -81,7 +80,7 @@ private fun getNavRouteFrom(routeName: String): Any? {
fun RadioConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
onNavigate: (Any) -> Unit = {}
onNavigate: (Route) -> Unit = {}
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
var isWaiting by remember { mutableStateOf(false) }
@ -222,54 +221,57 @@ fun NavCard(
}
}
@Suppress("LongMethod")
@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)
)
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)) }
}
}
},
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))

View file

@ -0,0 +1,619 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
import android.app.Application
import android.net.Uri
import android.os.RemoteException
import androidx.annotation.StringRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.Portnums
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.MyNodeEntity
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.getChannelList
import com.geeksville.mesh.model.getStringResFrom
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.MeshService.ConnectionState
import com.geeksville.mesh.ui.Route
import com.geeksville.mesh.util.UiText
import com.google.protobuf.MessageLite
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import javax.inject.Inject
/**
* Data class that represents the current RadioConfig state.
*/
data class RadioConfigState(
val isLocal: Boolean = false,
val connected: Boolean = false,
val route: String = "",
val metadata: MeshProtos.DeviceMetadata? = null,
val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
val channelList: List<ChannelProtos.ChannelSettings> = emptyList(),
val radioConfig: ConfigProtos.Config = config {},
val moduleConfig: ModuleConfigProtos.ModuleConfig = moduleConfig {},
val ringtone: String = "",
val cannedMessageMessages: String = "",
val responseState: ResponseState<Boolean> = ResponseState.Empty,
)
@HiltViewModel
class RadioConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val app: Application,
private val radioConfigRepository: RadioConfigRepository,
) : ViewModel(), Logging {
private val meshService: IMeshService? get() = radioConfigRepository.meshService
private val destNum = savedStateHandle.toRoute<Route.RadioConfig>().destNum
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?> get() = _destNode
private val requestIds = MutableStateFlow(hashSetOf<Int>())
private val _radioConfigState = MutableStateFlow(RadioConfigState())
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
private val _currentDeviceProfile = MutableStateFlow(deviceProfile {})
val currentDeviceProfile get() = _currentDeviceProfile.value
init {
@OptIn(ExperimentalCoroutinesApi::class)
radioConfigRepository.nodeDBbyNum
.mapLatest { nodes -> nodes[destNum] ?: nodes.values.firstOrNull() }
.distinctUntilChanged()
.onEach {
_destNode.value = it
_radioConfigState.update { state -> state.copy(metadata = it?.metadata) }
}
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach {
_currentDeviceProfile.value = it
}.launchIn(viewModelScope)
radioConfigRepository.meshPacketFlow.onEach(::processPacketResponse)
.launchIn(viewModelScope)
combine(radioConfigRepository.connectionState, radioConfigState) { connState, configState ->
_radioConfigState.update { it.copy(connected = connState == ConnectionState.CONNECTED) }
if (connState.isDisconnected() && configState.responseState.isWaiting()) {
sendError(R.string.disconnected)
}
}.launchIn(viewModelScope)
radioConfigRepository.myNodeInfo.onEach { ni ->
_radioConfigState.update { it.copy(isLocal = destNum == null || destNum == ni?.myNodeNum) }
}.launchIn(viewModelScope)
debug("RadioConfigViewModel created")
}
private val myNodeInfo: StateFlow<MyNodeEntity?> get() = radioConfigRepository.myNodeInfo
val myNodeNum get() = myNodeInfo.value?.myNodeNum
val maxChannels get() = myNodeInfo.value?.maxChannels ?: 8
val hasPaFan: Boolean
get() = destNode.value?.user?.hwModel in setOf(
null,
MeshProtos.HardwareModel.UNRECOGNIZED,
MeshProtos.HardwareModel.UNSET,
MeshProtos.HardwareModel.BETAFPV_2400_TX,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT_NANO,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT,
)
override fun onCleared() {
super.onCleared()
debug("RadioConfigViewModel cleared")
}
private fun request(
destNum: Int,
requestAction: suspend (IMeshService, Int, Int) -> Unit,
errorMessage: String,
) = viewModelScope.launch {
meshService?.let { service ->
val packetId = service.packetId
try {
requestAction(service, packetId, destNum)
requestIds.update { it.apply { add(packetId) } }
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
val total = maxOf(requestIds.value.size, state.responseState.total)
state.copy(responseState = state.responseState.copy(total = total))
} else {
state.copy(
route = "", // setter (response is PortNum.ROUTING_APP)
responseState = ResponseState.Loading(),
)
}
}
} catch (ex: RemoteException) {
errormsg("$errorMessage: ${ex.message}")
}
}
}
fun setOwner(user: MeshProtos.User) {
setRemoteOwner(destNode.value?.num ?: return, user)
}
private fun setRemoteOwner(destNum: Int, user: MeshProtos.User) = request(
destNum,
{ service, packetId, _ ->
_radioConfigState.update { it.copy(userConfig = user) }
service.setRemoteOwner(packetId, user.toByteArray())
},
"Request setOwner error",
)
private fun getOwner(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
"Request getOwner error"
)
fun updateChannels(
new: List<ChannelProtos.ChannelSettings>,
old: List<ChannelProtos.ChannelSettings>,
) {
val destNum = destNode.value?.num ?: return
getChannelList(new, old).forEach { setRemoteChannel(destNum, it) }
if (destNum == myNodeNum) viewModelScope.launch {
radioConfigRepository.replaceAllSettings(new)
}
_radioConfigState.update { it.copy(channelList = new) }
}
private fun setChannels(channelUrl: String) = viewModelScope.launch {
val new = Uri.parse(channelUrl).toChannelSet()
val old = radioConfigRepository.channelSetFlow.firstOrNull() ?: return@launch
updateChannels(new.settingsList, old.settingsList)
}
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) = request(
destNum,
{ service, packetId, dest ->
service.setRemoteChannel(packetId, dest, channel.toByteArray())
},
"Request setRemoteChannel error"
)
private fun getChannel(destNum: Int, index: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteChannel(packetId, dest, index) },
"Request getChannel error"
)
fun setConfig(config: ConfigProtos.Config) {
setRemoteConfig(destNode.value?.num ?: return, config)
}
private fun setRemoteConfig(destNum: Int, config: ConfigProtos.Config) = request(
destNum,
{ service, packetId, dest ->
_radioConfigState.update { it.copy(radioConfig = config) }
service.setRemoteConfig(packetId, dest, config.toByteArray())
},
"Request setConfig error",
)
private fun getConfig(destNum: Int, configType: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteConfig(packetId, dest, configType) },
"Request getConfig error",
)
fun setModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
setModuleConfig(destNode.value?.num ?: return, config)
}
private fun setModuleConfig(destNum: Int, config: ModuleConfigProtos.ModuleConfig) = request(
destNum,
{ service, packetId, dest ->
_radioConfigState.update { it.copy(moduleConfig = config) }
service.setModuleConfig(packetId, dest, config.toByteArray())
},
"Request setConfig error",
)
private fun getModuleConfig(destNum: Int, configType: Int) = request(
destNum,
{ service, packetId, dest -> service.getModuleConfig(packetId, dest, configType) },
"Request getModuleConfig error",
)
fun setRingtone(ringtone: String) {
val destNum = destNode.value?.num ?: return
_radioConfigState.update { it.copy(ringtone = ringtone) }
meshService?.setRingtone(destNum, ringtone)
}
private fun getRingtone(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getRingtone(packetId, dest) },
"Request getRingtone error"
)
fun setCannedMessages(messages: String) {
val destNum = destNode.value?.num ?: return
_radioConfigState.update { it.copy(cannedMessageMessages = messages) }
meshService?.setCannedMessages(destNum, messages)
}
private fun getCannedMessages(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getCannedMessages(packetId, dest) },
"Request getCannedMessages error"
)
private fun requestShutdown(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestShutdown(packetId, dest) },
"Request shutdown error"
)
private fun requestReboot(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestReboot(packetId, dest) },
"Request reboot error"
)
private fun requestFactoryReset(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestFactoryReset(packetId, dest) },
"Request factory reset error"
)
private fun requestNodedbReset(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestNodedbReset(packetId, dest) },
"Request NodeDB reset error"
)
private fun sendAdminRequest(destNum: Int) {
val route = radioConfigState.value.route
_radioConfigState.update { it.copy(route = "") } // setter (response is PortNum.ROUTING_APP)
when (route) {
AdminRoute.REBOOT.name -> requestReboot(destNum)
AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) {
if (metadata != null && !metadata.canShutdown) {
sendError(R.string.cant_shutdown)
} else {
requestShutdown(destNum)
}
}
AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum)
AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum)
}
}
fun setFixedPosition(position: Position) {
val destNum = destNode.value?.num ?: return
try {
meshService?.setFixedPosition(destNum, position)
} catch (ex: RemoteException) {
errormsg("Set fixed position error: ${ex.message}")
}
}
fun removeFixedPosition() = setFixedPosition(Position(0.0, 0.0, 0))
fun importProfile(
uri: Uri,
onResult: (DeviceProfile) -> Unit,
) = viewModelScope.launch(Dispatchers.IO) {
try {
app.contentResolver.openInputStream(uri).use { inputStream ->
val bytes = inputStream?.readBytes()
val protobuf = DeviceProfile.parseFrom(bytes)
onResult(protobuf)
}
} catch (ex: Exception) {
errormsg("Import DeviceProfile error: ${ex.message}")
sendError(ex.customMessage)
}
}
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch {
writeToUri(uri, profile)
}
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
try {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
message.writeTo(outputStream)
}
}
setResponseStateSuccess()
} catch (ex: Exception) {
errormsg("Can't write file error: ${ex.message}")
sendError(ex.customMessage)
}
}
fun installProfile(protobuf: DeviceProfile) = with(protobuf) {
meshService?.beginEditSettings()
if (hasLongName() || hasShortName()) {
destNode.value?.user?.let {
val user = MeshProtos.User.newBuilder()
.setId(it.id)
.setLongName(if (hasLongName()) longName else it.longName)
.setShortName(if (hasShortName()) shortName else it.shortName)
.setIsLicensed(it.isLicensed)
.build()
setOwner(user)
}
}
if (hasChannelUrl()) {
try {
setChannels(channelUrl)
} catch (ex: Exception) {
errormsg("DeviceProfile channel import error", ex)
sendError(ex.customMessage)
}
}
if (hasConfig()) {
val descriptor = ConfigProtos.Config.getDescriptor()
config.allFields.forEach { (field, value) ->
val newConfig = ConfigProtos.Config.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
setConfig(newConfig)
}
}
if (hasFixedPosition()) {
setFixedPosition(Position(fixedPosition))
}
if (hasModuleConfig()) {
val descriptor = ModuleConfigProtos.ModuleConfig.getDescriptor()
moduleConfig.allFields.forEach { (field, value) ->
val newConfig = ModuleConfigProtos.ModuleConfig.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
setModuleConfig(newConfig)
}
}
meshService?.commitEditSettings()
}
fun clearPacketResponse() {
requestIds.value = hashSetOf()
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
}
fun setResponseStateLoading(route: Enum<*>) {
val destNum = destNode.value?.num ?: return
_radioConfigState.update {
RadioConfigState(
isLocal = it.isLocal,
connected = it.connected,
route = route.name,
metadata = it.metadata,
responseState = ResponseState.Loading(),
)
}
when (route) {
ConfigRoute.USER -> getOwner(destNum)
ConfigRoute.CHANNELS -> {
getChannel(destNum, 0)
getConfig(destNum, ConfigRoute.LORA.type)
// channel editor is synchronous, so we don't use requestIds as total
setResponseStateTotal(maxChannels + 1)
}
is AdminRoute -> {
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE)
setResponseStateTotal(2)
}
is ConfigRoute -> {
if (route == ConfigRoute.LORA) {
getChannel(destNum, 0)
}
getConfig(destNum, route.type)
}
is ModuleRoute -> {
if (route == ModuleRoute.CANNED_MESSAGE) {
getCannedMessages(destNum)
}
if (route == ModuleRoute.EXT_NOTIFICATION) {
getRingtone(destNum)
}
getModuleConfig(destNum, route.type)
}
}
}
private fun setResponseStateTotal(total: Int) {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
state.copy(responseState = state.responseState.copy(total = total))
} else {
state // Return the unchanged state for other response states
}
}
}
private fun setResponseStateSuccess() {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
state.copy(responseState = ResponseState.Success(true))
} else {
state // Return the unchanged state for other response states
}
}
}
private val Exception.customMessage: String get() = "${javaClass.simpleName}: $message"
private fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
private fun sendError(@StringRes id: Int) = setResponseStateError(UiText.StringResource(id))
private fun setResponseStateError(error: UiText) {
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
}
private fun incrementCompleted() {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
val increment = state.responseState.completed + 1
state.copy(responseState = state.responseState.copy(completed = increment))
} else {
state // Return the unchanged state for other response states
}
}
}
private fun processPacketResponse(packet: MeshProtos.MeshPacket) {
val data = packet.decoded
if (data.requestId !in requestIds.value) return
val route = radioConfigState.value.route
val destNum = destNode.value?.num ?: return
val debugMsg = "requestId: ${data.requestId.toUInt()} to: ${destNum.toUInt()} received %s"
if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) {
val parsed = MeshProtos.Routing.parseFrom(data.payload)
debug(debugMsg.format(parsed.errorReason.name))
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
sendError(getStringResFrom(parsed.errorReasonValue))
} else if (packet.from == destNum && route.isEmpty()) {
requestIds.update { it.apply { remove(data.requestId) } }
if (requestIds.value.isEmpty()) {
setResponseStateSuccess()
} else {
incrementCompleted()
}
}
}
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
debug(debugMsg.format(parsed.payloadVariantCase.name))
if (destNum != packet.from) {
sendError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
return
}
when (parsed.payloadVariantCase) {
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
_radioConfigState.update { it.copy(metadata = parsed.getDeviceMetadataResponse) }
incrementCompleted()
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
val response = parsed.getChannelResponse
// Stop once we get to the first disabled entry
if (response.role != ChannelProtos.Channel.Role.DISABLED) {
_radioConfigState.update { state ->
state.copy(channelList = state.channelList.toMutableList().apply {
add(response.index, response.settings)
})
}
incrementCompleted()
if (response.index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
// Not done yet, request next channel
getChannel(destNum, response.index + 1)
}
} else {
// Received last channel, update total and start channel editor
setResponseStateTotal(response.index + 1)
}
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> {
_radioConfigState.update { it.copy(userConfig = parsed.getOwnerResponse) }
incrementCompleted()
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
val response = parsed.getConfigResponse
if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET
sendError(response.payloadVariantCase.name)
}
_radioConfigState.update { it.copy(radioConfig = response) }
incrementCompleted()
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> {
val response = parsed.getModuleConfigResponse
if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET
sendError(response.payloadVariantCase.name)
}
_radioConfigState.update { it.copy(moduleConfig = response) }
incrementCompleted()
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> {
_radioConfigState.update {
it.copy(cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse)
}
incrementCompleted()
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> {
_radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) }
incrementCompleted()
}
else -> debug("No custom processing needed for ${parsed.payloadVariantCase}")
}
if (AdminRoute.entries.any { it.name == route }) {
sendAdminRequest(destNum)
}
requestIds.update { it.apply { remove(data.requestId) } }
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
import com.geeksville.mesh.util.UiText
/**
* 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: UiText) : ResponseState<Nothing>()
fun isWaiting() = this !is Empty
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun AmbientLightingConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,13 +33,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun AudioConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -34,12 +34,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun BluetoothConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -36,13 +36,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun CannedMessageConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
@ -70,12 +70,12 @@ import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -36,13 +36,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun DetectionSensorConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -39,12 +39,12 @@ import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
private val DeviceConfig.Role.stringRes: Int
get() = when (this) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -34,12 +34,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun DisplayConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -36,13 +36,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.components.TextDividerPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun ExternalNotificationConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -37,7 +37,6 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.model.RegionInfo
import com.geeksville.mesh.model.numChannels
import com.geeksville.mesh.ui.components.DropDownPreference
@ -46,6 +45,7 @@ import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun LoRaConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -38,7 +38,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditPasswordPreference
import com.geeksville.mesh.ui.components.EditTextPreference
@ -46,6 +45,7 @@ import com.geeksville.mesh.ui.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun MQTTConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -35,12 +35,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun NeighborInfoConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
@ -46,7 +46,6 @@ import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditIPv4Preference
import com.geeksville.mesh.ui.components.EditPasswordPreference
@ -55,6 +54,7 @@ import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SimpleAlertDialog
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
@ -37,7 +37,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.ResponseState
import com.geeksville.mesh.ui.radioconfig.ResponseState
@Composable
fun <T> PacketResponseStateDialog(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun PaxcounterConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -36,13 +36,13 @@ import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
import com.geeksville.mesh.Position
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.BitwisePreference
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun PositionConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -34,11 +34,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.PowerConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun PowerConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun RangeTestConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditListPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun RemoteHardwareConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -34,12 +34,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.EditBase64Preference
import com.geeksville.mesh.ui.components.EditListPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun SecurityConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,13 +33,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun SerialConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun StoreForwardConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -33,12 +33,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
@Composable
fun TelemetryConfigScreen(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components.config
package com.geeksville.mesh.ui.radioconfig.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -36,13 +36,13 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.RegularPreference
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.user
@Composable