mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Improvements to Channel management (#2935)
Co-authored-by: DaneEvans <dane@goneepic.com> Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
653f7a1b3a
commit
dc9c325e1e
4 changed files with 294 additions and 41 deletions
|
|
@ -15,6 +15,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
|
@ -59,6 +61,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.getChannel
|
||||
|
|
@ -160,7 +164,7 @@ private fun SecurityIconDisplay(
|
|||
Icon(
|
||||
imageVector = badgeIcon,
|
||||
contentDescription = stringResource(R.string.security_icon_badge_warning_description),
|
||||
tint = badgeIconColor ?: MaterialTheme.colorScheme.onError, // Default for contrast
|
||||
tint = badgeIconColor ?: colorScheme.onError, // Default for contrast
|
||||
modifier = Modifier.size(16.dp), // Adjusted badge icon size
|
||||
)
|
||||
}
|
||||
|
|
@ -291,6 +295,29 @@ fun SecurityIcon(
|
|||
externalOnClick = externalOnClick,
|
||||
)
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that enables recomposition when making changes to the [ChannelSettings].
|
||||
*
|
||||
* @param baseContentDescription The base content description for the icon.
|
||||
* @param externalOnClick Optional lambda for external actions, invoked when the icon is clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channelSettings: ChannelSettings,
|
||||
loraConfig: LoRaConfig,
|
||||
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val channel = Channel(channelSettings, loraConfig)
|
||||
SecurityIcon(
|
||||
isLowEntropyKey = channel.isLowEntropyKey,
|
||||
isPreciseLocation = channel.isPreciseLocation,
|
||||
isMqttEnabled = channel.isMqttEnabled,
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel index. If the channel at the given
|
||||
* index is not found, nothing is rendered.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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.radio.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.CloudUpload
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.model.DeviceVersion
|
||||
|
||||
/**
|
||||
* At this firmware version periodic position sharing on a secondary channel was implemented. To enable this feature the
|
||||
* user must disable position on the primary channel and enable on a secondary channel. The lowest indexed secondary
|
||||
* channel with the position enabled will conduct the automatic position broadcasts.
|
||||
*/
|
||||
internal const val SECONDARY_CHANNEL_EPOCH = "2.6.10"
|
||||
|
||||
internal enum class ChannelIcons(
|
||||
val icon: ImageVector,
|
||||
@StringRes val descriptionResId: Int,
|
||||
@StringRes val additionalInfoResId: Int,
|
||||
) {
|
||||
LOCATION(
|
||||
icon = Icons.Filled.LocationOn,
|
||||
descriptionResId = R.string.location_sharing,
|
||||
additionalInfoResId = R.string.periodic_position_broadcast,
|
||||
),
|
||||
UPLINK(
|
||||
icon = Icons.Filled.CloudUpload,
|
||||
descriptionResId = R.string.uplink_enabled,
|
||||
additionalInfoResId = R.string.uplink_feature_description,
|
||||
),
|
||||
DOWNLINK(
|
||||
icon = Icons.Filled.CloudDownload,
|
||||
descriptionResId = R.string.downlink_enabled,
|
||||
additionalInfoResId = R.string.downlink_feature_description,
|
||||
),
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ChannelLegend(onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onClick.invoke() },
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Row {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = stringResource(R.string.info))
|
||||
Text(
|
||||
text = stringResource(R.string.primary),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.secondary),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ChannelLegendDialog(firmwareVersion: DeviceVersion, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.channel_features)) },
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.primary),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = "- ${stringResource(R.string.primary_channel_feature)}",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.secondary),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = "- ${stringResource(R.string.secondary_no_telemetry)}",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
|
||||
/* 2.6.10+ */
|
||||
"- ${stringResource(R.string.secondary_channel_position_feature)}"
|
||||
} else {
|
||||
"- ${stringResource(R.string.manual_position_request)}"
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
IconDefinitions()
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconDefinitions() {
|
||||
Text(text = stringResource(R.string.icon_meanings), style = MaterialTheme.typography.titleLarge)
|
||||
ChannelIcons.entries.forEach { icon ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(imageVector = icon.icon, contentDescription = stringResource(icon.descriptionResId))
|
||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||
Text(text = stringResource(icon.descriptionResId), style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = stringResource(icon.additionalInfoResId), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
if (icon != ChannelIcons.entries.lastOrNull()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewChannelLegendDialog() {
|
||||
ChannelLegendDialog(firmwareVersion = DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {}
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.RowScope
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
|
|
@ -59,6 +58,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -73,6 +73,7 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
|
|||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.channelSettings
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.common.components.SecurityIcon
|
||||
|
|
@ -89,18 +90,20 @@ private fun ChannelItem(
|
|||
onClick: () -> Unit = {},
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val fontColor = if (index == 0) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp).clickable(enabled = enabled) { onClick() }) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
|
||||
) {
|
||||
AssistChip(onClick = onClick, label = { Text(text = "$index") })
|
||||
AssistChip(onClick = onClick, label = { Text(text = "$index", color = fontColor) })
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.weight(1f),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = fontColor,
|
||||
)
|
||||
content()
|
||||
}
|
||||
|
|
@ -112,11 +115,34 @@ private fun ChannelCard(
|
|||
index: Int,
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
channelSettings: ChannelSettings,
|
||||
loraConfig: LoRaConfig,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
channel: Channel,
|
||||
sharesLocation: Boolean,
|
||||
) = ChannelItem(index = index, title = title, enabled = enabled, onClick = onEditClick) {
|
||||
SecurityIcon(channel)
|
||||
if (sharesLocation) {
|
||||
Icon(
|
||||
imageVector = ChannelIcons.LOCATION.icon,
|
||||
contentDescription = stringResource(ChannelIcons.LOCATION.descriptionResId),
|
||||
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
if (channelSettings.uplinkEnabled) {
|
||||
Icon(
|
||||
imageVector = ChannelIcons.UPLINK.icon,
|
||||
contentDescription = stringResource(ChannelIcons.UPLINK.descriptionResId),
|
||||
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
if (channelSettings.downlinkEnabled) {
|
||||
Icon(
|
||||
imageVector = ChannelIcons.DOWNLINK.icon,
|
||||
contentDescription = stringResource(ChannelIcons.DOWNLINK.descriptionResId),
|
||||
modifier = Modifier.wrapContentSize().padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
SecurityIcon(channelSettings, loraConfig)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
IconButton(onClick = { onDeleteClick() }) {
|
||||
Icon(
|
||||
|
|
@ -135,7 +161,7 @@ fun ChannelSelection(
|
|||
isSelected: Boolean,
|
||||
onSelected: (Boolean) -> Unit,
|
||||
channel: Channel,
|
||||
) = ChannelItem(index = index, title = title, enabled = enabled, onClick = {}) {
|
||||
) = ChannelItem(index = index, title = title, enabled = enabled) {
|
||||
SecurityIcon(channel)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Checkbox(enabled = enabled, checked = isSelected, onCheckedChange = onSelected)
|
||||
|
|
@ -152,25 +178,28 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
|
|||
ChannelSettingsItemList(
|
||||
settingsList = state.channelList,
|
||||
loraConfig = state.radioConfig.lora,
|
||||
enabled = state.connected,
|
||||
maxChannels = viewModel.maxChannels,
|
||||
firmwareVersion = state.metadata?.firmwareVersion ?: "0.0.0",
|
||||
enabled = state.connected,
|
||||
onPositiveClicked = { channelListInput -> viewModel.updateChannels(channelListInput, state.channelList) },
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun ChannelSettingsItemList(
|
||||
private fun ChannelSettingsItemList(
|
||||
settingsList: List<ChannelSettings>,
|
||||
loraConfig: LoRaConfig,
|
||||
maxChannels: Int = 8,
|
||||
firmwareVersion: String,
|
||||
enabled: Boolean,
|
||||
onNegativeClicked: () -> Unit = {},
|
||||
onPositiveClicked: (List<ChannelSettings>) -> Unit,
|
||||
) {
|
||||
val primarySettings = settingsList.getOrNull(0) ?: return
|
||||
val modemPresetName by remember(loraConfig) { mutableStateOf(Channel(loraConfig = loraConfig).name) }
|
||||
val primaryChannel by remember(loraConfig) { mutableStateOf(Channel(primarySettings, loraConfig)) }
|
||||
val fwVersion by
|
||||
remember(firmwareVersion) { mutableStateOf(DeviceVersion(firmwareVersion.substringBeforeLast("."))) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val settingsListInput =
|
||||
|
|
@ -180,7 +209,7 @@ fun ChannelSettingsItemList(
|
|||
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState =
|
||||
rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
|
||||
rememberDragDropState(listState) { fromIndex, toIndex ->
|
||||
if (toIndex in settingsListInput.indices && fromIndex in settingsListInput.indices) {
|
||||
settingsListInput.apply { add(toIndex, removeAt(fromIndex)) }
|
||||
}
|
||||
|
|
@ -191,6 +220,7 @@ fun ChannelSettingsItemList(
|
|||
settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
|
||||
|
||||
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var showChannelLegendDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showEditChannelDialog != null) {
|
||||
val index = showEditChannelDialog ?: return
|
||||
|
|
@ -209,6 +239,10 @@ fun ChannelSettingsItemList(
|
|||
)
|
||||
}
|
||||
|
||||
if (showChannelLegendDialog) {
|
||||
ChannelLegendDialog(fwVersion) { showChannelLegendDialog = false }
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().clickable(onClick = {}, enabled = false)) {
|
||||
Column {
|
||||
ChannelsConfigHeader(
|
||||
|
|
@ -230,11 +264,11 @@ fun ChannelSettingsItemList(
|
|||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.primary),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
|
||||
ChannelLegend { showChannelLegendDialog = true }
|
||||
|
||||
val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList())
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
|
||||
state = listState,
|
||||
|
|
@ -245,38 +279,16 @@ fun ChannelSettingsItemList(
|
|||
channel,
|
||||
isDragging,
|
||||
->
|
||||
val channelObj = Channel(channel, loraConfig)
|
||||
ChannelCard(
|
||||
index = index,
|
||||
title = channel.name.ifEmpty { modemPresetName },
|
||||
enabled = enabled,
|
||||
channelSettings = channel,
|
||||
loraConfig = loraConfig,
|
||||
onEditClick = { showEditChannelDialog = index },
|
||||
onDeleteClick = { settingsListInput.removeAt(index) },
|
||||
channel = channelObj,
|
||||
sharesLocation = locationChannel == index,
|
||||
)
|
||||
if (index == 0 && !isDragging) {
|
||||
Text(
|
||||
text = stringResource(R.string.primary_channel_feature),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(text = stringResource(R.string.secondary), color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.secondary_no_telemetry),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.manual_position_request),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceFooter(
|
||||
|
|
@ -286,7 +298,6 @@ fun ChannelSettingsItemList(
|
|||
focusManager.clearFocus()
|
||||
settingsListInput.clear()
|
||||
settingsListInput.addAll(settingsList)
|
||||
onNegativeClicked()
|
||||
},
|
||||
positiveText = R.string.send,
|
||||
onPositiveClicked = {
|
||||
|
|
@ -338,6 +349,33 @@ private fun ChannelsConfigHeader(frequency: Float, slot: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what [Channel] if any is enabled to conduct automatic location sharing.
|
||||
*
|
||||
* @param firmwareVersion of the connected node.
|
||||
* @param settingsList Current list of channels on the node.
|
||||
* @return the index of the channel within `settingsList`.
|
||||
*/
|
||||
private fun determineLocationSharingChannel(firmwareVersion: DeviceVersion, settingsList: List<ChannelSettings>): Int {
|
||||
var output = -1
|
||||
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
|
||||
/* Essentially the first index with the setting enabled */
|
||||
for ((i, settings) in settingsList.withIndex()) {
|
||||
if (settings.moduleSettings.positionPrecision > 0) {
|
||||
output = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Only the primary channel at index 0 can share locations automatically */
|
||||
val primary = settingsList[0]
|
||||
if (primary.moduleSettings.positionPrecision > 0) {
|
||||
output = 0
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ChannelSettingsPreview() {
|
||||
|
|
@ -351,6 +389,7 @@ private fun ChannelSettingsPreview() {
|
|||
channelSettings { name = stringResource(R.string.channel_name) },
|
||||
),
|
||||
loraConfig = Channel.default.loraConfig,
|
||||
firmwareVersion = "1.3.2",
|
||||
enabled = true,
|
||||
onPositiveClicked = {},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue