Relocate radio config to new top-level Settings screen (#2834)

This commit is contained in:
Phil Oliver 2025-08-25 15:44:32 -04:00 committed by GitHub
parent af6066d788
commit f1bb020203
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1077 additions and 1189 deletions

View file

@ -25,8 +25,8 @@ import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.ChannelConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.LoRaConfigScreen
import com.geeksville.mesh.ui.sharing.ChannelScreen
import kotlinx.serialization.Serializable

View file

@ -27,7 +27,7 @@ import androidx.navigation.navigation
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.connections.ConnectionsScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.LoRaConfigScreen
import kotlinx.serialization.Serializable
@Serializable
@ -56,7 +56,7 @@ fun NavGraphBuilder.connectionsGraph(
uiViewModel = uiViewModel,
bluetoothViewModel = bluetoothViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
onNavigateToRadioConfig = { navController.navigate(RadioConfigRoutes.RadioConfig()) },
onNavigateToSettings = { navController.navigate(SettingsRoutes.Settings()) },
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
onConfigNavigate = { route -> navController.navigate(route) },
)
@ -66,7 +66,7 @@ fun NavGraphBuilder.connectionsGraph(
}
private fun NavGraphBuilder.configRoutes(navController: NavHostController) {
composable<RadioConfigRoutes.LoRa> { backStackEntry ->
composable<SettingsRoutes.LoRa> { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(ConnectionsRoutes.ConnectionsGraph) }
LoRaConfigScreen(hiltViewModel(parentEntry))

View file

@ -59,7 +59,7 @@ fun NavDestination.isNodeDetailRoute(): Boolean = NodeDetailRoute.entries.any {
fun NavDestination.showLongNameTitle(): Boolean = !this.isTopLevel() &&
(
this.hasRoute<RadioConfigRoutes.RadioConfig>() ||
this.hasRoute<SettingsRoutes.Settings>() ||
this.hasRoute<NodesRoutes.NodeDetail>() ||
this.isConfigRoute() ||
this.isNodeDetailRoute()
@ -85,6 +85,6 @@ fun NavGraph(
) {
DebugScreen()
}
radioConfigGraph(navController, uIViewModel)
settingsGraph(navController, uIViewModel)
}
}

View file

@ -56,38 +56,40 @@ import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.CleanNodeDatabaseScreen
import com.geeksville.mesh.ui.radioconfig.RadioConfigScreen
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.radioconfig.components.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.AudioConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.BluetoothConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.CannedMessageConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DetectionSensorConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DeviceConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.DisplayConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.ExternalNotificationConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.MQTTConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NeighborInfoConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.NetworkConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PaxcounterConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PositionConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.PowerConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RangeTestConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.RemoteHardwareConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SecurityConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.SerialConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
import com.geeksville.mesh.ui.settings.radio.CleanNodeDatabaseScreen
import com.geeksville.mesh.ui.settings.radio.RadioConfigScreen
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.AudioConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.BluetoothConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.CannedMessageConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.ChannelConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.DetectionSensorConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.DeviceConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.DisplayConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.ExternalNotificationConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.LoRaConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.MQTTConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.NeighborInfoConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.NetworkConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.PaxcounterConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.PositionConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.PowerConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.RangeTestConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.RemoteHardwareConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.SecurityConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.SerialConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.settings.radio.components.UserConfigScreen
import kotlinx.serialization.Serializable
sealed class RadioConfigRoutes {
@Serializable data class RadioConfigGraph(val destNum: Int? = null) : Graph
sealed class SettingsRoutes {
@Serializable data class SettingsGraph(val destNum: Int? = null) : Graph
@Serializable data class RadioConfig(val destNum: Int? = null) : Route
@Serializable data class Settings(val destNum: Int? = null) : Route
// region radio Config Routes
@Serializable data object User : Route
@ -109,6 +111,10 @@ sealed class RadioConfigRoutes {
@Serializable data object Security : Route
// endregion
// region module config routes
@Serializable data object MQTT : Route
@Serializable data object Serial : Route
@ -135,30 +141,35 @@ sealed class RadioConfigRoutes {
@Serializable data object Paxcounter : Route
// endregion
// region advanced config routes
@Serializable data object CleanNodeDb : Route
// endregion
}
fun getNavRouteFrom(routeName: String): Route? =
ConfigRoute.entries.find { it.name == routeName }?.route ?: ModuleRoute.entries.find { it.name == routeName }?.route
@Suppress("LongMethod")
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<RadioConfigRoutes.RadioConfigGraph>(startDestination = RadioConfigRoutes.RadioConfig()) {
composable<RadioConfigRoutes.RadioConfig>(
deepLinks =
listOf(navDeepLink<RadioConfigRoutes.RadioConfig>(basePath = "$DEEP_LINK_BASE_URI/radio_config")),
fun NavGraphBuilder.settingsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<SettingsRoutes.SettingsGraph>(startDestination = SettingsRoutes.Settings()) {
composable<SettingsRoutes.Settings>(
deepLinks = listOf(navDeepLink<SettingsRoutes.Settings>(basePath = "$DEEP_LINK_BASE_URI/settings")),
) { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(RadioConfigRoutes.RadioConfigGraph::class) }
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
RadioConfigScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) {
navController.navigate(it) { popUpTo(RadioConfigRoutes.RadioConfig()) { inclusive = false } }
navController.navigate(it) { popUpTo(SettingsRoutes.Settings()) { inclusive = false } }
}
}
composable<RadioConfigRoutes.CleanNodeDb>(
composable<SettingsRoutes.CleanNodeDb>(
deepLinks =
listOf(
navDeepLink<RadioConfigRoutes.CleanNodeDb>(
basePath = "$DEEP_LINK_BASE_URI/radio_config/clean_node_db",
navDeepLink<SettingsRoutes.CleanNodeDb>(
basePath = "$DEEP_LINK_BASE_URI/settings/radio/clean_node_db",
),
),
) {
@ -174,7 +185,7 @@ fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewMod
*
* This function simplifies adding screens by handling common tasks like:
* - Setting up deep links based on the route's name.
* - Retrieving the parent [NavBackStackEntry] for the [RadioConfigRoutes.RadioConfigGraph].
* - Retrieving the parent [NavBackStackEntry] for the [SettingsRoutes.SettingsGraph].
* - Providing the [RadioConfigViewModel] scoped to the parent graph, which the [screenContent] will use.
*
* @param R The type of the [Route] object, must be serializable.
@ -191,12 +202,14 @@ private inline fun <reified R : Route> NavGraphBuilder.addRadioConfigScreenCompo
composable<R>(
deepLinks =
listOf(
navDeepLink<R>(basePath = "$DEEP_LINK_BASE_URI/radio_config/{destNum}/${routeNameString.lowercase()}"),
navDeepLink<R>(basePath = "$DEEP_LINK_BASE_URI/radio_config/${routeNameString.lowercase()}"),
navDeepLink<R>(
basePath = "$DEEP_LINK_BASE_URI/settings/radio/{destNum}/${routeNameString.lowercase()}",
),
navDeepLink<R>(basePath = "$DEEP_LINK_BASE_URI/settings/radio/${routeNameString.lowercase()}"),
),
) { backStackEntry ->
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(RadioConfigRoutes.RadioConfigGraph::class) }
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
val viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry)
screenContent(viewModel)
}
@ -206,62 +219,46 @@ private inline fun <reified R : Route> NavGraphBuilder.addRadioConfigScreenCompo
private fun NavGraphBuilder.configRoutesScreens(navController: NavHostController) {
ConfigRoute.entries.forEach { entry ->
when (entry.route) {
is RadioConfigRoutes.User ->
addRadioConfigScreenComposable<RadioConfigRoutes.User>(
is SettingsRoutes.User ->
addRadioConfigScreenComposable<SettingsRoutes.User>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.ChannelConfig ->
addRadioConfigScreenComposable<SettingsRoutes.ChannelConfig>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.ChannelConfig ->
addRadioConfigScreenComposable<RadioConfigRoutes.ChannelConfig>(
is SettingsRoutes.Device ->
addRadioConfigScreenComposable<SettingsRoutes.Device>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.Position ->
addRadioConfigScreenComposable<SettingsRoutes.Position>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Device ->
addRadioConfigScreenComposable<RadioConfigRoutes.Device>(
is SettingsRoutes.Power ->
addRadioConfigScreenComposable<SettingsRoutes.Power>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.Network ->
addRadioConfigScreenComposable<SettingsRoutes.Network>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Position ->
addRadioConfigScreenComposable<RadioConfigRoutes.Position>(
is SettingsRoutes.Display ->
addRadioConfigScreenComposable<SettingsRoutes.Display>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Power ->
addRadioConfigScreenComposable<RadioConfigRoutes.Power>(
is SettingsRoutes.LoRa ->
addRadioConfigScreenComposable<SettingsRoutes.LoRa>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.Bluetooth ->
addRadioConfigScreenComposable<SettingsRoutes.Bluetooth>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Network ->
addRadioConfigScreenComposable<RadioConfigRoutes.Network>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Display ->
addRadioConfigScreenComposable<RadioConfigRoutes.Display>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.LoRa ->
addRadioConfigScreenComposable<RadioConfigRoutes.LoRa>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Bluetooth ->
addRadioConfigScreenComposable<RadioConfigRoutes.Bluetooth>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Security ->
addRadioConfigScreenComposable<RadioConfigRoutes.Security>(
is SettingsRoutes.Security ->
addRadioConfigScreenComposable<SettingsRoutes.Security>(
navController,
entry.name,
entry.screenComposable,
@ -275,80 +272,68 @@ private fun NavGraphBuilder.configRoutesScreens(navController: NavHostController
private fun NavGraphBuilder.moduleRoutesScreens(navController: NavHostController) {
ModuleRoute.entries.forEach { entry ->
when (entry.route) {
is RadioConfigRoutes.MQTT ->
addRadioConfigScreenComposable<RadioConfigRoutes.MQTT>(
is SettingsRoutes.MQTT ->
addRadioConfigScreenComposable<SettingsRoutes.MQTT>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.Serial ->
addRadioConfigScreenComposable<SettingsRoutes.Serial>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.ExtNotification ->
addRadioConfigScreenComposable<SettingsRoutes.ExtNotification>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Serial ->
addRadioConfigScreenComposable<RadioConfigRoutes.Serial>(
is SettingsRoutes.StoreForward ->
addRadioConfigScreenComposable<SettingsRoutes.StoreForward>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.ExtNotification ->
addRadioConfigScreenComposable<RadioConfigRoutes.ExtNotification>(
is SettingsRoutes.RangeTest ->
addRadioConfigScreenComposable<SettingsRoutes.RangeTest>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.StoreForward ->
addRadioConfigScreenComposable<RadioConfigRoutes.StoreForward>(
is SettingsRoutes.Telemetry ->
addRadioConfigScreenComposable<SettingsRoutes.Telemetry>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.RangeTest ->
addRadioConfigScreenComposable<RadioConfigRoutes.RangeTest>(
is SettingsRoutes.CannedMessage ->
addRadioConfigScreenComposable<SettingsRoutes.CannedMessage>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Telemetry ->
addRadioConfigScreenComposable<RadioConfigRoutes.Telemetry>(
is SettingsRoutes.Audio ->
addRadioConfigScreenComposable<SettingsRoutes.Audio>(navController, entry.name, entry.screenComposable)
is SettingsRoutes.RemoteHardware ->
addRadioConfigScreenComposable<SettingsRoutes.RemoteHardware>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.CannedMessage ->
addRadioConfigScreenComposable<RadioConfigRoutes.CannedMessage>(
is SettingsRoutes.NeighborInfo ->
addRadioConfigScreenComposable<SettingsRoutes.NeighborInfo>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Audio ->
addRadioConfigScreenComposable<RadioConfigRoutes.Audio>(
is SettingsRoutes.AmbientLighting ->
addRadioConfigScreenComposable<SettingsRoutes.AmbientLighting>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.RemoteHardware ->
addRadioConfigScreenComposable<RadioConfigRoutes.RemoteHardware>(
is SettingsRoutes.DetectionSensor ->
addRadioConfigScreenComposable<SettingsRoutes.DetectionSensor>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.NeighborInfo ->
addRadioConfigScreenComposable<RadioConfigRoutes.NeighborInfo>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.AmbientLighting ->
addRadioConfigScreenComposable<RadioConfigRoutes.AmbientLighting>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.DetectionSensor ->
addRadioConfigScreenComposable<RadioConfigRoutes.DetectionSensor>(
navController,
entry.name,
entry.screenComposable,
)
is RadioConfigRoutes.Paxcounter ->
addRadioConfigScreenComposable<RadioConfigRoutes.Paxcounter>(
is SettingsRoutes.Paxcounter ->
addRadioConfigScreenComposable<SettingsRoutes.Paxcounter>(
navController,
entry.name,
entry.screenComposable,
@ -366,66 +351,66 @@ enum class ConfigRoute(
val type: Int = 0,
val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit,
) {
USER(R.string.user, RadioConfigRoutes.User, Icons.Default.Person, 0, { vm -> UserConfigScreen(vm) }),
USER(R.string.user, SettingsRoutes.User, Icons.Default.Person, 0, { vm -> UserConfigScreen(vm) }),
CHANNELS(
R.string.channels,
RadioConfigRoutes.ChannelConfig,
SettingsRoutes.ChannelConfig,
Icons.AutoMirrored.Default.List,
0,
{ vm -> ChannelConfigScreen(vm) },
),
DEVICE(
R.string.device,
RadioConfigRoutes.Device,
SettingsRoutes.Device,
Icons.Default.Router,
AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE,
{ vm -> DeviceConfigScreen(vm) },
),
POSITION(
R.string.position,
RadioConfigRoutes.Position,
SettingsRoutes.Position,
Icons.Default.LocationOn,
AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE,
{ vm -> PositionConfigScreen(vm) },
),
POWER(
R.string.power,
RadioConfigRoutes.Power,
SettingsRoutes.Power,
Icons.Default.Power,
AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE,
{ vm -> PowerConfigScreen(vm) },
),
NETWORK(
R.string.network,
RadioConfigRoutes.Network,
SettingsRoutes.Network,
Icons.Default.Wifi,
AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE,
{ vm -> NetworkConfigScreen(vm) },
),
DISPLAY(
R.string.display,
RadioConfigRoutes.Display,
SettingsRoutes.Display,
Icons.Default.DisplaySettings,
AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE,
{ vm -> DisplayConfigScreen(vm) },
),
LORA(
R.string.lora,
RadioConfigRoutes.LoRa,
SettingsRoutes.LoRa,
Icons.Default.CellTower,
AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE,
{ vm -> LoRaConfigScreen(vm) },
),
BLUETOOTH(
R.string.bluetooth,
RadioConfigRoutes.Bluetooth,
SettingsRoutes.Bluetooth,
Icons.Default.Bluetooth,
AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE,
{ vm -> BluetoothConfigScreen(vm) },
),
SECURITY(
R.string.security,
RadioConfigRoutes.Security,
SettingsRoutes.Security,
Icons.Default.Security,
AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE,
{ vm -> SecurityConfigScreen(vm) },
@ -454,91 +439,91 @@ enum class ModuleRoute(
) {
MQTT(
R.string.mqtt,
RadioConfigRoutes.MQTT,
SettingsRoutes.MQTT,
Icons.Default.Cloud,
AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE,
{ vm -> MQTTConfigScreen(vm) },
),
SERIAL(
R.string.serial,
RadioConfigRoutes.Serial,
SettingsRoutes.Serial,
Icons.Default.Usb,
AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE,
{ vm -> SerialConfigScreen(vm) },
),
EXT_NOTIFICATION(
R.string.external_notification,
RadioConfigRoutes.ExtNotification,
SettingsRoutes.ExtNotification,
Icons.Default.Notifications,
AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE,
{ vm -> ExternalNotificationConfigScreen(vm) },
),
STORE_FORWARD(
R.string.store_forward,
RadioConfigRoutes.StoreForward,
SettingsRoutes.StoreForward,
Icons.AutoMirrored.Default.Forward,
AdminProtos.AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG_VALUE,
{ vm -> StoreForwardConfigScreen(vm) },
),
RANGE_TEST(
R.string.range_test,
RadioConfigRoutes.RangeTest,
SettingsRoutes.RangeTest,
Icons.Default.Speed,
AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE,
{ vm -> RangeTestConfigScreen(vm) },
),
TELEMETRY(
R.string.telemetry,
RadioConfigRoutes.Telemetry,
SettingsRoutes.Telemetry,
Icons.Default.DataUsage,
AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE,
{ vm -> TelemetryConfigScreen(vm) },
),
CANNED_MESSAGE(
R.string.canned_message,
RadioConfigRoutes.CannedMessage,
SettingsRoutes.CannedMessage,
Icons.AutoMirrored.Default.Message,
AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE,
{ vm -> CannedMessageConfigScreen(vm) },
),
AUDIO(
R.string.audio,
RadioConfigRoutes.Audio,
SettingsRoutes.Audio,
Icons.AutoMirrored.Default.VolumeUp,
AdminProtos.AdminMessage.ModuleConfigType.AUDIO_CONFIG_VALUE,
{ vm -> AudioConfigScreen(vm) },
),
REMOTE_HARDWARE(
R.string.remote_hardware,
RadioConfigRoutes.RemoteHardware,
SettingsRoutes.RemoteHardware,
Icons.Default.SettingsRemote,
AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE,
{ vm -> RemoteHardwareConfigScreen(vm) },
),
NEIGHBOR_INFO(
R.string.neighbor_info,
RadioConfigRoutes.NeighborInfo,
SettingsRoutes.NeighborInfo,
Icons.Default.People,
AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE,
{ vm -> NeighborInfoConfigScreen(vm) },
),
AMBIENT_LIGHTING(
R.string.ambient_lighting,
RadioConfigRoutes.AmbientLighting,
SettingsRoutes.AmbientLighting,
Icons.Default.LightMode,
AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE,
{ vm -> AmbientLightingConfigScreen(vm) },
),
DETECTION_SENSOR(
R.string.detection_sensor,
RadioConfigRoutes.DetectionSensor,
SettingsRoutes.DetectionSensor,
Icons.Default.Sensors,
AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE,
{ vm -> DetectionSensorConfigScreen(vm) },
),
PAXCOUNTER(
R.string.paxcounter,
RadioConfigRoutes.Paxcounter,
SettingsRoutes.Paxcounter,
Icons.Default.PermScanWifi,
AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE,
{ vm -> PaxcounterConfigScreen(vm) },

View file

@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.QrCode2
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme.colorScheme
@ -90,8 +89,8 @@ import com.geeksville.mesh.navigation.ContactsRoutes
import com.geeksville.mesh.navigation.MapRoutes
import com.geeksville.mesh.navigation.NavGraph
import com.geeksville.mesh.navigation.NodesRoutes
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
@ -104,6 +103,7 @@ import com.geeksville.mesh.ui.common.icons.Conversations
import com.geeksville.mesh.ui.common.icons.Map
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.icons.Nodes
import com.geeksville.mesh.ui.common.icons.Settings
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusBlue
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.connections.DeviceType
@ -120,7 +120,7 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
Conversations(R.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph),
Nodes(R.string.nodes, MeshtasticIcons.Nodes, NodesRoutes.NodesGraph),
Map(R.string.map, MeshtasticIcons.Map, MapRoutes.Map),
Share(R.string.bottom_nav_share, Icons.Rounded.QrCode2, ChannelsRoutes.ChannelsGraph),
Settings(R.string.bottom_nav_settings, MeshtasticIcons.Settings, SettingsRoutes.SettingsGraph()),
Connections(R.string.connections, Icons.Rounded.Wifi, ConnectionsRoutes.ConnectionsGraph),
;
@ -150,7 +150,6 @@ fun MainScreen(
) {
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
val localConfig by uIViewModel.localConfig.collectAsStateWithLifecycle()
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -205,7 +204,7 @@ fun MainScreen(
text = { Text(text = message) },
onConfirm = {
if (compromisedKeys) {
navController.navigate(RadioConfigRoutes.Security)
navController.navigate(SettingsRoutes.Security)
}
uIViewModel.clearClientNotification(notification)
},
@ -348,13 +347,11 @@ fun MainScreen(
}
MainAppBar(
viewModel = uIViewModel,
isManaged = localConfig.security.isManaged,
navController = navController,
onAction = { action ->
if (action is MainMenuAction) {
when (action) {
MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel)
MainMenuAction.RADIO_CONFIG -> navController.navigate(RadioConfigRoutes.RadioConfig())
MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat)
MainMenuAction.SHOW_INTRO -> uIViewModel.onMainMenuAction(action)
else -> onAction(action)

View file

@ -55,14 +55,14 @@ import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.ContactsRoutes
import com.geeksville.mesh.navigation.NodesRoutes
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.navigation.showLongNameTitle
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.debug.DebugMenuActions
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.radioconfig.RadioConfigMenuActions
import com.geeksville.mesh.ui.settings.radio.RadioConfigMenuActions
@Suppress("CyclomaticComplexMethod")
@Composable
@ -70,7 +70,6 @@ fun MainAppBar(
modifier: Modifier = Modifier,
viewModel: UIViewModel = hiltViewModel(),
navController: NavHostController,
isManaged: Boolean,
onAction: (Any?) -> Unit,
) {
val backStackEntry by navController.currentBackStackEntryAsState()
@ -119,11 +118,11 @@ fun MainAppBar(
actions = {
currentDestination?.let {
when {
it.isTopLevel() -> MainMenuActions(isManaged, onAction)
it.isTopLevel() -> MainMenuActions(onAction)
currentDestination.hasRoute<Route.DebugPanel>() -> DebugMenuActions()
currentDestination.hasRoute<RadioConfigRoutes.RadioConfig>() ->
currentDestination.hasRoute<SettingsRoutes.Settings>() ->
RadioConfigMenuActions(viewModel = viewModel)
else -> {}
@ -208,7 +207,6 @@ private fun TopBarActions(
enum class MainMenuAction(@StringRes val stringRes: Int) {
DEBUG(R.string.debug_panel),
RADIO_CONFIG(R.string.radio_configuration),
EXPORT_RANGETEST(R.string.save_rangetest),
THEME(R.string.theme),
LANGUAGE(R.string.preferences_language),
@ -217,7 +215,7 @@ enum class MainMenuAction(@StringRes val stringRes: Int) {
}
@Composable
private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Unit) {
private fun MainMenuActions(onAction: (MainMenuAction) -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMenu = true }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
@ -235,11 +233,7 @@ private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Un
onAction(action)
showMenu = false
},
enabled =
when (action) {
MainMenuAction.RADIO_CONFIG -> !isManaged
else -> true
},
enabled = true,
)
}
}
@ -257,7 +251,7 @@ private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavig
showNodeChip = true,
canNavigateUp = canNavigateUp,
onNavigateUp = {},
actions = { MainMenuActions(isManaged = false, onAction = {}) },
actions = { MainMenuActions(onAction = {}) },
) {}
}
}

View file

@ -57,7 +57,7 @@ import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
@Composable
fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -35,8 +35,8 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun TitledCard(title: String, content: @Composable ColumnScope.() -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
fun TitledCard(title: String, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
title,
modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(),
@ -49,6 +49,6 @@ fun TitledCard(title: String, content: @Composable ColumnScope.() -> Unit) {
@PreviewLightDark
@Composable
fun TitledCardPreview() {
private fun TitledCardPreview() {
AppTheme { Surface { TitledCard(title = "Title") { Box(modifier = Modifier.fillMaxWidth().height(100.dp)) {} } } }
}

View file

@ -0,0 +1,160 @@
/*
* 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.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [settings](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:settings:FILL@0;wght@400;GRAD@0;opsz@24&icon.style=Rounded&icon.query=settings&icon.set=Material+Symbols&icon.size=24&icon.color=%23e3e3e3&icon.platform=android)
*/
val MeshtasticIcons.Settings: ImageVector
get() {
if (settings != null) {
return settings!!
}
settings =
ImageVector.Builder(
name = "Settings",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color(0xFFE3E3E3))) {
moveTo(433f, 880f)
quadToRelative(-27f, 0f, -46.5f, -18f)
reflectiveQuadTo(363f, 818f)
lineToRelative(-9f, -66f)
quadToRelative(-13f, -5f, -24.5f, -12f)
reflectiveQuadTo(307f, 725f)
lineToRelative(-62f, 26f)
quadToRelative(-25f, 11f, -50f, 2f)
reflectiveQuadToRelative(-39f, -32f)
lineToRelative(-47f, -82f)
quadToRelative(-14f, -23f, -8f, -49f)
reflectiveQuadToRelative(27f, -43f)
lineToRelative(53f, -40f)
quadToRelative(-1f, -7f, -1f, -13.5f)
verticalLineToRelative(-27f)
quadToRelative(0f, -6.5f, 1f, -13.5f)
lineToRelative(-53f, -40f)
quadToRelative(-21f, -17f, -27f, -43f)
reflectiveQuadToRelative(8f, -49f)
lineToRelative(47f, -82f)
quadToRelative(14f, -23f, 39f, -32f)
reflectiveQuadToRelative(50f, 2f)
lineToRelative(62f, 26f)
quadToRelative(11f, -8f, 23f, -15f)
reflectiveQuadToRelative(24f, -12f)
lineToRelative(9f, -66f)
quadToRelative(4f, -26f, 23.5f, -44f)
reflectiveQuadToRelative(46.5f, -18f)
horizontalLineToRelative(94f)
quadToRelative(27f, 0f, 46.5f, 18f)
reflectiveQuadToRelative(23.5f, 44f)
lineToRelative(9f, 66f)
quadToRelative(13f, 5f, 24.5f, 12f)
reflectiveQuadToRelative(22.5f, 15f)
lineToRelative(62f, -26f)
quadToRelative(25f, -11f, 50f, -2f)
reflectiveQuadToRelative(39f, 32f)
lineToRelative(47f, 82f)
quadToRelative(14f, 23f, 8f, 49f)
reflectiveQuadToRelative(-27f, 43f)
lineToRelative(-53f, 40f)
quadToRelative(1f, 7f, 1f, 13.5f)
verticalLineToRelative(27f)
quadToRelative(0f, 6.5f, -2f, 13.5f)
lineToRelative(53f, 40f)
quadToRelative(21f, 17f, 27f, 43f)
reflectiveQuadToRelative(-8f, 49f)
lineToRelative(-48f, 82f)
quadToRelative(-14f, 23f, -39f, 32f)
reflectiveQuadToRelative(-50f, -2f)
lineToRelative(-60f, -26f)
quadToRelative(-11f, 8f, -23f, 15f)
reflectiveQuadToRelative(-24f, 12f)
lineToRelative(-9f, 66f)
quadToRelative(-4f, 26f, -23.5f, 44f)
reflectiveQuadTo(527f, 880f)
horizontalLineToRelative(-94f)
close()
moveTo(440f, 800f)
horizontalLineToRelative(79f)
lineToRelative(14f, -106f)
quadToRelative(31f, -8f, 57.5f, -23.5f)
reflectiveQuadTo(639f, 633f)
lineToRelative(99f, 41f)
lineToRelative(39f, -68f)
lineToRelative(-86f, -65f)
quadToRelative(5f, -14f, 7f, -29.5f)
reflectiveQuadToRelative(2f, -31.5f)
quadToRelative(0f, -16f, -2f, -31.5f)
reflectiveQuadToRelative(-7f, -29.5f)
lineToRelative(86f, -65f)
lineToRelative(-39f, -68f)
lineToRelative(-99f, 42f)
quadToRelative(-22f, -23f, -48.5f, -38.5f)
reflectiveQuadTo(533f, 266f)
lineToRelative(-13f, -106f)
horizontalLineToRelative(-79f)
lineToRelative(-14f, 106f)
quadToRelative(-31f, 8f, -57.5f, 23.5f)
reflectiveQuadTo(321f, 327f)
lineToRelative(-99f, -41f)
lineToRelative(-39f, 68f)
lineToRelative(86f, 64f)
quadToRelative(-5f, 15f, -7f, 30f)
reflectiveQuadToRelative(-2f, 32f)
quadToRelative(0f, 16f, 2f, 31f)
reflectiveQuadToRelative(7f, 30f)
lineToRelative(-86f, 65f)
lineToRelative(39f, 68f)
lineToRelative(99f, -42f)
quadToRelative(22f, 23f, 48.5f, 38.5f)
reflectiveQuadTo(427f, 694f)
lineToRelative(13f, 106f)
close()
moveTo(482f, 620f)
quadToRelative(58f, 0f, 99f, -41f)
reflectiveQuadToRelative(41f, -99f)
quadToRelative(0f, -58f, -41f, -99f)
reflectiveQuadToRelative(-99f, -41f)
quadToRelative(-59f, 0f, -99.5f, 41f)
reflectiveQuadTo(342f, 480f)
quadToRelative(0f, 58f, 40.5f, 99f)
reflectiveQuadToRelative(99.5f, 41f)
close()
moveTo(480f, 480f)
close()
}
}
.build()
return settings!!
}
private var settings: ImageVector? = null

View file

@ -94,8 +94,8 @@ import com.geeksville.mesh.model.NO_DEVICE_SELECTED
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.SwitchPreference
@ -103,8 +103,8 @@ import com.geeksville.mesh.ui.connections.components.BLEDevices
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedCard
import com.geeksville.mesh.ui.connections.components.NetworkDevices
import com.geeksville.mesh.ui.connections.components.UsbDevices
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@ -129,7 +129,7 @@ fun ConnectionsScreen(
scanModel: BTScanModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
onNavigateToRadioConfig: () -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToNodeDetails: (Int) -> Unit,
onConfigNavigate: (Route) -> Unit,
) {
@ -167,8 +167,8 @@ fun ConnectionsScreen(
getNavRouteFrom(radioConfigState.route)?.let { route ->
isWaiting = false
radioConfigViewModel.clearPacketResponse()
if (route == RadioConfigRoutes.LoRa) {
onConfigNavigate(RadioConfigRoutes.LoRa)
if (route == SettingsRoutes.LoRa) {
onConfigNavigate(SettingsRoutes.LoRa)
}
}
},
@ -260,7 +260,7 @@ fun ConnectionsScreen(
node = node,
onNavigateToNodeDetails = onNavigateToNodeDetails,
onSetShowSharedContact = { showSharedContact = it },
onNavigateToRadioConfig = onNavigateToRadioConfig,
onNavigateToSettings = onNavigateToSettings,
onClickDisconnect = { scanModel.disconnect() },
)
}

View file

@ -50,6 +50,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState

View file

@ -58,7 +58,7 @@ fun CurrentlyConnectedCard(
modifier: Modifier = Modifier,
onNavigateToNodeDetails: (Int) -> Unit,
onSetShowSharedContact: (Node) -> Unit,
onNavigateToRadioConfig: () -> Unit,
onNavigateToSettings: () -> Unit,
onClickDisconnect: () -> Unit,
) {
Card(modifier = modifier) {
@ -98,7 +98,7 @@ fun CurrentlyConnectedCard(
}
}
IconButton(enabled = true, onClick = onNavigateToRadioConfig) {
IconButton(enabled = true, onClick = onNavigateToSettings) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(id = R.string.radio_configuration),
@ -142,7 +142,7 @@ private fun CurrentlyConnectedCardPreview() {
),
onNavigateToNodeDetails = {},
onSetShowSharedContact = {},
onNavigateToRadioConfig = {},
onNavigateToSettings = {},
onClickDisconnect = {},
)
}

View file

@ -56,6 +56,7 @@ import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.connections.isIPAddress
import kotlinx.coroutines.launch

View file

@ -27,6 +27,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable

View file

@ -141,8 +141,8 @@ import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.isUnmessageableRole
import com.geeksville.mesh.navigation.NodeDetailRoutes
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
@ -153,7 +153,7 @@ import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.radioconfig.NavCard
import com.geeksville.mesh.ui.settings.radio.NavCard
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.geeksville.mesh.util.UnitConversions
import com.geeksville.mesh.util.UnitConversions.toTempString
@ -440,7 +440,7 @@ private fun AdministrationSection(
icon = Icons.Default.Settings,
enabled = metricsState.isLocal || node.metadata != null,
) {
onAction(NodeDetailAction.Navigate(RadioConfigRoutes.RadioConfig(node.num)))
onAction(NodeDetailAction.Navigate(SettingsRoutes.Settings(node.num)))
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.settings.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun SettingsItem(
text: String,
enabled: Boolean,
leadingIcon: ImageVector? = null,
trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
onClick: () -> Unit,
) {
Card(
onClick = onClick,
enabled = enabled,
colors = CardDefaults.cardColors(
containerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 16.dp),
) {
leadingIcon?.let {
Icon(imageVector = it, contentDescription = text, modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(16.dp))
}
Text(text = text, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
trailingIcon?.let {
Icon(imageVector = it, contentDescription = null, modifier = Modifier.wrapContentSize())
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun SettingsItemPreview() {
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
}
@Preview(showBackground = true)
@Composable
private fun SettingsItemDisabledPreview() {
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
package com.geeksville.mesh.ui.settings.radio
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.radioconfig
package com.geeksville.mesh.ui.settings.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
package com.geeksville.mesh.ui.settings.radio
import android.app.Activity
import android.content.Intent
@ -72,13 +72,15 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.AdminRoute
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@ -105,6 +107,7 @@ fun RadioConfigScreen(
nodeName?.let { uiViewModel.setTitle(it) }
val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle()
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
var isWaiting by remember { mutableStateOf(false) }
@ -177,6 +180,7 @@ fun RadioConfigScreen(
RadioConfigItemList(
modifier = modifier,
state = state,
isManaged = localConfig.security.isManaged,
excludedModulesUnlocked = excludedModulesUnlocked,
onRouteClick = { route ->
isWaiting = true
@ -272,9 +276,11 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
}
}
@Suppress("LongMethod")
@Composable
private fun RadioConfigItemList(
state: RadioConfigState,
isManaged: Boolean,
excludedModulesUnlocked: Boolean = false,
modifier: Modifier = Modifier,
onRouteClick: (Enum<*>) -> Unit = {},
@ -282,7 +288,7 @@ private fun RadioConfigItemList(
onExport: () -> Unit = {},
onNavigate: (Route) -> Unit,
) {
val enabled = state.connected && !state.responseState.isWaiting()
val enabled = state.connected && !state.responseState.isWaiting() && !isManaged
var modules by remember { mutableStateOf(ModuleRoute.filterExcludedFrom(state.metadata)) }
LaunchedEffect(excludedModulesUnlocked) {
if (excludedModulesUnlocked) {
@ -291,44 +297,72 @@ private fun RadioConfigItemList(
modules = ModuleRoute.filterExcludedFrom(state.metadata)
}
}
LazyColumn(modifier = modifier, contentPadding = PaddingValues(horizontal = 16.dp)) {
item { PreferenceCategory(stringResource(R.string.radio_configuration)) }
items(ConfigRoute.filterExcludedFrom(state.metadata)) {
NavCard(title = stringResource(it.title), icon = it.icon, enabled = enabled) { onRouteClick(it) }
LazyColumn(modifier = modifier, contentPadding = PaddingValues(16.dp)) {
item {
TitledCard(title = stringResource(R.string.radio_configuration)) {
if (isManaged) {
ManagedMessage()
}
ConfigRoute.filterExcludedFrom(state.metadata).forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
}
}
}
item { PreferenceCategory(stringResource(R.string.module_settings)) }
items(modules) {
NavCard(title = stringResource(it.title), icon = it.icon, enabled = enabled) { onRouteClick(it) }
item {
TitledCard(title = stringResource(R.string.module_settings), modifier = Modifier.padding(top = 16.dp)) {
if (isManaged) {
ManagedMessage()
}
modules.forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
}
}
}
if (state.isLocal) {
item {
PreferenceCategory(stringResource(R.string.backup_restore))
NavCard(
title = stringResource(R.string.import_configuration),
icon = Icons.Default.Download,
enabled = enabled,
onClick = onImport,
)
NavCard(
title = stringResource(R.string.export_configuration),
icon = Icons.Default.Upload,
enabled = enabled,
onClick = onExport,
)
TitledCard(title = stringResource(R.string.backup_restore), modifier = Modifier.padding(top = 16.dp)) {
if (isManaged) {
ManagedMessage()
}
SettingsItem(
text = stringResource(R.string.import_configuration),
leadingIcon = Icons.Default.Download,
enabled = enabled,
onClick = onImport,
)
SettingsItem(
text = stringResource(R.string.export_configuration),
leadingIcon = Icons.Default.Upload,
enabled = enabled,
onClick = onExport,
)
}
}
}
items(AdminRoute.entries) { NavButton(it.title, enabled) { onRouteClick(it) } }
item {
PreferenceCategory("Advanced")
NavCard(
title = stringResource(R.string.clean_node_database_title),
enabled = enabled,
onClick = { onNavigate(RadioConfigRoutes.CleanNodeDb) },
)
TitledCard(title = "Advanced", modifier = Modifier.padding(top = 16.dp)) {
if (isManaged) {
ManagedMessage()
}
SettingsItem(
text = stringResource(R.string.clean_node_database_title),
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.CleanNodeDb) },
)
}
}
}
}
@ -362,5 +396,28 @@ fun RadioConfigMenuActions(modifier: Modifier = Modifier, viewModel: UIViewModel
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenPreview() = AppTheme {
RadioConfigItemList(state = RadioConfigState(isLocal = true, connected = true), onNavigate = { _ -> })
RadioConfigItemList(
state = RadioConfigState(isLocal = true, connected = true),
isManaged = false,
onNavigate = { _ -> },
)
}
@Composable
private fun ManagedMessage() {
Text(
text = stringResource(R.string.message_device_managed),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
color = MaterialTheme.colorScheme.StatusRed,
)
}
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenManagedPreview() = AppTheme {
RadioConfigItemList(
state = RadioConfigState(isLocal = true, connected = true),
isManaged = true,
onNavigate = { _ -> },
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
package com.geeksville.mesh.ui.settings.radio
import android.Manifest
import android.app.Application
@ -56,7 +56,7 @@ import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.navigation.AdminRoute
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.ConnectionState
@ -108,7 +108,7 @@ constructor(
private val meshService: IMeshService?
get() = radioConfigRepository.meshService
private val destNum = savedStateHandle.toRoute<RadioConfigRoutes.RadioConfig>().destNum
private val destNum = savedStateHandle.toRoute<SettingsRoutes.Settings>().destNum
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?>
get() = _destNode

View file

@ -15,17 +15,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig
package com.geeksville.mesh.ui.settings.radio
import com.geeksville.mesh.util.UiText
/**
* Generic sealed class defines each possible state of a response.
*/
/** 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.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun AmbientLightingConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
AmbientLightingConfigItemList(
@ -61,7 +56,7 @@ fun AmbientLightingConfigScreen(
onSaveClicked = { ambientLightingInput ->
val config = moduleConfig { ambientLighting = ambientLightingInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun AmbientLightingConfigItemList(
val focusManager = LocalFocusManager.current
var ambientLightingInput by rememberSaveable { mutableStateOf(ambientLightingConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.ambient_lighting_config)) }
item {
@ -84,9 +77,7 @@ fun AmbientLightingConfigItemList(
title = stringResource(R.string.led_state),
checked = ambientLightingInput.ledState,
enabled = enabled,
onCheckedChange = {
ambientLightingInput = ambientLightingInput.copy { ledState = it }
}
onCheckedChange = { ambientLightingInput = ambientLightingInput.copy { ledState = it } },
)
}
item { HorizontalDivider() }
@ -97,9 +88,7 @@ fun AmbientLightingConfigItemList(
value = ambientLightingInput.current,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
ambientLightingInput = ambientLightingInput.copy { current = it }
}
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { current = it } },
)
}
@ -109,7 +98,7 @@ fun AmbientLightingConfigItemList(
value = ambientLightingInput.red,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } }
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } },
)
}
@ -119,7 +108,7 @@ fun AmbientLightingConfigItemList(
value = ambientLightingInput.green,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } }
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } },
)
}
@ -129,7 +118,7 @@ fun AmbientLightingConfigItemList(
value = ambientLightingInput.blue,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { blue = it } }
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { blue = it } },
)
}
@ -143,7 +132,7 @@ fun AmbientLightingConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ambientLightingInput)
}
},
)
}
}
@ -155,6 +144,6 @@ private fun AmbientLightingConfigPreview() {
AmbientLightingConfigItemList(
ambientLightingConfig = ModuleConfigProtos.ModuleConfig.AmbientLightingConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -42,19 +42,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun AudioConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
AudioConfigItemList(
@ -63,22 +58,17 @@ fun AudioConfigScreen(
onSaveClicked = { audioInput ->
val config = moduleConfig { audio = audioInput }
viewModel.setModuleConfig(config)
}
},
)
}
@Suppress("LongMethod")
@Composable
fun AudioConfigItemList(
audioConfig: AudioConfig,
enabled: Boolean,
onSaveClicked: (AudioConfig) -> Unit,
) {
fun AudioConfigItemList(audioConfig: AudioConfig, enabled: Boolean, onSaveClicked: (AudioConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var audioInput by rememberSaveable { mutableStateOf(audioConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.audio_config)) }
item {
@ -86,7 +76,7 @@ fun AudioConfigItemList(
title = stringResource(R.string.codec_2_enabled),
checked = audioInput.codec2Enabled,
enabled = enabled,
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } }
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } },
)
}
item { HorizontalDivider() }
@ -97,7 +87,7 @@ fun AudioConfigItemList(
value = audioInput.pttPin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { pttPin = it } }
onValueChanged = { audioInput = audioInput.copy { pttPin = it } },
)
}
@ -105,11 +95,12 @@ fun AudioConfigItemList(
DropDownPreference(
title = stringResource(R.string.codec2_sample_rate),
enabled = enabled,
items = AudioConfig.Audio_Baud.entries
items =
AudioConfig.Audio_Baud.entries
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = audioInput.bitrate,
onItemSelected = { audioInput = audioInput.copy { bitrate = it } }
onItemSelected = { audioInput = audioInput.copy { bitrate = it } },
)
}
item { Divider() }
@ -120,7 +111,7 @@ fun AudioConfigItemList(
value = audioInput.i2SWs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SWs = it } }
onValueChanged = { audioInput = audioInput.copy { i2SWs = it } },
)
}
@ -130,7 +121,7 @@ fun AudioConfigItemList(
value = audioInput.i2SSd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSd = it } }
onValueChanged = { audioInput = audioInput.copy { i2SSd = it } },
)
}
@ -140,7 +131,7 @@ fun AudioConfigItemList(
value = audioInput.i2SDin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SDin = it } }
onValueChanged = { audioInput = audioInput.copy { i2SDin = it } },
)
}
@ -150,7 +141,7 @@ fun AudioConfigItemList(
value = audioInput.i2SSck,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSck = it } }
onValueChanged = { audioInput = audioInput.copy { i2SSck = it } },
)
}
@ -164,7 +155,7 @@ fun AudioConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(audioInput)
}
},
)
}
}
@ -173,9 +164,5 @@ fun AudioConfigItemList(
@Preview(showBackground = true)
@Composable
private fun AudioConfigPreview() {
AudioConfigItemList(
audioConfig = AudioConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
AudioConfigItemList(audioConfig = AudioConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -41,19 +41,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun BluetoothConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
BluetoothConfigItemList(
@ -62,7 +57,7 @@ fun BluetoothConfigScreen(
onSaveClicked = { bluetoothInput ->
val config = config { bluetooth = bluetoothInput }
viewModel.setConfig(config)
}
},
)
}
@ -75,9 +70,7 @@ fun BluetoothConfigItemList(
val focusManager = LocalFocusManager.current
var bluetoothInput by rememberSaveable { mutableStateOf(bluetoothConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.bluetooth_config)) }
item {
@ -85,7 +78,7 @@ fun BluetoothConfigItemList(
title = stringResource(R.string.bluetooth_enabled),
checked = bluetoothInput.enabled,
enabled = enabled,
onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } }
onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -94,11 +87,12 @@ fun BluetoothConfigItemList(
DropDownPreference(
title = stringResource(R.string.pairing_mode),
enabled = enabled,
items = BluetoothConfig.PairingMode.entries
items =
BluetoothConfig.PairingMode.entries
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = bluetoothInput.mode,
onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } }
onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } },
)
}
item { HorizontalDivider() }
@ -113,7 +107,7 @@ fun BluetoothConfigItemList(
if (it.toString().length == 6) { // ensure 6 digits
bluetoothInput = bluetoothInput.copy { fixedPin = it }
}
}
},
)
}
@ -127,7 +121,7 @@ fun BluetoothConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(bluetoothInput)
}
},
)
}
}
@ -136,9 +130,5 @@ fun BluetoothConfigItemList(
@Preview(showBackground = true)
@Composable
private fun BluetoothConfigPreview() {
BluetoothConfigItemList(
bluetoothConfig = BluetoothConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
BluetoothConfigItemList(bluetoothConfig = BluetoothConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -44,19 +44,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun CannedMessageConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
CannedMessageConfigItemList(
@ -71,7 +66,7 @@ fun CannedMessageConfigScreen(
val config = moduleConfig { cannedMessage = cannedMessageInput }
viewModel.setModuleConfig(config)
}
}
},
)
}
@ -86,9 +81,7 @@ fun CannedMessageConfigItemList(
var messagesInput by rememberSaveable { mutableStateOf(messages) }
var cannedMessageInput by rememberSaveable { mutableStateOf(cannedMessageConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.canned_message_config)) }
item {
@ -96,9 +89,7 @@ fun CannedMessageConfigItemList(
title = stringResource(R.string.canned_message_enabled),
checked = cannedMessageInput.enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { this.enabled = it }
}
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -108,9 +99,7 @@ fun CannedMessageConfigItemList(
title = stringResource(R.string.rotary_encoder_1_enabled),
checked = cannedMessageInput.rotary1Enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it }
}
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it } },
)
}
item { HorizontalDivider() }
@ -121,9 +110,7 @@ fun CannedMessageConfigItemList(
value = cannedMessageInput.inputbrokerPinA,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it }
}
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it } },
)
}
@ -133,9 +120,7 @@ fun CannedMessageConfigItemList(
value = cannedMessageInput.inputbrokerPinB,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it }
}
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it } },
)
}
@ -145,9 +130,7 @@ fun CannedMessageConfigItemList(
value = cannedMessageInput.inputbrokerPinPress,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it }
}
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it } },
)
}
@ -155,13 +138,12 @@ fun CannedMessageConfigItemList(
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_press),
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventPress,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it }
}
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it } },
)
}
item { HorizontalDivider() }
@ -170,13 +152,12 @@ fun CannedMessageConfigItemList(
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_cw),
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCw,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it }
}
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it } },
)
}
item { HorizontalDivider() }
@ -185,13 +166,12 @@ fun CannedMessageConfigItemList(
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_ccw),
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCcw,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it }
}
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it } },
)
}
item { HorizontalDivider() }
@ -201,9 +181,7 @@ fun CannedMessageConfigItemList(
title = stringResource(R.string.up_down_select_input_enabled),
checked = cannedMessageInput.updown1Enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it }
}
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it } },
)
}
item { HorizontalDivider() }
@ -215,13 +193,10 @@ fun CannedMessageConfigItemList(
maxSize = 63, // allow_input_source max_size:16
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { allowInputSource = it }
}
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { allowInputSource = it } },
)
}
@ -230,9 +205,7 @@ fun CannedMessageConfigItemList(
title = stringResource(R.string.send_bell),
checked = cannedMessageInput.sendBell,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { sendBell = it }
}
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
@ -244,11 +217,10 @@ fun CannedMessageConfigItemList(
maxSize = 200, // messages max_size:201
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { messagesInput = it }
onValueChanged = { messagesInput = it },
)
}
@ -263,7 +235,7 @@ fun CannedMessageConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(messagesInput, cannedMessageInput)
}
},
)
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
@ -79,7 +79,7 @@ import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.common.components.dragContainer
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
private fun ChannelItem(

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -44,19 +44,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun DetectionSensorConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
DetectionSensorConfigItemList(
@ -65,7 +60,7 @@ fun DetectionSensorConfigScreen(
onSaveClicked = { detectionSensorInput ->
val config = moduleConfig { detectionSensor = detectionSensorInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -79,9 +74,7 @@ fun DetectionSensorConfigItemList(
val focusManager = LocalFocusManager.current
var detectionSensorInput by rememberSaveable { mutableStateOf(detectionSensorConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.detection_sensor_config)) }
item {
@ -89,9 +82,7 @@ fun DetectionSensorConfigItemList(
title = stringResource(R.string.detection_sensor_enabled),
checked = detectionSensorInput.enabled,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { this.enabled = it }
}
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -102,9 +93,7 @@ fun DetectionSensorConfigItemList(
value = detectionSensorInput.minimumBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it }
}
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it } },
)
}
@ -114,9 +103,7 @@ fun DetectionSensorConfigItemList(
value = detectionSensorInput.stateBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it }
}
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it } },
)
}
@ -125,9 +112,7 @@ fun DetectionSensorConfigItemList(
title = stringResource(R.string.send_bell_with_alert_message),
checked = detectionSensorInput.sendBell,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { sendBell = it }
}
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
@ -139,13 +124,10 @@ fun DetectionSensorConfigItemList(
maxSize = 19, // name max_size:20
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { name = it }
}
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { name = it } },
)
}
@ -155,9 +137,7 @@ fun DetectionSensorConfigItemList(
value = detectionSensorInput.monitorPin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { monitorPin = it }
}
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { monitorPin = it } },
)
}
@ -165,13 +145,12 @@ fun DetectionSensorConfigItemList(
DropDownPreference(
title = stringResource(R.string.detection_trigger_type),
enabled = enabled,
items = ModuleConfig.DetectionSensorConfig.TriggerType.entries
items =
ModuleConfig.DetectionSensorConfig.TriggerType.entries
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = detectionSensorInput.detectionTriggerType,
onItemSelected = {
detectionSensorInput = detectionSensorInput.copy { detectionTriggerType = it }
}
onItemSelected = { detectionSensorInput = detectionSensorInput.copy { detectionTriggerType = it } },
)
}
item { HorizontalDivider() }
@ -181,9 +160,7 @@ fun DetectionSensorConfigItemList(
title = stringResource(R.string.use_input_pullup_mode),
checked = detectionSensorInput.usePullup,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { usePullup = it }
}
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { usePullup = it } },
)
}
item { HorizontalDivider() }
@ -198,7 +175,7 @@ fun DetectionSensorConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(detectionSensorInput)
}
},
)
}
}
@ -210,6 +187,6 @@ private fun DetectionSensorConfigPreview() {
DetectionSensorConfigItemList(
detectionSensorConfig = ModuleConfig.DetectionSensorConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -58,47 +58,44 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
private val DeviceConfig.Role.stringRes: Int
get() = when (this) {
DeviceConfig.Role.CLIENT -> R.string.role_client
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute
DeviceConfig.Role.ROUTER -> R.string.role_router
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client
DeviceConfig.Role.REPEATER -> R.string.role_repeater
DeviceConfig.Role.TRACKER -> R.string.role_tracker
DeviceConfig.Role.SENSOR -> R.string.role_sensor
DeviceConfig.Role.TAK -> R.string.role_tak
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late
else -> R.string.unrecognized
}
get() =
when (this) {
DeviceConfig.Role.CLIENT -> R.string.role_client
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute
DeviceConfig.Role.ROUTER -> R.string.role_router
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client
DeviceConfig.Role.REPEATER -> R.string.role_repeater
DeviceConfig.Role.TRACKER -> R.string.role_tracker
DeviceConfig.Role.SENSOR -> R.string.role_sensor
DeviceConfig.Role.TAK -> R.string.role_tak
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late
else -> R.string.unrecognized
}
private val DeviceConfig.RebroadcastMode.stringRes: Int
get() = when (this) {
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only
else -> R.string.unrecognized
}
get() =
when (this) {
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only
else -> R.string.unrecognized
}
@Composable
fun DeviceConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
DeviceConfigItemList(
@ -107,94 +104,62 @@ fun DeviceConfigScreen(
onSaveClicked = { deviceInput ->
val config = config { device = deviceInput }
viewModel.setConfig(config)
}
},
)
}
@Suppress("LongMethod")
@Composable
fun RouterRoleConfirmationDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
val dialogTitle = stringResource(R.string.are_you_sure)
val annotatedDialogText = AnnotatedString.fromHtml(
htmlString = stringResource(R.string.router_role_confirmation_text),
linkStyles = TextLinkStyles(style = SpanStyle(color = Color.Blue))
)
val annotatedDialogText =
AnnotatedString.fromHtml(
htmlString = stringResource(R.string.router_role_confirmation_text),
linkStyles = TextLinkStyles(style = SpanStyle(color = Color.Blue)),
)
var confirmed by rememberSaveable { mutableStateOf(false) }
AlertDialog(
title = {
Text(text = dialogTitle)
},
title = { Text(text = dialogTitle) },
text = {
Column {
Text(text = annotatedDialogText)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(true) {
confirmed = !confirmed
},
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth().clickable(true) { confirmed = !confirmed },
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = confirmed,
onCheckedChange = { confirmed = it }
)
Checkbox(checked = confirmed, onCheckedChange = { confirmed = it })
Text(stringResource(R.string.i_know_what_i_m_doing))
}
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = onConfirm,
enabled = confirmed
) {
Text(stringResource(R.string.accept))
}
TextButton(onClick = onConfirm, enabled = confirmed) { Text(stringResource(R.string.accept)) }
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(stringResource(R.string.cancel))
}
}
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
)
}
@Suppress("LongMethod")
@Composable
fun DeviceConfigItemList(
deviceConfig: DeviceConfig,
enabled: Boolean,
onSaveClicked: (DeviceConfig) -> Unit,
) {
fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveClicked: (DeviceConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var deviceInput by rememberSaveable { mutableStateOf(deviceConfig) }
var selectedRole by rememberSaveable { mutableStateOf(deviceInput.role) }
val infrastructureRoles = listOf(
DeviceConfig.Role.ROUTER,
DeviceConfig.Role.REPEATER,
)
val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER)
if (selectedRole != deviceInput.role) {
if (selectedRole in infrastructureRoles) {
RouterRoleConfirmationDialog(
onDismiss = { selectedRole = deviceInput.role },
onConfirm = {
deviceInput = deviceInput.copy { role = selectedRole }
}
onConfirm = { deviceInput = deviceInput.copy { role = selectedRole } },
)
} else {
deviceInput = deviceInput.copy { role = selectedRole }
}
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.device_config)) }
item {
@ -202,9 +167,7 @@ fun DeviceConfigItemList(
title = stringResource(R.string.role),
enabled = enabled,
selectedItem = deviceInput.role,
onItemSelected = {
selectedRole = it
},
onItemSelected = { selectedRole = it },
summary = stringResource(id = deviceInput.role.stringRes),
)
HorizontalDivider()
@ -216,9 +179,7 @@ fun DeviceConfigItemList(
value = deviceInput.buttonGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { buttonGpio = it }
}
onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } },
)
}
@ -228,9 +189,7 @@ fun DeviceConfigItemList(
value = deviceInput.buzzerGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { buzzerGpio = it }
}
onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } },
)
}
@ -251,9 +210,7 @@ fun DeviceConfigItemList(
value = deviceInput.nodeInfoBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it }
}
onValueChanged = { deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it } },
)
}
@ -263,7 +220,7 @@ fun DeviceConfigItemList(
summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary),
checked = deviceInput.doubleTapAsButtonPress,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } }
onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } },
)
HorizontalDivider()
}
@ -274,7 +231,7 @@ fun DeviceConfigItemList(
summary = stringResource(id = R.string.config_device_disableTripleClick_summary),
checked = deviceInput.disableTripleClick,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } }
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } },
)
HorizontalDivider()
}
@ -286,13 +243,10 @@ fun DeviceConfigItemList(
maxSize = 64, // tzdef max_size:65
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { tzdef = it }
},
onValueChanged = { deviceInput = deviceInput.copy { tzdef = it } },
)
}
@ -302,7 +256,7 @@ fun DeviceConfigItemList(
summary = stringResource(id = R.string.config_device_ledHeartbeatDisabled_summary),
checked = deviceInput.ledHeartbeatDisabled,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } }
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } },
)
HorizontalDivider()
}
@ -317,7 +271,7 @@ fun DeviceConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(deviceInput)
}
},
)
}
}
@ -326,9 +280,5 @@ fun DeviceConfigItemList(
@Preview(showBackground = true)
@Composable
private fun DeviceConfigPreview() {
DeviceConfigItemList(
deviceConfig = DeviceConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
DeviceConfigItemList(deviceConfig = DeviceConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -41,7 +41,7 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.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.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -45,7 +45,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.google.protobuf.Descriptors
private const val SupportedFields = 7
private const val SUPPORTED_FIELDS = 7
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@ -58,10 +58,13 @@ fun EditDeviceProfileDialog(
modifier: Modifier = Modifier,
) {
val state = remember {
val fields = deviceProfile.descriptorForType.fields
.filter { it.number < SupportedFields } // TODO add ringtone & canned messages
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>()
.apply { putAll(fields.associateWith(deviceProfile::hasField)) }
val fields =
deviceProfile.descriptorForType.fields.filter {
it.number < SUPPORTED_FIELDS
} // TODO add ringtone & canned messages
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>().apply {
putAll(fields.associateWith(deviceProfile::hasField))
}
}
AlertDialog(
@ -71,38 +74,36 @@ fun EditDeviceProfileDialog(
Column(modifier.fillMaxWidth()) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge.copy(
style =
MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
)
HorizontalDivider()
state.keys.sortedBy { it.number }.forEach { field ->
SwitchPreference(
title = field.name,
checked = state[field] == true,
enabled = deviceProfile.hasField(field),
onCheckedChange = { state[field] = it },
padding = PaddingValues(0.dp)
)
}
state.keys
.sortedBy { it.number }
.forEach { field ->
SwitchPreference(
title = field.name,
checked = state[field] == true,
enabled = deviceProfile.hasField(field),
onCheckedChange = { state[field] = it },
padding = PaddingValues(0.dp),
)
}
HorizontalDivider()
}
},
confirmButton = {
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
modifier = modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(
modifier = modifier.weight(1f),
onClick = onDismiss
) { Text(stringResource(R.string.cancel)) }
TextButton(modifier = modifier.weight(1f), onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
Button(
modifier = modifier.weight(1f),
onClick = {
@ -115,9 +116,11 @@ fun EditDeviceProfileDialog(
onConfirm(builder.build())
},
enabled = state.values.any { it },
) { Text(stringResource(R.string.save)) }
) {
Text(stringResource(R.string.save))
}
}
}
},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -44,19 +44,14 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.common.components.TextDividerPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun ExternalNotificationConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun ExternalNotificationConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
ExternalNotificationConfigItemList(
@ -71,7 +66,7 @@ fun ExternalNotificationConfigScreen(
val config = moduleConfig { externalNotification = extNotificationInput }
viewModel.setModuleConfig(config)
}
}
},
)
}
@ -86,9 +81,7 @@ fun ExternalNotificationConfigItemList(
var ringtoneInput by rememberSaveable { mutableStateOf(ringtone) }
var externalNotificationInput by rememberSaveable { mutableStateOf(extNotificationConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.external_notification_config)) }
item {
@ -96,9 +89,7 @@ fun ExternalNotificationConfigItemList(
title = stringResource(R.string.external_notification_enabled),
checked = externalNotificationInput.enabled,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { this.enabled = it }
}
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { this.enabled = it } },
)
}
@ -109,9 +100,7 @@ fun ExternalNotificationConfigItemList(
title = stringResource(R.string.alert_message_led),
checked = externalNotificationInput.alertMessage,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertMessage = it }
}
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertMessage = it } },
)
}
item { HorizontalDivider() }
@ -122,9 +111,8 @@ fun ExternalNotificationConfigItemList(
checked = externalNotificationInput.alertMessageBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertMessageBuzzer = it }
}
externalNotificationInput = externalNotificationInput.copy { alertMessageBuzzer = it }
},
)
}
item { HorizontalDivider() }
@ -135,9 +123,8 @@ fun ExternalNotificationConfigItemList(
checked = externalNotificationInput.alertMessageVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertMessageVibra = it }
}
externalNotificationInput = externalNotificationInput.copy { alertMessageVibra = it }
},
)
}
@ -148,9 +135,7 @@ fun ExternalNotificationConfigItemList(
title = stringResource(R.string.alert_bell_led),
checked = externalNotificationInput.alertBell,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertBell = it }
}
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertBell = it } },
)
}
item { HorizontalDivider() }
@ -161,9 +146,8 @@ fun ExternalNotificationConfigItemList(
checked = externalNotificationInput.alertBellBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertBellBuzzer = it }
}
externalNotificationInput = externalNotificationInput.copy { alertBellBuzzer = it }
},
)
}
item { HorizontalDivider() }
@ -174,9 +158,8 @@ fun ExternalNotificationConfigItemList(
checked = externalNotificationInput.alertBellVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertBellVibra = it }
}
externalNotificationInput = externalNotificationInput.copy { alertBellVibra = it }
},
)
}
item { HorizontalDivider() }
@ -187,23 +170,19 @@ fun ExternalNotificationConfigItemList(
value = externalNotificationInput.output,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { output = it }
}
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { output = it } },
)
}
if (externalNotificationInput.output != 0) {
item {
SwitchPreference(
title = stringResource(R.string.output_led_active_high),
checked = externalNotificationInput.active,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { active = it }
}
)
}
SwitchPreference(
title = stringResource(R.string.output_led_active_high),
checked = externalNotificationInput.active,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { active = it } },
)
}
}
item { HorizontalDivider() }
@ -213,23 +192,19 @@ fun ExternalNotificationConfigItemList(
value = externalNotificationInput.outputBuzzer,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it }
}
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it } },
)
}
if (externalNotificationInput.outputBuzzer != 0) {
item {
SwitchPreference(
title = stringResource(R.string.use_pwm_buzzer),
checked = externalNotificationInput.usePwm,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { usePwm = it }
}
)
}
SwitchPreference(
title = stringResource(R.string.use_pwm_buzzer),
checked = externalNotificationInput.usePwm,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { usePwm = it } },
)
}
}
item { HorizontalDivider() }
@ -239,9 +214,7 @@ fun ExternalNotificationConfigItemList(
value = externalNotificationInput.outputVibra,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputVibra = it }
}
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputVibra = it } },
)
}
@ -251,9 +224,7 @@ fun ExternalNotificationConfigItemList(
value = externalNotificationInput.outputMs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputMs = it }
}
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputMs = it } },
)
}
@ -263,9 +234,7 @@ fun ExternalNotificationConfigItemList(
value = externalNotificationInput.nagTimeout,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { nagTimeout = it }
}
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { nagTimeout = it } },
)
}
@ -276,11 +245,10 @@ fun ExternalNotificationConfigItemList(
maxSize = 230, // ringtone max_size:231
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ringtoneInput = it }
onValueChanged = { ringtoneInput = it },
)
}
@ -291,7 +259,7 @@ fun ExternalNotificationConfigItemList(
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { useI2SAsBuzzer = it }
}
},
)
}
item { HorizontalDivider() }
@ -307,7 +275,7 @@ fun ExternalNotificationConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ringtoneInput, externalNotificationInput)
}
},
)
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -48,19 +48,14 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun LoRaConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun LoRaConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
LoRaConfigItemList(
@ -86,13 +81,9 @@ fun LoRaConfigItemList(
) {
val focusManager = LocalFocusManager.current
var loraInput by rememberSaveable { mutableStateOf(loraConfig) }
val primaryChannel by remember(loraInput) {
mutableStateOf(Channel(primarySettings, loraInput))
}
val primaryChannel by remember(loraInput) { mutableStateOf(Channel(primarySettings, loraInput)) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.lora_config)) }
item {
@ -100,7 +91,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.use_modem_preset),
checked = loraInput.usePreset,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } }
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } },
)
}
item { HorizontalDivider() }
@ -110,11 +101,12 @@ fun LoRaConfigItemList(
DropDownPreference(
title = stringResource(R.string.modem_preset),
enabled = enabled && loraInput.usePreset,
items = LoRaConfig.ModemPreset.entries
items =
LoRaConfig.ModemPreset.entries
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
.map { it to it.name },
selectedItem = loraInput.modemPreset,
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } }
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } },
)
}
item { HorizontalDivider() }
@ -125,7 +117,7 @@ fun LoRaConfigItemList(
value = loraInput.bandwidth,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { bandwidth = it } }
onValueChanged = { loraInput = loraInput.copy { bandwidth = it } },
)
}
@ -135,7 +127,7 @@ fun LoRaConfigItemList(
value = loraInput.spreadFactor,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } }
onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } },
)
}
@ -145,7 +137,7 @@ fun LoRaConfigItemList(
value = loraInput.codingRate,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { codingRate = it } }
onValueChanged = { loraInput = loraInput.copy { codingRate = it } },
)
}
}
@ -156,7 +148,7 @@ fun LoRaConfigItemList(
value = loraInput.frequencyOffset,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } }
onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } },
)
}
@ -166,7 +158,7 @@ fun LoRaConfigItemList(
enabled = enabled,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } }
onItemSelected = { loraInput = loraInput.copy { region = it } },
)
}
item { HorizontalDivider() }
@ -177,7 +169,7 @@ fun LoRaConfigItemList(
value = loraInput.hopLimit,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } }
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } },
)
}
@ -186,7 +178,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.tx_enabled),
checked = loraInput.txEnabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } }
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } },
)
}
item { HorizontalDivider() }
@ -213,7 +205,7 @@ fun LoRaConfigItemList(
if (it <= loraInput.numChannels) { // total num of LoRa channels
loraInput = loraInput.copy { channelNum = it }
}
}
},
)
}
@ -222,7 +214,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.override_duty_cycle),
checked = loraInput.overrideDutyCycle,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } }
onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } },
)
}
item { HorizontalDivider() }
@ -235,11 +227,12 @@ fun LoRaConfigItemList(
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
loraInput = loraInput.copy {
ignoreIncoming.clear()
ignoreIncoming.addAll(list.filter { it != 0 })
}
}
loraInput =
loraInput.copy {
ignoreIncoming.clear()
ignoreIncoming.addAll(list.filter { it != 0 })
}
},
)
}
@ -248,7 +241,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.sx126x_rx_boosted_gain),
checked = loraInput.sx126XRxBoostedGain,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } }
onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } },
)
}
item { HorizontalDivider() }
@ -257,11 +250,16 @@ fun LoRaConfigItemList(
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.override_frequency_mhz),
value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq,
value =
if (isFocused || loraInput.overrideFrequency != 0f) {
loraInput.overrideFrequency
} else {
primaryChannel.radioFreq
},
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } }
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } },
)
}
@ -271,7 +269,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.pa_fan_disabled),
checked = loraInput.paFanDisabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } }
onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } },
)
}
item { HorizontalDivider() }
@ -282,7 +280,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.ignore_mqtt),
checked = loraInput.ignoreMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } }
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } },
)
}
item { HorizontalDivider() }
@ -292,7 +290,7 @@ fun LoRaConfigItemList(
title = stringResource(R.string.ok_to_mqtt),
checked = loraInput.configOkToMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } }
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } },
)
}
item { HorizontalDivider() }
@ -307,7 +305,7 @@ fun LoRaConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(loraInput)
}
},
)
}
}
@ -320,6 +318,6 @@ private fun LoRaConfigPreview() {
loraConfig = Channel.default.loraConfig,
primarySettings = Channel.default.settings,
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -17,7 +17,7 @@
@file:Suppress("LongMethod")
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -46,7 +46,7 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
@ -48,8 +48,9 @@ import com.geeksville.mesh.util.DistanceUnit
import com.geeksville.mesh.util.toDistanceString
import kotlin.math.roundToInt
private const val PositionPrecisionMin = 12
private const val PositionPrecisionMax = 15
private const val POSITION_PRECISION_MIN = 12
private const val POSITION_PRECISION_MAX = 15
@Suppress("LongMethod")
@Composable
fun MapReportingPreference(
@ -62,13 +63,11 @@ fun MapReportingPreference(
publishIntervalSecs: Int = 3600,
onPublishIntervalSecsChanged: (Int) -> Unit = {},
enabled: Boolean,
focusManager: FocusManager
focusManager: FocusManager,
) {
Column {
var showMapReportingWarning by rememberSaveable { mutableStateOf(mapReportingEnabled) }
LaunchedEffect(mapReportingEnabled) {
showMapReportingWarning = mapReportingEnabled
}
LaunchedEffect(mapReportingEnabled) { showMapReportingWarning = mapReportingEnabled }
SwitchPreference(
title = stringResource(R.string.map_reporting),
summary = stringResource(R.string.map_reporting_summary),
@ -81,21 +80,13 @@ fun MapReportingPreference(
} else if (!checked) {
onMapReportingEnabledChanged(false)
}
}
},
)
AnimatedVisibility(showMapReportingWarning) {
Card(
modifier = Modifier.padding(16.dp),
) {
Text(
text = stringResource(R.string.map_reporting_consent_header),
modifier = Modifier.padding(16.dp),
)
Card(modifier = Modifier.padding(16.dp)) {
Text(text = stringResource(R.string.map_reporting_consent_header), modifier = Modifier.padding(16.dp))
HorizontalDivider()
Text(
stringResource(R.string.map_reporting_consent_text),
modifier = Modifier.padding(16.dp)
)
Text(stringResource(R.string.map_reporting_consent_text), modifier = Modifier.padding(16.dp))
SwitchPreference(
title = stringResource(R.string.i_agree),
@ -118,18 +109,14 @@ fun MapReportingPreference(
value = positionPrecision.toFloat(),
onValueChange = { onPositionPrecisionChanged(it.roundToInt()) },
enabled = enabled,
valueRange = PositionPrecisionMin.toFloat()..PositionPrecisionMax.toFloat(),
steps = PositionPrecisionMax - PositionPrecisionMin - 1,
valueRange = POSITION_PRECISION_MIN.toFloat()..POSITION_PRECISION_MAX.toFloat(),
steps = POSITION_PRECISION_MAX - POSITION_PRECISION_MIN - 1,
)
val precisionMeters = precisionBitsToMeters(positionPrecision).toInt()
val unit = DistanceUnit.Companion.getFromLocale()
Text(
text = precisionMeters.toDistanceString(unit),
modifier = Modifier.Companion.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp
),
modifier = Modifier.Companion.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
fontSize = MaterialTheme.typography.bodyLarge.fontSize,
overflow = TextOverflow.Companion.Ellipsis,
maxLines = 1,
@ -161,6 +148,6 @@ fun MapReportingPreview() {
positionPrecision = 5,
onPositionPrecisionChanged = {},
enabled = true,
focusManager = focusManager
focusManager = focusManager,
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun NeighborInfoConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
NeighborInfoConfigItemList(
@ -61,7 +56,7 @@ fun NeighborInfoConfigScreen(
onSaveClicked = { neighborInfoInput ->
val config = moduleConfig { neighborInfo = neighborInfoInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun NeighborInfoConfigItemList(
val focusManager = LocalFocusManager.current
var neighborInfoInput by rememberSaveable { mutableStateOf(neighborInfoConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.neighbor_info_config)) }
item {
@ -84,9 +77,7 @@ fun NeighborInfoConfigItemList(
title = stringResource(R.string.neighbor_info_enabled),
checked = neighborInfoInput.enabled,
enabled = enabled,
onCheckedChange = {
neighborInfoInput = neighborInfoInput.copy { this.enabled = it }
}
onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -97,9 +88,7 @@ fun NeighborInfoConfigItemList(
value = neighborInfoInput.updateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
neighborInfoInput = neighborInfoInput.copy { updateInterval = it }
}
onValueChanged = { neighborInfoInput = neighborInfoInput.copy { updateInterval = it } },
)
}
@ -109,9 +98,7 @@ fun NeighborInfoConfigItemList(
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
checked = neighborInfoInput.transmitOverLora,
enabled = enabled,
onCheckedChange = {
neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it }
}
onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it } },
)
HorizontalDivider()
}
@ -126,7 +113,7 @@ fun NeighborInfoConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(neighborInfoInput)
}
},
)
}
}
@ -138,6 +125,6 @@ private fun NeighborInfoConfigPreview() {
NeighborInfoConfigItemList(
neighborInfoConfig = ModuleConfigProtos.ModuleConfig.NeighborInfoConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
@ -54,30 +54,20 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
@Composable
private fun ScanErrorDialog(
onDismiss: () -> Unit = {}
) = SimpleAlertDialog(
title = R.string.error,
text = R.string.wifi_qr_code_error,
onDismiss = onDismiss,
)
private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
SimpleAlertDialog(title = R.string.error, text = R.string.wifi_qr_code_error, onDismiss = onDismiss)
@Composable
fun NetworkConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
NetworkConfigItemList(
@ -88,13 +78,13 @@ fun NetworkConfigScreen(
onSaveClicked = { networkInput ->
val config = config { network = networkInput }
viewModel.setConfig(config)
}
},
)
}
private fun extractWifiCredentials(qrCode: String) = Regex("""WIFI:S:(.*?);.*?P:(.*?);""")
.find(qrCode)?.destructured
?.let { (ssid, password) -> ssid to password } ?: (null to null)
private fun extractWifiCredentials(qrCode: String) =
Regex("""WIFI:S:(.*?);.*?P:(.*?);""").find(qrCode)?.destructured?.let { (ssid, password) -> ssid to password }
?: (null to null)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ -113,33 +103,34 @@ fun NetworkConfigItemList(
ScanErrorDialog { showScanErrorDialog = false }
}
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
if (result.contents != null) {
val (ssid, psk) = extractWifiCredentials(result.contents)
if (ssid != null && psk != null) {
networkInput = networkInput.copy {
wifiSsid = ssid
wifiPsk = psk
val barcodeLauncher =
rememberLauncherForActivityResult(ScanContract()) { result ->
if (result.contents != null) {
val (ssid, psk) = extractWifiCredentials(result.contents)
if (ssid != null && psk != null) {
networkInput =
networkInput.copy {
wifiSsid = ssid
wifiPsk = psk
}
} else {
showScanErrorDialog = true
}
} else {
showScanErrorDialog = true
}
}
}
fun zxingScan() {
val zxingScan = ScanOptions().apply {
setCameraId(0)
setPrompt("")
setBeepEnabled(false)
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
}
val zxingScan =
ScanOptions().apply {
setCameraId(0)
setPrompt("")
setBeepEnabled(false)
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
}
barcodeLauncher.launch(zxingScan)
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.network_config)) }
item {
@ -147,7 +138,7 @@ fun NetworkConfigItemList(
title = stringResource(R.string.wifi_enabled),
checked = networkInput.wifiEnabled,
enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } }
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } },
)
HorizontalDivider()
}
@ -159,13 +150,10 @@ fun NetworkConfigItemList(
maxSize = 32, // wifi_ssid max_size:33
enabled = enabled && hasWifi,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { wifiSsid = it }
}
onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } },
)
}
@ -176,17 +164,14 @@ fun NetworkConfigItemList(
maxSize = 64, // wifi_psk max_size:65
enabled = enabled && hasWifi,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } }
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } },
)
}
item {
Button(
onClick = { zxingScan() },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.height(48.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = enabled && hasWifi,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
@ -198,7 +183,7 @@ fun NetworkConfigItemList(
title = stringResource(R.string.ethernet_enabled),
checked = networkInput.ethEnabled,
enabled = enabled && hasEthernet,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } }
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } },
)
HorizontalDivider()
}
@ -210,13 +195,10 @@ fun NetworkConfigItemList(
maxSize = 32, // ntp_server max_size:33
enabled = enabled,
isError = networkInput.ntpServer.isEmpty(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { ntpServer = it }
}
onValueChanged = { networkInput = networkInput.copy { ntpServer = it } },
)
}
@ -227,13 +209,10 @@ fun NetworkConfigItemList(
maxSize = 32, // rsyslog_server max_size:33
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { rsyslogServer = it }
}
onValueChanged = { networkInput = networkInput.copy { rsyslogServer = it } },
)
}
@ -241,11 +220,12 @@ fun NetworkConfigItemList(
DropDownPreference(
title = stringResource(R.string.ipv4_mode),
enabled = enabled,
items = NetworkConfig.AddressMode.entries
items =
NetworkConfig.AddressMode.entries
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = networkInput.addressMode,
onItemSelected = { networkInput = networkInput.copy { addressMode = it } }
onItemSelected = { networkInput = networkInput.copy { addressMode = it } },
)
HorizontalDivider()
}
@ -259,7 +239,7 @@ fun NetworkConfigItemList(
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { ip = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
}
},
)
}
@ -272,7 +252,7 @@ fun NetworkConfigItemList(
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { gateway = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
}
},
)
}
@ -285,7 +265,7 @@ fun NetworkConfigItemList(
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { subnet = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
}
},
)
}
@ -298,14 +278,12 @@ fun NetworkConfigItemList(
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { dns = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
}
},
)
}
item { HorizontalDivider() }
if (hasEthernet || hasWifi) {
item {
PreferenceCategory(text = stringResource(R.string.udp_config))
}
item { PreferenceCategory(text = stringResource(R.string.udp_config)) }
item {
SwitchPreference(
@ -313,11 +291,8 @@ fun NetworkConfigItemList(
checked = networkInput.enabledProtocols == 1,
enabled = enabled,
onCheckedChange = {
networkInput =
networkInput.copy {
if (it) enabledProtocols = 1 else enabledProtocols = 0
}
}
networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
},
)
}
@ -333,7 +308,7 @@ fun NetworkConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(networkInput)
}
},
)
}
}
@ -347,7 +322,7 @@ private fun NetworkConfigPreview() {
hasEthernet = true,
networkConfig = NetworkConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.core.animateFloatAsState
@ -37,35 +37,24 @@ 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.radioconfig.ResponseState
import com.geeksville.mesh.ui.settings.radio.ResponseState
@Composable
fun <T> PacketResponseStateDialog(
state: ResponseState<T>,
onDismiss: () -> Unit = {},
onComplete: () -> Unit = {},
) {
fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit = {}, onComplete: () -> Unit = {}) {
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
AlertDialog(
onDismissRequest = {},
shape = RoundedCornerShape(16.dp),
title = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
if (state is ResponseState.Loading) {
val progress by animateFloatAsState(
targetValue = state.completed.toFloat() / state.total.toFloat(),
label = "progress",
)
val progress by
animateFloatAsState(
targetValue = state.completed.toFloat() / state.total.toFloat(),
label = "progress",
)
Text("%.0f%%".format(progress * 100))
LinearProgressIndicator(
progress = progress,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
if (state.total == state.completed) onComplete()
}
if (state is ResponseState.Success) {
@ -79,10 +68,8 @@ fun <T> PacketResponseStateDialog(
},
confirmButton = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Center
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
Button(
onClick = {
@ -91,20 +78,17 @@ fun <T> PacketResponseStateDialog(
backDispatcher?.onBackPressed()
}
},
modifier = Modifier.padding(top = 16.dp)
) { Text(stringResource(R.string.close)) }
modifier = Modifier.padding(top = 16.dp),
) {
Text(stringResource(R.string.close))
}
}
}
},
)
}
@Preview(showBackground = true)
@Composable
private fun PacketResponseStateDialogPreview() {
PacketResponseStateDialog(
state = ResponseState.Loading(
total = 17,
completed = 5,
),
)
PacketResponseStateDialog(state = ResponseState.Loading(total = 17, completed = 5))
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun PaxcounterConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
PaxcounterConfigItemList(
@ -61,7 +56,7 @@ fun PaxcounterConfigScreen(
onSaveClicked = { paxcounterConfigInput ->
val config = moduleConfig { paxcounter = paxcounterConfigInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -75,9 +70,7 @@ fun PaxcounterConfigItemList(
val focusManager = LocalFocusManager.current
var paxcounterInput by rememberSaveable { mutableStateOf(paxcounterConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.paxcounter_config)) }
item {
@ -85,9 +78,7 @@ fun PaxcounterConfigItemList(
title = stringResource(R.string.paxcounter_enabled),
checked = paxcounterInput.enabled,
enabled = enabled,
onCheckedChange = {
paxcounterInput = paxcounterInput.copy { this.enabled = it }
}
onCheckedChange = { paxcounterInput = paxcounterInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -98,9 +89,7 @@ fun PaxcounterConfigItemList(
value = paxcounterInput.paxcounterUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it }
}
onValueChanged = { paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it } },
)
}
@ -110,9 +99,7 @@ fun PaxcounterConfigItemList(
value = paxcounterInput.wifiThreshold,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { wifiThreshold = it }
}
onValueChanged = { paxcounterInput = paxcounterInput.copy { wifiThreshold = it } },
)
}
@ -122,9 +109,7 @@ fun PaxcounterConfigItemList(
value = paxcounterInput.bleThreshold,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { bleThreshold = it }
}
onValueChanged = { paxcounterInput = paxcounterInput.copy { bleThreshold = it } },
)
}
@ -138,7 +123,7 @@ fun PaxcounterConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(paxcounterInput)
}
},
)
}
}
@ -150,6 +135,6 @@ private fun PaxcounterConfigPreview() {
PaxcounterConfigItemList(
paxcounterConfig = ModuleConfigProtos.ModuleConfig.PaxcounterConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import android.Manifest
import android.annotation.SuppressLint
@ -54,7 +54,7 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.launch

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,7 +40,7 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun RangeTestConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
RangeTestConfigItemList(
@ -61,7 +56,7 @@ fun RangeTestConfigScreen(
onSaveClicked = { rangeTestInput ->
val config = moduleConfig { rangeTest = rangeTestInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun RangeTestConfigItemList(
val focusManager = LocalFocusManager.current
var rangeTestInput by rememberSaveable { mutableStateOf(rangeTestConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.range_test_config)) }
item {
@ -84,7 +77,7 @@ fun RangeTestConfigItemList(
title = stringResource(R.string.range_test_enabled),
checked = rangeTestInput.enabled,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } }
onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -95,7 +88,7 @@ fun RangeTestConfigItemList(
value = rangeTestInput.sender,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } }
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } },
)
}
@ -104,7 +97,7 @@ fun RangeTestConfigItemList(
title = stringResource(R.string.save_csv_in_storage_esp32_only),
checked = rangeTestInput.save,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } }
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } },
)
}
item { HorizontalDivider() }
@ -119,7 +112,7 @@ fun RangeTestConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(rangeTestInput)
}
},
)
}
}
@ -128,9 +121,5 @@ fun RangeTestConfigItemList(
@Preview(showBackground = true)
@Composable
private fun RangeTestConfig() {
RangeTestConfigItemList(
rangeTestConfig = RangeTestConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
RangeTestConfigItemList(rangeTestConfig = RangeTestConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun RemoteHardwareConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
RemoteHardwareConfigItemList(
@ -61,7 +56,7 @@ fun RemoteHardwareConfigScreen(
onSaveClicked = { remoteHardwareInput ->
val config = moduleConfig { remoteHardware = remoteHardwareInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun RemoteHardwareConfigItemList(
val focusManager = LocalFocusManager.current
var remoteHardwareInput by rememberSaveable { mutableStateOf(remoteHardwareConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.remote_hardware_config)) }
item {
@ -84,9 +77,7 @@ fun RemoteHardwareConfigItemList(
title = stringResource(R.string.remote_hardware_enabled),
checked = remoteHardwareInput.enabled,
enabled = enabled,
onCheckedChange = {
remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it }
}
onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -96,9 +87,7 @@ fun RemoteHardwareConfigItemList(
title = stringResource(R.string.allow_undefined_pin_access),
checked = remoteHardwareInput.allowUndefinedPinAccess,
enabled = enabled,
onCheckedChange = {
remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it }
}
onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it } },
)
}
item { HorizontalDivider() }
@ -111,11 +100,12 @@ fun RemoteHardwareConfigItemList(
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
remoteHardwareInput = remoteHardwareInput.copy {
availablePins.clear()
availablePins.addAll(list)
}
}
remoteHardwareInput =
remoteHardwareInput.copy {
availablePins.clear()
availablePins.addAll(list)
}
},
)
}
@ -129,7 +119,7 @@ fun RemoteHardwareConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(remoteHardwareInput)
}
},
)
}
}
@ -141,6 +131,6 @@ private fun RemoteHardwareConfigPreview() {
RemoteHardwareConfigItemList(
remoteHardwareConfig = RemoteHardwareConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import android.app.Activity
import android.content.Intent
@ -58,24 +58,19 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.node.NodeActionButton
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.util.encodeToString
import com.geeksville.mesh.util.toByteString
import com.google.protobuf.ByteString
import java.security.SecureRandom
@Composable
fun SecurityConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val node by viewModel.destNode.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
SecurityConfigItemList(
@ -86,9 +81,7 @@ fun SecurityConfigScreen(
val config = config { security = securityInput }
viewModel.setConfig(config)
},
onExport = { uri, securityConfig ->
viewModel.exportSecurityConfig(uri, securityConfig)
},
onExport = { uri, securityConfig -> viewModel.exportSecurityConfig(uri, securityConfig) },
)
}
@ -114,13 +107,12 @@ fun SecurityConfigItemList(
}
}
val exportConfigLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> onExport(uri, securityConfig) }
val exportConfigLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> onExport(uri, securityConfig) }
}
}
}
var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) }
PrivateKeyRegenerateDialog(
@ -131,7 +123,7 @@ fun SecurityConfigItemList(
showKeyGenerationDialog = false
onConfirm(securityInput)
},
onDismiss = { showKeyGenerationDialog = false }
onDismiss = { showKeyGenerationDialog = false },
)
var showEditSecurityConfigDialog by rememberSaveable { mutableStateOf(false) }
if (showEditSecurityConfigDialog) {
@ -143,14 +135,15 @@ fun SecurityConfigItemList(
TextButton(
onClick = {
showEditSecurityConfigDialog = false
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
putExtra(
Intent.EXTRA_TITLE,
"${user?.shortName}_keys_${System.currentTimeMillis()}.json"
)
}
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
putExtra(
Intent.EXTRA_TITLE,
"${user?.shortName}_keys_${System.currentTimeMillis()}.json",
)
}
exportConfigLauncher.launch(intent)
},
) {
@ -160,9 +153,7 @@ fun SecurityConfigItemList(
)
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.security_config)) }
item {
@ -177,11 +168,7 @@ fun SecurityConfigItemList(
securityInput = securityInput.copy { publicKey = it }
}
},
trailingIcon = {
CopyIconButton(
valueToCopy = securityInput.publicKey.encodeToString(),
)
}
trailingIcon = { CopyIconButton(valueToCopy = securityInput.publicKey.encodeToString()) },
)
}
@ -196,11 +183,7 @@ fun SecurityConfigItemList(
securityInput = securityInput.copy { privateKey = it }
}
},
trailingIcon = {
CopyIconButton(
valueToCopy = securityInput.privateKey.encodeToString(),
)
}
trailingIcon = { CopyIconButton(valueToCopy = securityInput.privateKey.encodeToString()) },
)
}
@ -210,9 +193,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.regenerate_private_key),
enabled = enabled,
icon = Icons.TwoTone.Warning,
onClick = {
showKeyGenerationDialog = true
}
onClick = { showKeyGenerationDialog = true },
)
}
@ -222,9 +203,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.export_keys),
enabled = enabled,
icon = Icons.TwoTone.Warning,
onClick = {
showEditSecurityConfigDialog = true
}
onClick = { showEditSecurityConfigDialog = true },
)
}
@ -236,10 +215,11 @@ fun SecurityConfigItemList(
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = {
securityInput = securityInput.copy {
adminKey.clear()
adminKey.addAll(it)
}
securityInput =
securityInput.copy {
adminKey.clear()
adminKey.addAll(it)
}
},
)
}
@ -249,9 +229,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.managed_mode),
checked = securityInput.isManaged,
enabled = enabled && securityInput.adminKeyCount > 0,
onCheckedChange = {
securityInput = securityInput.copy { isManaged = it }
}
onCheckedChange = { securityInput = securityInput.copy { isManaged = it } },
)
}
item { HorizontalDivider() }
@ -261,7 +239,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.serial_console),
checked = securityInput.serialEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } }
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } },
)
}
item { HorizontalDivider() }
@ -271,9 +249,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.debug_log_api_enabled),
checked = securityInput.debugLogApiEnabled,
enabled = enabled,
onCheckedChange = {
securityInput = securityInput.copy { debugLogApiEnabled = it }
}
onCheckedChange = { securityInput = securityInput.copy { debugLogApiEnabled = it } },
)
}
item { HorizontalDivider() }
@ -283,9 +259,7 @@ fun SecurityConfigItemList(
title = stringResource(R.string.legacy_admin_channel),
checked = securityInput.adminChannelEnabled,
enabled = enabled,
onCheckedChange = {
securityInput = securityInput.copy { adminChannelEnabled = it }
}
onCheckedChange = { securityInput = securityInput.copy { adminChannelEnabled = it } },
)
}
item { HorizontalDivider() }
@ -300,7 +274,7 @@ fun SecurityConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onConfirm(securityInput)
}
},
)
}
}
@ -323,34 +297,27 @@ fun PrivateKeyRegenerateDialog(
confirmButton = {
TextButton(
onClick = {
securityInput = securityInput.copy {
clearPrivateKey()
clearPublicKey()
// Generate a random "f" value
val f = ByteArray(32).apply {
SecureRandom().nextBytes(this)
securityInput =
securityInput.copy {
clearPrivateKey()
clearPublicKey()
// Generate a random "f" value
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
privateKey = ByteString.copyFrom(f)
}
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
privateKey = ByteString.copyFrom(f)
}
onConfirm(securityInput)
},
) {
Text(stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
) {
Text(stringResource(R.string.cancel))
}
}
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
)
}
}
@ -358,9 +325,5 @@ fun PrivateKeyRegenerateDialog(
@Preview(showBackground = true)
@Composable
private fun SecurityConfigPreview() {
SecurityConfigItemList(
securityConfig = SecurityConfig.getDefaultInstance(),
enabled = true,
onConfirm = {},
)
SecurityConfigItemList(securityConfig = SecurityConfig.getDefaultInstance(), enabled = true, onConfirm = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -41,19 +41,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun SerialConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
SerialConfigItemList(
@ -62,22 +57,17 @@ fun SerialConfigScreen(
onSaveClicked = { serialInput ->
val config = moduleConfig { serial = serialInput }
viewModel.setModuleConfig(config)
}
},
)
}
@Suppress("LongMethod")
@Composable
fun SerialConfigItemList(
serialConfig: SerialConfig,
enabled: Boolean,
onSaveClicked: (SerialConfig) -> Unit,
) {
fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var serialInput by rememberSaveable { mutableStateOf(serialConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.serial_config)) }
item {
@ -85,7 +75,7 @@ fun SerialConfigItemList(
title = stringResource(R.string.serial_enabled),
checked = serialInput.enabled,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } }
onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -95,7 +85,7 @@ fun SerialConfigItemList(
title = stringResource(R.string.echo_enabled),
checked = serialInput.echo,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { echo = it } }
onCheckedChange = { serialInput = serialInput.copy { echo = it } },
)
}
item { HorizontalDivider() }
@ -106,7 +96,7 @@ fun SerialConfigItemList(
value = serialInput.rxd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { rxd = it } }
onValueChanged = { serialInput = serialInput.copy { rxd = it } },
)
}
@ -116,7 +106,7 @@ fun SerialConfigItemList(
value = serialInput.txd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { txd = it } }
onValueChanged = { serialInput = serialInput.copy { txd = it } },
)
}
@ -124,11 +114,12 @@ fun SerialConfigItemList(
DropDownPreference(
title = stringResource(R.string.serial_baud_rate),
enabled = enabled,
items = SerialConfig.Serial_Baud.entries
items =
SerialConfig.Serial_Baud.entries
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.baud,
onItemSelected = { serialInput = serialInput.copy { baud = it } }
onItemSelected = { serialInput = serialInput.copy { baud = it } },
)
}
item { HorizontalDivider() }
@ -139,7 +130,7 @@ fun SerialConfigItemList(
value = serialInput.timeout,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { timeout = it } }
onValueChanged = { serialInput = serialInput.copy { timeout = it } },
)
}
@ -147,11 +138,12 @@ fun SerialConfigItemList(
DropDownPreference(
title = stringResource(R.string.serial_mode),
enabled = enabled,
items = SerialConfig.Serial_Mode.entries
items =
SerialConfig.Serial_Mode.entries
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.mode,
onItemSelected = { serialInput = serialInput.copy { mode = it } }
onItemSelected = { serialInput = serialInput.copy { mode = it } },
)
}
item { HorizontalDivider() }
@ -161,9 +153,7 @@ fun SerialConfigItemList(
title = stringResource(R.string.override_console_serial_port),
checked = serialInput.overrideConsoleSerialPort,
enabled = enabled,
onCheckedChange = {
serialInput = serialInput.copy { overrideConsoleSerialPort = it }
}
onCheckedChange = { serialInput = serialInput.copy { overrideConsoleSerialPort = it } },
)
}
item { HorizontalDivider() }
@ -178,7 +168,7 @@ fun SerialConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(serialInput)
}
},
)
}
}
@ -187,9 +177,5 @@ fun SerialConfigItemList(
@Preview(showBackground = true)
@Composable
private fun SerialConfigPreview() {
SerialConfigItemList(
serialConfig = SerialConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
SerialConfigItemList(serialConfig = SerialConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun StoreForwardConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
StoreForwardConfigItemList(
@ -61,7 +56,7 @@ fun StoreForwardConfigScreen(
onSaveClicked = { storeForwardInput ->
val config = moduleConfig { storeForward = storeForwardInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun StoreForwardConfigItemList(
val focusManager = LocalFocusManager.current
var storeForwardInput by rememberSaveable { mutableStateOf(storeForwardConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.store_forward_config)) }
item {
@ -84,9 +77,7 @@ fun StoreForwardConfigItemList(
title = stringResource(R.string.store_forward_enabled),
checked = storeForwardInput.enabled,
enabled = enabled,
onCheckedChange = {
storeForwardInput = storeForwardInput.copy { this.enabled = it }
}
onCheckedChange = { storeForwardInput = storeForwardInput.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -96,7 +87,7 @@ fun StoreForwardConfigItemList(
title = stringResource(R.string.heartbeat),
checked = storeForwardInput.heartbeat,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } }
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } },
)
}
item { HorizontalDivider() }
@ -107,7 +98,7 @@ fun StoreForwardConfigItemList(
value = storeForwardInput.records,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } }
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } },
)
}
@ -117,9 +108,7 @@ fun StoreForwardConfigItemList(
value = storeForwardInput.historyReturnMax,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnMax = it }
}
onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnMax = it } },
)
}
@ -129,9 +118,7 @@ fun StoreForwardConfigItemList(
value = storeForwardInput.historyReturnWindow,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnWindow = it }
}
onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnWindow = it } },
)
}
@ -140,7 +127,7 @@ fun StoreForwardConfigItemList(
title = stringResource(R.string.server),
checked = storeForwardInput.isServer,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } }
onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } },
)
}
item { HorizontalDivider() }
@ -155,7 +142,7 @@ fun StoreForwardConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(storeForwardInput)
}
},
)
}
}
@ -167,6 +154,6 @@ private fun StoreForwardConfigPreview() {
StoreForwardConfigItemList(
storeForwardConfig = StoreForwardConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
onSaveClicked = {},
)
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -40,19 +40,14 @@ import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@Composable
fun TelemetryConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
TelemetryConfigItemList(
@ -61,7 +56,7 @@ fun TelemetryConfigScreen(
onSaveClicked = { telemetryInput ->
val config = moduleConfig { telemetry = telemetryInput }
viewModel.setModuleConfig(config)
}
},
)
}
@ -74,9 +69,7 @@ fun TelemetryConfigItemList(
val focusManager = LocalFocusManager.current
var telemetryInput by rememberSaveable { mutableStateOf(telemetryConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.telemetry_config)) }
item {
@ -85,9 +78,7 @@ fun TelemetryConfigItemList(
value = telemetryInput.deviceUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { deviceUpdateInterval = it }
}
onValueChanged = { telemetryInput = telemetryInput.copy { deviceUpdateInterval = it } },
)
}
@ -97,9 +88,7 @@ fun TelemetryConfigItemList(
value = telemetryInput.environmentUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { environmentUpdateInterval = it }
}
onValueChanged = { telemetryInput = telemetryInput.copy { environmentUpdateInterval = it } },
)
}
@ -108,9 +97,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.environment_metrics_module_enabled),
checked = telemetryInput.environmentMeasurementEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
@ -120,9 +107,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.environment_metrics_on_screen_enabled),
checked = telemetryInput.environmentScreenEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentScreenEnabled = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentScreenEnabled = it } },
)
}
item { HorizontalDivider() }
@ -132,9 +117,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.environment_metrics_use_fahrenheit),
checked = telemetryInput.environmentDisplayFahrenheit,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it } },
)
}
item { HorizontalDivider() }
@ -144,9 +127,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.air_quality_metrics_module_enabled),
checked = telemetryInput.airQualityEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { airQualityEnabled = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { airQualityEnabled = it } },
)
}
item { HorizontalDivider() }
@ -157,9 +138,7 @@ fun TelemetryConfigItemList(
value = telemetryInput.airQualityInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { airQualityInterval = it }
}
onValueChanged = { telemetryInput = telemetryInput.copy { airQualityInterval = it } },
)
}
@ -168,9 +147,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.power_metrics_module_enabled),
checked = telemetryInput.powerMeasurementEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
@ -181,9 +158,7 @@ fun TelemetryConfigItemList(
value = telemetryInput.powerUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { powerUpdateInterval = it }
}
onValueChanged = { telemetryInput = telemetryInput.copy { powerUpdateInterval = it } },
)
}
@ -192,9 +167,7 @@ fun TelemetryConfigItemList(
title = stringResource(R.string.power_metrics_on_screen_enabled),
checked = telemetryInput.powerScreenEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { powerScreenEnabled = it }
}
onCheckedChange = { telemetryInput = telemetryInput.copy { powerScreenEnabled = it } },
)
}
item { HorizontalDivider() }
@ -209,7 +182,7 @@ fun TelemetryConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(telemetryInput)
}
},
)
}
}
@ -218,9 +191,5 @@ fun TelemetryConfigItemList(
@Preview(showBackground = true)
@Composable
private fun TelemetryConfigPreview() {
TelemetryConfigItemList(
telemetryConfig = TelemetryConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
TelemetryConfigItemList(telemetryConfig = TelemetryConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.radioconfig.components
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@ -46,20 +46,15 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.RegularPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.user
@Composable
fun UserConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
UserConfigItemList(
@ -85,18 +80,10 @@ fun UserConfigItemList(
val validLongName = userInput.longName.isNotBlank()
val validShortName = userInput.shortName.isNotBlank()
val validNames = validLongName && validShortName
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.user_config)) }
item {
RegularPreference(
title = stringResource(R.string.node_id),
subtitle = userInput.id,
onClick = {}
)
}
item { RegularPreference(title = stringResource(R.string.node_id), subtitle = userInput.id, onClick = {}) }
item { HorizontalDivider() }
item {
@ -106,13 +93,10 @@ fun UserConfigItemList(
maxSize = 39, // long_name max_size:40
enabled = enabled,
isError = !validLongName,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
userInput = userInput.copy { longName = it }
}
onValueChanged = { userInput = userInput.copy { longName = it } },
)
}
@ -123,11 +107,10 @@ fun UserConfigItemList(
maxSize = 4, // short_name max_size:5
enabled = enabled,
isError = !validShortName,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { userInput = userInput.copy { shortName = it } }
onValueChanged = { userInput = userInput.copy { shortName = it } },
)
}
@ -135,7 +118,7 @@ fun UserConfigItemList(
RegularPreference(
title = stringResource(R.string.hardware_model),
subtitle = userInput.hwModel.name,
onClick = {}
onClick = {},
)
}
item { HorizontalDivider() }
@ -144,12 +127,11 @@ fun UserConfigItemList(
SwitchPreference(
title = stringResource(R.string.unmessageable),
summary = stringResource(R.string.unmonitored_or_infrastructure),
checked = userInput.isUnmessagable || (
firmwareVersion < DeviceVersion("2.6.9") &&
userInput.role.isUnmessageableRole()
),
checked =
userInput.isUnmessagable ||
(firmwareVersion < DeviceVersion("2.6.9") && userInput.role.isUnmessageableRole()),
enabled = userInput.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"),
onCheckedChange = { userInput = userInput.copy { isUnmessagable = it } }
onCheckedChange = { userInput = userInput.copy { isUnmessagable = it } },
)
}
@ -161,7 +143,7 @@ fun UserConfigItemList(
summary = stringResource(R.string.licensed_amateur_radio_text),
checked = userInput.isLicensed,
enabled = enabled,
onCheckedChange = { userInput = userInput.copy { isLicensed = it } }
onCheckedChange = { userInput = userInput.copy { isLicensed = it } },
)
}
item { HorizontalDivider() }
@ -172,10 +154,11 @@ fun UserConfigItemList(
onCancelClicked = {
focusManager.clearFocus()
userInput = userConfig
}, onSaveClicked = {
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(userInput)
}
},
)
}
}
@ -185,7 +168,8 @@ fun UserConfigItemList(
@Composable
private fun UserConfigPreview() {
UserConfigItemList(
userConfig = user {
userConfig =
user {
id = "!a280d9c8"
longName = "Meshtastic d9c8"
shortName = "d9c8"
@ -193,9 +177,7 @@ private fun UserConfigPreview() {
isLicensed = false
},
enabled = true,
onSaveClicked = { },
metadata = deviceMetadata {
firmwareVersion = "2.8.0"
}
onSaveClicked = {},
metadata = deviceMetadata { firmwareVersion = "2.8.0" },
)
}

View file

@ -104,9 +104,9 @@ import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.AdaptiveTwoPane
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState