Feat/2334 channel indicators (#2356)

Signed-off-by: DaneEvans <dane@goneepic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
DaneEvans
2025-07-07 12:31:36 +10:00
committed by GitHub
parent ffd399f44d
commit aa9ce9dfdf
15 changed files with 318 additions and 98 deletions

View File

@@ -176,8 +176,7 @@ data class Contact(
val messageCount: Int,
val isMuted: Boolean,
val isUnmessageable: Boolean,
val nodeColors: Pair<Int, Int>? = null,
val isDefaultPSK: Boolean? = false
val nodeColors: Pair<Int, Int>? = null
)
@Suppress("LongParameterList", "LargeClass")
@@ -520,15 +519,6 @@ class UIViewModel @Inject constructor(
} else {
user.longName
}
val isDefaultPSK = if (toBroadcast) {
val _channel = channelSet.getChannel(data.channel)
val isDefaultPSK = (_channel?.settings?.psk?.size() == 1 &&
_channel.settings.psk.byteAt(0) == 1.toByte()) ||
_channel?.settings?.psk?.toByteArray()?.isEmpty() == true
isDefaultPSK
} else {
false
}
Contact(
contactKey = contactKey,
@@ -544,8 +534,7 @@ class UIViewModel @Inject constructor(
node.colors
} else {
null
},
isDefaultPSK = isDefaultPSK
}
)
}
}.stateIn(

View File

@@ -506,7 +506,7 @@ private fun MainMenuActions(
IconButton(onClick = { showMenu = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Overflow menu",
contentDescription = stringResource(R.string.overflow_menu),
)
}

View File

@@ -129,7 +129,7 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill
)
Icon(
imageVector = if (iaq < 100) Icons.Default.ThumbUp else Icons.Filled.Warning,
contentDescription = "AQI Icon",
contentDescription = stringResource(R.string.air_quality_icon),
tint = Color.White
)
}

View File

@@ -136,6 +136,7 @@ fun ScannedQrCodeDialog(
)
}
itemsIndexed(channelSet.settingsList) { index, channel ->
val channelObj = Channel(channel, channelSet.loraConfig)
ChannelSelection(
index = index,
title = channel.name.ifEmpty { modemPresetName },
@@ -146,6 +147,7 @@ fun ScannedQrCodeDialog(
channelSelections[index] = it
}
},
channel = channelObj,
)
}

View File

@@ -0,0 +1,224 @@
/*
* 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.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.getChannel
private const val PRECISE_POSITION_BITS = 32
/**
* Returns the appropriate security icon composable based on the channel's security settings.
*
* @param isLowEntropyKey Whether the channel uses a low entropy key (0 or 1 byte PSK)
* @param isPreciseLocation Whether the channel has precise location enabled (32 bits)
* @param isMqttEnabled Whether MQTT is enabled (adds warning icon)
* @param contentDescription The content description for the icon
* @return A composable Icon element with appropriate imageVector and tint
*/
@Composable
fun SecurityIcon(
isLowEntropyKey: Boolean,
isPreciseLocation: Boolean = false,
isMqttEnabled: Boolean = false,
contentDescription: String = stringResource(id = R.string.security_icon_description)
) {
val (icon, color, computedDescription) = when {
!isLowEntropyKey -> {
Triple(Icons.Default.Lock, Color.Green, stringResource(id = R.string.security_icon_secure))
}
isPreciseLocation && isMqttEnabled -> {
Triple(Icons.Default.Warning, Color.Red, stringResource(id = R.string.security_icon_warning))
}
isPreciseLocation -> {
Triple(ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
Color.Red,
stringResource(id = R.string.security_icon_insecure_precise))
}
else -> {
Triple(ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
Color.Yellow,
stringResource(id = R.string.security_icon_insecure))
}
}
Icon(
imageVector = icon,
contentDescription = contentDescription + computedDescription,
tint = color
)
}
fun Channel.isLowEntropyKey(): Boolean = settings.psk.size() <= 1
fun Channel.isPreciseLocation(): Boolean = settings.getModuleSettings().positionPrecision == PRECISE_POSITION_BITS
fun Channel.isMqttEnabled(): Boolean = settings.uplinkEnabled
@Composable
fun SecurityIcon(
channel: Channel,
contentDescription: String = stringResource(id = R.string.security_icon_description)
) = SecurityIcon(
channel.isLowEntropyKey(),
channel.isPreciseLocation(),
channel.isMqttEnabled(),
contentDescription
)
@Composable
fun SecurityIcon(
channelSet: AppOnlyProtos.ChannelSet,
channelIndex: Int,
contentDescription: String = stringResource(id = R.string.security_icon_description)
) {
val channel = channelSet.getChannel(channelIndex) ?: return
SecurityIcon(channel, contentDescription)
}
@Composable
fun SecurityIcon(
channelSet: AppOnlyProtos.ChannelSet,
channelName: String,
contentDescription: String = stringResource(id = R.string.security_icon_description)
) {
val channel = channelSet.settingsList.find {
Channel(it, channelSet.loraConfig).name == channelName
}?.let { Channel(it, channelSet.loraConfig) } ?: return
SecurityIcon(channel, contentDescription)
}
// Preview functions for development and testing
@Preview(name = "Secure Channel - Green Lock")
@Composable
private fun PreviewSecureChannel() {
SecurityIcon(
isLowEntropyKey = false,
isPreciseLocation = false,
isMqttEnabled = false
)
}
@Preview(name = "Insecure Channel with Precise Location - Red Unlock")
@Composable
private fun PreviewInsecureChannelWithPreciseLocation() {
SecurityIcon(
isLowEntropyKey = true,
isPreciseLocation = true,
isMqttEnabled = false
)
}
@Preview(name = "Insecure Channel without Precise Location - Yellow Unlock")
@Composable
private fun PreviewInsecureChannelWithoutPreciseLocation() {
SecurityIcon(
isLowEntropyKey = true,
isPreciseLocation = false,
isMqttEnabled = false
)
}
@Preview(name = "MQTT Enabled - Red Warning")
@Composable
private fun PreviewMqttEnabled() {
SecurityIcon(
isLowEntropyKey = false,
isPreciseLocation = false,
isMqttEnabled = true
)
}
@Preview(name = "All Security Icons")
@Composable
private fun PreviewAllSecurityIcons() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"Security Icons Preview",
style = MaterialTheme.typography.headlineSmall
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
SecurityIcon(
isLowEntropyKey = false,
isPreciseLocation = false,
isMqttEnabled = false
)
Text("Secure")
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
SecurityIcon(
isLowEntropyKey = true,
isPreciseLocation = true,
isMqttEnabled = false
)
Text("Insecure + Precise Location")
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
SecurityIcon(
isLowEntropyKey = true,
isPreciseLocation = false,
isMqttEnabled = false
)
Text("Insecure")
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
SecurityIcon(
isLowEntropyKey = false,
isPreciseLocation = false,
isMqttEnabled = true
)
Text("MQTT Enabled")
}
}
}

View File

@@ -42,16 +42,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.common.theme.AppTheme
import androidx.compose.ui.graphics.vector.ImageVector
import com.geeksville.mesh.ui.common.components.SecurityIcon
@Suppress("LongMethod")
@Composable
@@ -61,6 +61,7 @@ fun ContactItem(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
channels: AppOnlyProtos.ChannelSet? = null,
) = with(contact) {
Card(
modifier = modifier
@@ -111,28 +112,29 @@ fun ContactItem(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
Text(
text = longName,
)
Text(
text = longName,
modifier = Modifier.weight(1f)
)
Row(verticalAlignment = Alignment.CenterVertically) {
// Show unlock icon for broadcast with default PSK
val isBroadcast = contact.contactKey.getOrNull(1) == '^' ||
contact.contactKey.endsWith("^all") ||
contact.contactKey.endsWith("^broadcast")
if (isBroadcast && isDefaultPSK == true) {
Spacer(modifier = Modifier.width(10.dp))
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
contentDescription = "Unlocked"
)
if (isBroadcast && channels != null) {
val channelIndex = contact.contactKey[0].digitToIntOrNull()
channelIndex?.let { index ->
SecurityIcon(channels, index)
}
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = lastMessageTime.orEmpty(),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
modifier = Modifier.width(80.dp),
)
}
Text(
text = lastMessageTime.orEmpty(),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
Row(
modifier = Modifier

View File

@@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.UIViewModel
@@ -136,12 +137,14 @@ fun ContactsScreen(
}
}
) { paddingValues ->
val channels by uiViewModel.channels.collectAsStateWithLifecycle()
ContactListView(
contacts = contacts,
selectedList = selectedContactKeys,
onClick = onContactClick,
onLongClick = onContactLongClick,
contentPadding = paddingValues
contentPadding = paddingValues,
channels = channels
)
}
DeleteConfirmationDialog(
@@ -302,7 +305,7 @@ fun SelectionToolbar(
title = { Text(text = "$selectedCount") },
navigationIcon = {
IconButton(onClick = onCloseSelection) {
Icon(Icons.Default.Close, contentDescription = "Close selection")
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.close_selection))
}
},
actions = {
@@ -321,10 +324,10 @@ fun SelectionToolbar(
)
}
IconButton(onClick = onDeleteSelected) {
Icon(Icons.Default.Delete, contentDescription = "Delete selected")
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.delete_selection))
}
IconButton(onClick = onSelectAll) {
Icon(Icons.Default.SelectAll, contentDescription = "Select all")
Icon(Icons.Default.SelectAll, contentDescription = stringResource(R.string.select_all))
}
}
)
@@ -336,7 +339,8 @@ fun ContactListView(
selectedList: List<String>,
onClick: (Contact) -> Unit,
onLongClick: (Contact) -> Unit,
contentPadding: PaddingValues
contentPadding: PaddingValues,
channels: AppOnlyProtos.ChannelSet? = null
) {
val haptics = LocalHapticFeedback.current
LazyColumn(
@@ -355,6 +359,7 @@ fun ContactListView(
onLongClick(contact)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
channels = channels
)
}
}

View File

@@ -494,7 +494,7 @@ private fun DebugMenuActionsPreview() {
) {
Icon(
imageVector = Icons.Outlined.FileDownload,
contentDescription = "Export Logs"
contentDescription = stringResource(id = R.string.debug_logs_export)
)
}
IconButton(
@@ -503,7 +503,7 @@ private fun DebugMenuActionsPreview() {
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Clear All"
contentDescription = stringResource(id = R.string.debug_clear)
)
}
}
@@ -559,7 +559,7 @@ private fun DebugScreenEmptyPreview() {
)
Icon(
imageVector = Icons.TwoTone.FilterAltOff,
contentDescription = "Filter"
contentDescription = stringResource(id = R.string.debug_filters)
)
}
}
@@ -709,7 +709,7 @@ fun DebugMenuActions(
) {
Icon(
imageVector = Icons.Outlined.FileDownload,
contentDescription = "Export Logs"
contentDescription = stringResource(id = R.string.debug_logs_export)
)
}
IconButton(
@@ -718,7 +718,7 @@ fun DebugMenuActions(
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Clear All"
contentDescription = stringResource(id = R.string.debug_clear)
)
}
if (showDeleteLogsDialog) {

View File

@@ -101,7 +101,7 @@ fun DebugCustomFilterInput(
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add filter"
contentDescription = stringResource(id = R.string.debug_filter_add)
)
}
}
@@ -151,7 +151,7 @@ internal fun DebugPresetFilters(
leadingIcon = { if (filter in filterTexts) {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Done icon",
contentDescription = stringResource(id = R.string.debug_filter_included),
)
}
}
@@ -194,7 +194,7 @@ internal fun DebugFilterBar(
} else {
Icons.TwoTone.FilterAltOff
},
contentDescription = "Filter"
contentDescription = stringResource(id = R.string.debug_filters)
)
}
}
@@ -270,7 +270,7 @@ internal fun DebugActiveFilters(
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Clear all filters"
contentDescription = stringResource(id = R.string.debug_filter_clear)
)
}
}

View File

@@ -83,7 +83,7 @@ internal fun DebugSearchNavigation(
) {
Icon(
imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = "Previous match",
contentDescription = stringResource(R.string.debug_search_prev),
modifier = Modifier.size(16.dp)
)
}
@@ -94,7 +94,7 @@ internal fun DebugSearchNavigation(
) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = "Next match",
contentDescription = stringResource(R.string.debug_search_next),
modifier = Modifier.size(16.dp)
)
}
@@ -143,7 +143,7 @@ internal fun DebugSearchBar(
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Clear search",
contentDescription = stringResource(R.string.debug_search_clear),
modifier = Modifier.size(16.dp)
)
}

View File

@@ -77,7 +77,6 @@ import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -87,6 +86,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.QuickChatAction
@@ -99,7 +99,7 @@ import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import kotlinx.coroutines.launch
import androidx.compose.ui.graphics.vector.ImageVector
import com.geeksville.mesh.ui.common.components.SecurityIcon
private const val MESSAGE_CHARACTER_LIMIT = 200
private const val SNIPPET_CHARACTER_LIMIT = 50
@@ -123,22 +123,18 @@ internal fun MessageScreen(
val channelIndex = contactKey[0].digitToIntOrNull()
val nodeId = contactKey.substring(1)
val channels by viewModel.channels.collectAsStateWithLifecycle()
val channelName by remember(channelIndex) {
val unknownChannelText = stringResource(id = R.string.unknown_channel)
val channelName by remember(channelIndex, unknownChannelText) {
derivedStateOf {
channelIndex?.let {
val channel = channels.getChannel(it)
val name = channel?.name ?: "Unknown Channel"
// Check if PSK is the default (base64 'AQ==', i.e., single byte 0x01)
val isDefaultPSK = (channel?.settings?.psk?.size() == 1 &&
channel.settings.psk.byteAt(0) == 1.toByte()) ||
channel?.psk?.toByteArray()?.isEmpty() == true
Pair(name, isDefaultPSK)
} ?: Pair("Unknown Channel", false)
channel?.name ?: unknownChannelText
} ?: unknownChannelText
}
}
val (channelTitle, isDefaultPsk) = channelName
val title = when (nodeId) {
DataPacket.ID_BROADCAST -> channelTitle
DataPacket.ID_BROADCAST -> channelName
else -> viewModel.getUser(nodeId).longName
}
viewModel.setTitle(title)
@@ -212,8 +208,7 @@ internal fun MessageScreen(
}
}
} else {
MessageTopBar(title, channelIndex, mismatchKey, onNavigateBack, isDefaultPsk = isDefaultPsk
)
MessageTopBar(title, channelIndex, mismatchKey, onNavigateBack, channels, channelIndex)
}
},
) { padding ->
@@ -455,20 +450,18 @@ private fun MessageTopBar(
channelIndex: Int?,
mismatchKey: Boolean = false,
onNavigateBack: () -> Unit,
isDefaultPsk: Boolean = false
channels: AppOnlyProtos.ChannelSet,
channelIndexParam: Int?,
) = TopAppBar(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = title)
if (isDefaultPsk
) {
Spacer(modifier = Modifier.width(10.dp))
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
contentDescription = "Unlocked"
)
}
Spacer(modifier = Modifier.width(10.dp))
channelIndexParam?.let { index ->
SecurityIcon(channels, index)
}
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {

View File

@@ -255,7 +255,7 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
) {
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = "warning",
contentDescription = stringResource(id = R.string.warning),
modifier = Modifier.padding(end = 8.dp)
)
Text(
@@ -263,7 +263,7 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
)
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = "warning",
contentDescription = stringResource(id = R.string.warning),
modifier = Modifier.padding(start = 8.dp)
)
}

View File

@@ -62,7 +62,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -80,7 +79,7 @@ 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 androidx.compose.ui.graphics.vector.ImageVector
import com.geeksville.mesh.ui.common.components.SecurityIcon
@Composable
private fun ChannelItem(
@@ -125,20 +124,15 @@ fun ChannelCard(
enabled: Boolean,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
isDefaultPSK: Boolean = false,
channel: Channel,
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = onEditClick,
) {
if (isDefaultPSK) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
contentDescription = "Unlocked"
)
Spacer(modifier = Modifier.width(10.dp))
}
SecurityIcon(channel)
Spacer(modifier = Modifier.width(10.dp))
IconButton(onClick = { onDeleteClick() }) {
Icon(
imageVector = Icons.TwoTone.Close,
@@ -155,19 +149,15 @@ fun ChannelSelection(
enabled: Boolean,
isSelected: Boolean,
onSelected: (Boolean) -> Unit,
isDefaultPSK: Boolean = false,
channel: Channel,
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = {},
) {
if (isDefaultPSK) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_lock_open_right_24),
contentDescription = "Unlocked"
)
}
SecurityIcon(channel)
Spacer(modifier = Modifier.width(10.dp))
Checkbox(
enabled = enabled,
checked = isSelected,
@@ -292,15 +282,14 @@ fun ChannelSettingsItemList(
items = settingsListInput,
dragDropState = dragDropState,
) { index, channel, isDragging ->
val isDefaultPSK = (channel.psk.size() == 1 && channel.psk.byteAt(0) == 1.toByte()) ||
channel.psk.toByteArray().isEmpty()
val channelObj = Channel(channel, loraConfig)
ChannelCard(
index = index,
title = channel.name.ifEmpty { primaryChannel.name },
enabled = enabled,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { settingsListInput.removeAt(index) },
isDefaultPSK = isDefaultPSK
channel = channelObj
)
if (index == 0 && !isDragging) {
Text(

View File

@@ -448,8 +448,7 @@ private fun ChannelListView(
AdaptiveTwoPane(
first = {
channelSet.settingsList.forEachIndexed { index, channel ->
val isDefaultPSK = (channel.psk.size() == 1 && channel.psk.byteAt(0) == 1.toByte()) ||
channel.psk.toByteArray().isEmpty()
val channelObj = Channel(channel, channelSet.loraConfig)
val displayTitle = channel.name.ifEmpty { modemPresetName }
ChannelSelection(
@@ -462,7 +461,7 @@ private fun ChannelListView(
channelSelections[index] = it
}
},
isDefaultPSK = isDefaultPSK
channel = channelObj
)
}
OutlinedButton(

View File

@@ -167,6 +167,12 @@
<string name="debug_filters">Filters</string>
<string name="debug_active_filters">Active filters</string>
<string name="debug_default_search">Search in logs…</string>
<string name="debug_search_next">Next match</string>
<string name="debug_search_prev">Previous match</string>
<string name="debug_search_clear">Clear search</string>
<string name="debug_filter_add">Add filter</string>
<string name="debug_filter_included">Filter included</string>
<string name="debug_filter_clear">Clear all filters</string>
<string name="debug_clear">Clear Logs</string>
<string name="match_any">Match Any | All</string>
<string name="match_all">Match All | Any</string>
@@ -219,6 +225,8 @@
<string name="delete_for_everyone">Delete for everyone</string>
<string name="delete_for_me">Delete for me</string>
<string name="select_all">Select all</string>
<string name="close_selection">Close selection</string>
<string name="delete_selection">Delete selected</string>
<string name="modem_config_slow_long">Long Range / Slow</string>
<string name="map_style_selection">Style Selection</string>
<string name="map_download_region">Download Region</string>
@@ -604,6 +612,7 @@
<string name="environment_metrics_use_fahrenheit">Environment metrics use Fahrenheit</string>
<string name="air_quality_metrics_module_enabled">Air quality metrics module enabled</string>
<string name="air_quality_metrics_update_interval_seconds">Air quality metrics update interval (seconds)</string>
<string name="air_quality_icon">Air quality icon</string>
<string name="power_metrics_module_enabled">Power metrics module enabled</string>
<string name="power_metrics_update_interval_seconds">Power metrics update interval (seconds)</string>
<string name="power_metrics_on_screen_enabled">Power metrics on-screen enabled</string>
@@ -711,4 +720,12 @@
<string name="app_intro_next_button">Next</string>
<string name="app_intro_done_button">Done</string>
<string name="app_intro_skip_button">Skip</string>
<string name="security_icon_description">Security Status</string>
<string name="security_icon_secure"> - Secure</string>
<string name="security_icon_warning"> - WARN, insecure MQTT</string>
<string name="security_icon_insecure"> - WARN, low entropy key</string>
<string name="security_icon_insecure_precise"> - WARNING, insecure location enabled</string>
<string name="unknown_channel">Unknown Channel</string>
<string name="warning">Warning</string>
<string name="overflow_menu">Overflow menu</string>
</resources>