Modularize common composables (#3286)

This commit is contained in:
Phil Oliver 2025-10-02 05:56:49 -04:00 committed by GitHub
parent 81804500bd
commit fe9491121c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 110 additions and 84 deletions

View file

@ -8,19 +8,16 @@
<ID>CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */</ID>
<ID>ComposableNaming:NodeDetailScreen.kt$notesSection</ID>
<ID>ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList</ID>
<ID>ComposableParamOrder:Debug.kt$DebugMenuActions</ID>
<ID>ComposableParamOrder:Debug.kt$DecodedPayloadBlock</ID>
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchState</ID>
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchStateviewModelDefaults</ID>
<ID>ComposableParamOrder:DeviceMetrics.kt$DeviceMetricsChart</ID>
<ID>ComposableParamOrder:EditBase64Preference.kt$EditBase64Preference</ID>
<ID>ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$ChartContent</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
<ID>ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem</ID>
<ID>ComposableParamOrder:HostMetricsLog.kt$LogLine</ID>
<ID>ComposableParamOrder:MainAppBar.kt$MainAppBar</ID>
<ID>ComposableParamOrder:MapReportingPreference.kt$MapReportingPreference</ID>
<ID>ComposableParamOrder:Message.kt$MessageScreen</ID>
<ID>ComposableParamOrder:Message.kt$QuickChatRow</ID>
@ -69,7 +66,6 @@
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onClickChip</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
@ -105,9 +101,6 @@
<ID>MagicNumber:Debug.kt$3</ID>
<ID>MagicNumber:EditChannelDialog.kt$16</ID>
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
<ID>MagicNumber:EditListPreference.kt$12</ID>
<ID>MagicNumber:EditListPreference.kt$12345</ID>
<ID>MagicNumber:EditListPreference.kt$67890</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$1000L</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$30</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$31</ID>
@ -161,8 +154,6 @@
<ID>ModifierMissing:Contacts.kt$ContactsScreen</ID>
<ID>ModifierMissing:Contacts.kt$SelectionToolbar</ID>
<ID>ModifierMissing:DeviceMetrics.kt$DeviceMetricsScreen</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPicker</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPickerDialog</ID>
<ID>ModifierMissing:EmptyStateContent.kt$EmptyStateContent</ID>
<ID>ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen</ID>
<ID>ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen</ID>
@ -181,12 +172,10 @@
<ID>ModifierMissing:PositionLog.kt$PositionItem</ID>
<ID>ModifierMissing:PositionLog.kt$PositionLogScreen</ID>
<ID>ModifierMissing:PowerMetrics.kt$PowerMetricsScreen</ID>
<ID>ModifierMissing:PreferenceDivider.kt$PreferenceDivider</ID>
<ID>ModifierMissing:RadioConfig.kt$RadioConfigItemList</ID>
<ID>ModifierMissing:RadioConfigScreenList.kt$RadioConfigScreenList</ID>
<ID>ModifierMissing:Reaction.kt$ReactionDialog</ID>
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
@ -276,15 +265,12 @@
<ID>ParameterNaming:ContactSharing.kt$onSharedContactRequested</ID>
<ID>ParameterNaming:Contacts.kt$onDeleteSelected</ID>
<ID>ParameterNaming:Contacts.kt$onMuteSelected</ID>
<ID>ParameterNaming:DropDownPreference.kt$onItemSelected</ID>
<ID>ParameterNaming:EditListPreference.kt$onValuesChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPositionPrecisionChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged</ID>
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
<ID>ParameterNaming:NodeDetailScreen.kt$onFirmwareSelected</ID>
<ID>ParameterNaming:PositionPrecisionPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:UsbDevices.kt$onDeviceSelected</ID>
<ID>ParameterNaming:WelcomeScreen.kt$onGetStarted</ID>
<ID>PreviewPublic:Channel.kt$ModemPresetInfoPreview</ID>
@ -335,7 +321,6 @@
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$title: String</ID>
<ID>UnusedParameter:DropDownPreference.kt$modifier: Modifier = Modifier</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
<ID>ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)</ID>
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>

View file

@ -64,10 +64,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.geeksville.mesh.MeshProtos.Waypoint
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
import com.geeksville.mesh.waypoint
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.theme.AppTheme
import java.text.SimpleDateFormat
import java.util.Calendar

View file

@ -64,8 +64,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.geeksville.mesh.MeshProtos.Waypoint
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale

View file

@ -28,9 +28,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.map.NodeMapViewModel
import org.meshtastic.core.ui.component.MainAppBar
const val DEG_D = 1e-7

View file

@ -87,7 +87,6 @@ import com.geeksville.mesh.navigation.nodesGraph
import com.geeksville.mesh.navigation.settingsGraph
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
@ -107,6 +106,7 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.icon.Conversations

View file

@ -1,33 +0,0 @@
/*
* 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
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs
import javax.inject.Inject
@HiltViewModel
class EmojiPickerViewModel @Inject constructor(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() {
var customEmojiFrequency: String?
get() = customEmojiPrefs.customEmojiFrequency
set(value) {
customEmojiPrefs.customEmojiFrequency = value
}
}

View file

@ -1,145 +0,0 @@
/*
* 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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.protobuf.ProtocolMessageEnum
@Composable
fun <T : Enum<T>> DropDownPreference(
title: String,
enabled: Boolean,
selectedItem: T,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
DropDownPreference(
title = title,
enabled = enabled,
items =
selectedItem.declaringJavaClass.enumConstants?.filter { it.name != "UNRECOGNIZED" }?.map { it to it.name }
?: emptyList(),
selectedItem = selectedItem,
onItemSelected = onItemSelected,
modifier = modifier,
summary = summary,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DropDownPreference(
title: String,
enabled: Boolean,
items: List<Pair<T, String>>,
selectedItem: T,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
var expanded by remember { mutableStateOf(false) }
val deprecatedItems: List<T> = remember {
if (selectedItem is ProtocolMessageEnum) {
val enum = (selectedItem as? Enum<*>)?.declaringJavaClass?.enumConstants
val descriptor = (selectedItem as ProtocolMessageEnum).descriptorForType
@Suppress("UNCHECKED_CAST")
enum?.filter { entries -> descriptor.values.any { it.name == entries.name && it.options.deprecated } }
as? List<T> ?: emptyList() // Safe cast to List<T> or return emptyList if cast fails
} else {
emptyList()
}
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = "",
onValueChange = {},
prefix = { Text(title) },
suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors =
ExposedDropdownMenuDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
enabled = enabled,
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
onItemSelected(selectionOption.first)
expanded = false
},
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun DropDownPreferencePreview() {
DropDownPreference(
title = "Settings",
summary = "Lorem ipsum dolor sit amet",
enabled = true,
items = listOf("TEST1" to "text1", "TEST2" to "text2"),
selectedItem = "TEST2",
onItemSelected = {},
)
}

View file

@ -1,152 +0,0 @@
/*
* 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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.util.encodeToString
import com.geeksville.mesh.util.toByteString
import com.google.protobuf.ByteString
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
@Suppress("LongMethod")
@Composable
fun EditBase64Preference(
modifier: Modifier = Modifier,
title: String,
summary: String? = null,
value: ByteString,
enabled: Boolean,
readOnly: Boolean = false,
keyboardActions: KeyboardActions,
onValueChange: (ByteString) -> Unit,
onGenerateKey: (() -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
var valueState by remember { mutableStateOf(value.encodeToString()) }
val isError = value.encodeToString() != valueState
// don't update values while the user is editing
var isFocused by remember { mutableStateOf(false) }
LaunchedEffect(value) {
if (!isFocused) {
valueState = value.encodeToString()
}
}
val (icon, description) =
when {
isError -> Icons.TwoTone.Close to stringResource(R.string.error)
onGenerateKey != null && !isFocused -> Icons.TwoTone.Refresh to stringResource(R.string.reset)
else -> null to null
}
Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
OutlinedTextField(
value = valueState,
onValueChange = {
valueState = it
runCatching { it.toByteString() }.onSuccess(onValueChange)
},
modifier = Modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused },
enabled = enabled,
readOnly = readOnly,
label = { Text(text = title) },
isError = isError,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
trailingIcon = {
if (icon != null) {
IconButton(
onClick = {
if (isError) {
valueState = value.encodeToString()
onValueChange(value)
} else if (onGenerateKey != null && !isFocused) {
onGenerateKey()
}
},
enabled = enabled,
) {
Icon(
imageVector = icon,
contentDescription = description,
tint =
if (isError) {
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
},
)
}
} else if (trailingIcon != null) {
trailingIcon()
}
},
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun EditBase64PreferencePreview() {
EditBase64Preference(
title = "Title",
summary = "This is a summary",
value = Channel.getRandomKey(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChange = {},
onGenerateKey = {},
modifier = Modifier.padding(16.dp),
)
}

View file

@ -1,214 +0,0 @@
/*
* 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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ModuleConfigProtos.RemoteHardwarePin
import com.geeksville.mesh.ModuleConfigProtos.RemoteHardwarePinType
import com.geeksville.mesh.copy
import com.geeksville.mesh.remoteHardwarePin
import com.google.protobuf.ByteString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
@Suppress("LongMethod")
@Composable
inline fun <reified T> EditListPreference(
title: String,
list: List<T>,
maxCount: Int,
enabled: Boolean,
keyboardActions: KeyboardActions,
crossinline onValuesChanged: (List<T>) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
val focusManager = LocalFocusManager.current
val listState = remember(list) { mutableStateListOf<T>().apply { addAll(list) } }
Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Text(text = title, style = MaterialTheme.typography.titleLarge)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
listState.forEachIndexed { index, value ->
val trailingIcon =
@Composable {
IconButton(
onClick = {
focusManager.clearFocus()
listState.removeAt(index)
onValuesChanged(listState)
},
) {
Icon(
imageVector = Icons.TwoTone.Close,
contentDescription = stringResource(R.string.delete),
modifier = Modifier.wrapContentSize(),
)
}
}
when (value) {
is Int -> {
EditTextPreference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = it as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
}
is ByteString -> {
EditBase64Preference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChange = {
listState[index] = it as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
}
is RemoteHardwarePin -> {
EditTextPreference(
title = stringResource(R.string.gpio_pin),
value = value.gpioPin,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
if (it in 0..255) {
listState[index] = value.copy { gpioPin = it } as T
onValuesChanged(listState)
}
},
)
EditTextPreference(
title = stringResource(R.string.name),
value = value.name,
maxSize = 14, // name max_size:15
enabled = enabled,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = value.copy { name = it } as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
DropDownPreference(
title = stringResource(R.string.type),
enabled = enabled,
items =
RemoteHardwarePinType.entries
.filter { it != RemoteHardwarePinType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = value.type,
onItemSelected = {
listState[index] = value.copy { type = it } as T
onValuesChanged(listState)
},
)
}
}
}
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
// Add element based on the type T
val newElement =
when (T::class) {
Int::class -> 0 as T
ByteString::class -> ByteString.EMPTY as T
RemoteHardwarePin::class -> remoteHardwarePin {} as T
else -> throw IllegalArgumentException("Unsupported type: ${T::class}")
}
listState.add(listState.size, newElement)
},
enabled = maxCount > listState.size,
) {
Text(text = stringResource(R.string.add))
}
}
}
@Preview(showBackground = true)
@Composable
private fun EditListPreferencePreview() {
Column {
EditListPreference(
title = stringResource(R.string.ignore_incoming),
summary = "This is a summary",
list = listOf(12345, 67890),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
EditListPreference(
title = "Available pins",
list =
listOf(
remoteHardwarePin {
gpioPin = 12
name = "Front door"
type = RemoteHardwarePinType.DIGITAL_READ
},
),
maxCount = 4,
enabled = true,
keyboardActions = KeyboardActions {},
onValuesChanged = {},
)
}
}

View file

@ -1,70 +0,0 @@
/*
* 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.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.emoji2.emojipicker.RecentEmojiProviderAdapter
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.geeksville.mesh.ui.common.EmojiPickerViewModel
import com.geeksville.mesh.util.CustomRecentEmojiProvider
import org.meshtastic.core.ui.component.BottomSheetDialog
@Composable
fun EmojiPicker(
viewModel: EmojiPickerViewModel = hiltViewModel(),
onDismiss: () -> Unit = {},
onConfirm: (String) -> Unit,
) {
Column(verticalArrangement = Arrangement.Bottom) {
BackHandler { onDismiss() }
AndroidView(
factory = { context ->
androidx.emoji2.emojipicker.EmojiPickerView(context).apply {
clipToOutline = true
setRecentEmojiProvider(
RecentEmojiProviderAdapter(
CustomRecentEmojiProvider(viewModel.customEmojiFrequency) { updatedValue ->
viewModel.customEmojiFrequency = updatedValue
},
),
)
setOnEmojiPickedListener { emoji ->
onDismiss()
onConfirm(emoji.emoji)
}
}
},
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background),
)
}
}
@Composable
fun EmojiPickerDialog(onDismiss: () -> Unit = {}, onConfirm: (String) -> Unit) =
BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .4f)) {
EmojiPicker(onConfirm = onConfirm, onDismiss = onDismiss)
}

View file

@ -1,173 +0,0 @@
/*
* 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.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("CyclomaticComplexMethod")
@Composable
fun MainAppBar(
modifier: Modifier = Modifier,
navController: NavHostController,
ourNode: Node?,
onClickChip: (Node) -> Unit,
) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
if (currentDestination?.hasRoute<ContactsRoutes.Messages>() == true) {
return
}
val title: String =
when {
currentDestination == null -> ""
currentDestination.hasRoute<SettingsRoutes.DebugPanel>() -> stringResource(id = R.string.debug_panel)
currentDestination.hasRoute<ContactsRoutes.QuickChat>() -> stringResource(id = R.string.quick_chat)
currentDestination.hasRoute<ContactsRoutes.Share>() -> stringResource(id = R.string.share_to)
else -> ""
}
MainAppBar(
modifier = modifier,
title = title,
subtitle = null,
canNavigateUp = navController.previousBackStackEntry != null,
ourNode = ourNode,
showNodeChip = false,
onNavigateUp = navController::navigateUp,
actions = {},
onClickChip = onClickChip,
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MainAppBar(
modifier: Modifier = Modifier,
title: String,
subtitle: String? = null,
ourNode: Node?,
showNodeChip: Boolean,
canNavigateUp: Boolean,
onNavigateUp: () -> Unit,
actions: @Composable () -> Unit,
onClickChip: (Node) -> Unit,
) {
TopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
)
},
subtitle = { subtitle?.let { Text(text = it) } },
modifier = modifier,
navigationIcon =
if (canNavigateUp) {
{
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.navigate_back),
)
}
}
} else {
{
IconButton(enabled = false, onClick = {}) {
Icon(
imageVector = ImageVector.vectorResource(id = com.geeksville.mesh.R.drawable.app_icon),
contentDescription = stringResource(id = R.string.application_icon),
)
}
}
},
actions = {
TopBarActions(ourNode = ourNode, showNodeChip = showNodeChip, actions = actions, onClickChip = onClickChip)
},
)
}
@Composable
private fun TopBarActions(
ourNode: Node?,
showNodeChip: Boolean,
actions: @Composable () -> Unit,
onClickChip: (Node) -> Unit,
) {
AnimatedVisibility(visible = showNodeChip, enter = fadeIn(), exit = fadeOut()) {
ourNode?.let { node ->
NodeChip(modifier = Modifier.padding(horizontal = 16.dp), node = node, onClick = onClickChip)
}
}
actions()
}
@PreviewLightDark
@Composable
private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavigateUp: Boolean) {
AppTheme {
MainAppBar(
title = "Title",
subtitle = "Subtitle",
ourNode = previewNode,
showNodeChip = true,
canNavigateUp = canNavigateUp,
onNavigateUp = {},
actions = {},
) {}
}
}

View file

@ -1,115 +0,0 @@
/*
* 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.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.model.util.DistanceUnit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SwitchPreference
import kotlin.math.pow
import kotlin.math.roundToInt
private const val POSITION_ENABLED = 32
private const val POSITION_DISABLED = 0
private const val POSITION_PRECISION_MIN = 10
private const val POSITION_PRECISION_MAX = 19
private const val POSITION_PRECISION_DEFAULT = 13
@Suppress("MagicNumber")
fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble())
@Composable
fun PositionPrecisionPreference(
value: Int,
enabled: Boolean,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val unit = remember { DistanceUnit.getFromLocale() }
Column(modifier = modifier) {
SwitchPreference(
title = stringResource(R.string.position_enabled),
checked = value != POSITION_DISABLED,
enabled = enabled,
onCheckedChange = { enabled ->
val newValue = if (enabled) POSITION_ENABLED else POSITION_DISABLED
onValueChanged(newValue)
},
padding = PaddingValues(0.dp),
)
if (value != POSITION_DISABLED) {
SwitchPreference(
title = stringResource(R.string.precise_location),
checked = value == POSITION_ENABLED,
enabled = enabled,
onCheckedChange = { enabled ->
val newValue = if (enabled) POSITION_ENABLED else POSITION_PRECISION_DEFAULT
onValueChanged(newValue)
},
padding = PaddingValues(0.dp),
)
}
if (value in (POSITION_DISABLED + 1) until POSITION_ENABLED) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Slider(
value = value.toFloat(),
onValueChange = { onValueChanged(it.roundToInt()) },
enabled = enabled,
valueRange = POSITION_PRECISION_MIN.toFloat()..POSITION_PRECISION_MAX.toFloat(),
steps = POSITION_PRECISION_MAX - POSITION_PRECISION_MIN - 1,
)
val precisionMeters = precisionBitsToMeters(value).toInt()
Text(
text = precisionMeters.toDistanceString(unit),
modifier = Modifier.padding(bottom = 16.dp),
fontSize = MaterialTheme.typography.bodyLarge.fontSize,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PositionPrecisionPreferencePreview() {
PositionPrecisionPreference(
value = POSITION_PRECISION_DEFAULT,
enabled = true,
onValueChanged = {},
modifier = Modifier.padding(horizontal = 16.dp),
)
}

View file

@ -1,29 +0,0 @@
/*
* 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.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceDivider() {
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
}

View file

@ -1,42 +0,0 @@
/*
* 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/>.
*/
@file:Suppress("MatchingDeclarationName")
package com.geeksville.mesh.ui.common.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.TelemetryProtos
import org.meshtastic.core.database.model.Node
/** Simple [PreviewParameterProvider] that provides true and false values. */
class BooleanProvider : PreviewParameterProvider<Boolean> {
override val values: Sequence<Boolean> = sequenceOf(false, true)
}
private val user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build()
val previewNode =
Node(
num = 13444,
user = user,
isIgnored = false,
paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
environmentMetrics =
TelemetryProtos.EnvironmentMetrics.newBuilder().setTemperature(25f).setRelativeHumidity(60f).build(),
)

View file

@ -1,542 +0,0 @@
/*
* 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/>.
*/
@file:Suppress("TooManyFunctions")
package com.geeksville.mesh.ui.common.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.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 org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
private const val PRECISE_POSITION_BITS = 32
/**
* Represents the various visual states of the security icon as an enum. Each enum constant encapsulates the icon,
* color, descriptive text, and optional badge details.
*
* @property icon The primary vector graphic for the icon.
* @property color The tint color for the primary icon.
* @property descriptionResId The string resource ID for the accessibility description of the icon's state.
* @property helpTextResId The string resource ID for the detailed help text associated with this state.
* @property badgeIcon Optional vector graphic for a badge to be displayed on the icon.
* @property badgeIconColor Optional tint color for the badge icon.
*/
@Immutable
enum class SecurityState(
@Stable val icon: ImageVector,
@Stable val color: @Composable () -> Color,
@StringRes val descriptionResId: Int,
@StringRes val helpTextResId: Int,
@Stable val badgeIcon: ImageVector? = null,
@Stable val badgeIconColor: @Composable () -> Color? = { null },
) {
/** State for a secure channel (green lock). */
SECURE(
icon = Icons.Filled.Lock,
color = { colorScheme.StatusGreen },
descriptionResId = R.string.security_icon_secure,
helpTextResId = R.string.security_icon_help_green_lock,
),
/**
* State for an insecure channel, not used for precise location, and MQTT not the primary concern for a higher
* warning. (yellow open lock)
*/
INSECURE_NO_PRECISE(
icon = Icons.Filled.LockOpen,
color = { colorScheme.StatusYellow },
descriptionResId = R.string.security_icon_insecure_no_precise,
helpTextResId = R.string.security_icon_help_yellow_open_lock,
),
/**
* State for an insecure channel with precise location enabled, but MQTT not causing the highest warning. (red open
* lock)
*/
INSECURE_PRECISE_ONLY(
icon = Icons.Filled.LockOpen,
color = { colorScheme.StatusRed },
descriptionResId = R.string.security_icon_insecure_precise_only,
helpTextResId = R.string.security_icon_help_red_open_lock,
),
/**
* State indicating an insecure channel with precise location and MQTT enabled (red open lock with yellow warning
* badge).
*/
INSECURE_PRECISE_MQTT_WARNING(
icon = Icons.Filled.LockOpen,
color = { colorScheme.StatusRed },
descriptionResId = R.string.security_icon_warning_precise_mqtt,
helpTextResId = R.string.security_icon_help_warning_precise_mqtt,
badgeIcon = Icons.Filled.Warning,
badgeIconColor = { colorScheme.StatusYellow },
),
}
/**
* Internal composable to display the security icon, potentially with a badge.
*
* @param icon The main vector graphic for the icon.
* @param mainIconTint The tint color for the main icon.
* @param contentDescription The accessibility description for the icon.
* @param modifier Modifier for this composable.
* @param badgeIcon Optional vector graphic for the badge.
* @param badgeIconColor Optional tint color for the badge icon.
*/
@Composable
private fun SecurityIconDisplay(
icon: ImageVector,
mainIconTint: Color,
contentDescription: String,
modifier: Modifier = Modifier,
badgeIcon: ImageVector? = null,
badgeIconColor: Color? = null,
) {
BadgedBox(
badge = {
if (badgeIcon != null) {
Badge(
containerColor = Color.Transparent, // Allows badgeIconColor to define appearance
) {
Icon(
imageVector = badgeIcon,
contentDescription = stringResource(R.string.security_icon_badge_warning_description),
tint = badgeIconColor ?: colorScheme.onError, // Default for contrast
modifier = Modifier.size(16.dp), // Adjusted badge icon size
)
}
}
},
modifier = modifier,
) {
Icon(imageVector = icon, contentDescription = contentDescription, tint = mainIconTint)
}
}
/**
* Determines the [SecurityState] based on channel properties. The priority of states is: MQTT warning, then secure,
* then insecure variations.
*
* @param isLowEntropyKey True if the channel uses a low entropy key (not securely encrypted).
* @param isPreciseLocation True if precise location is enabled.
* @param isMqttEnabled True if MQTT is enabled for the channel.
* @return The determined [SecurityState].
*/
private fun determineSecurityState(
isLowEntropyKey: Boolean,
isPreciseLocation: Boolean,
isMqttEnabled: Boolean,
): SecurityState = when {
!isLowEntropyKey -> SecurityState.SECURE
isMqttEnabled && isPreciseLocation -> SecurityState.INSECURE_PRECISE_MQTT_WARNING
isPreciseLocation -> SecurityState.INSECURE_PRECISE_ONLY
else -> SecurityState.INSECURE_NO_PRECISE
}
/**
* Displays an icon representing the security status of a channel. Clicking the icon shows a detailed help dialog.
*
* @param securityState The current [SecurityState] to display.
* @param baseContentDescription The base content description for the icon, to which the specific state description will
* be appended. Defaults to a generic security icon description.
* @param externalOnClick Optional lambda to be invoked when the icon is clicked, in addition to its primary action
* (showing a help dialog). This allows callers to inject custom side effects.
*/
@Composable
fun SecurityIcon(
securityState: SecurityState,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) {
var showHelpDialog by rememberSaveable { mutableStateOf(false) }
val fullContentDescription = baseContentDescription + " " + stringResource(id = securityState.descriptionResId)
IconButton(
onClick = {
showHelpDialog = true
externalOnClick?.invoke()
},
) {
SecurityIconDisplay(
icon = securityState.icon,
mainIconTint = securityState.color.invoke(),
contentDescription = fullContentDescription,
badgeIcon = securityState.badgeIcon,
badgeIconColor = securityState.badgeIconColor.invoke(),
)
}
if (showHelpDialog) {
SecurityHelpDialog(securityState = securityState, onDismiss = { showHelpDialog = false })
}
}
/**
* Overload for [SecurityIcon] that derives the [SecurityState] from boolean flags.
*
* @param isLowEntropyKey Whether the channel uses a low entropy key.
* @param isPreciseLocation Whether the channel has precise location enabled. Defaults to false.
* @param isMqttEnabled Whether MQTT is enabled for the channel. Defaults to false.
* @param baseContentDescription The base content description for the icon.
* @param externalOnClick Optional lambda to be invoked when the icon is clicked, in addition to its primary action
* (showing a help dialog). This allows callers to inject custom side effects.
*/
@Composable
fun SecurityIcon(
isLowEntropyKey: Boolean,
isPreciseLocation: Boolean = false,
isMqttEnabled: Boolean = false,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) {
val securityState = determineSecurityState(isLowEntropyKey, isPreciseLocation, isMqttEnabled)
SecurityIcon(
securityState = securityState,
baseContentDescription = baseContentDescription,
externalOnClick = externalOnClick,
)
}
/** Extension property to check if the channel uses a low entropy PSK (not securely encrypted). */
val Channel.isLowEntropyKey: Boolean
get() = settings.psk.size() <= 1
/** Extension property to check if the channel has precise location enabled. */
val Channel.isPreciseLocation: Boolean
get() = settings.moduleSettings.positionPrecision == PRECISE_POSITION_BITS
/** Extension property to check if MQTT is enabled for the channel. */
val Channel.isMqttEnabled: Boolean
get() = settings.uplinkEnabled
/**
* Overload for [SecurityIcon] that takes a [Channel] object to determine its security state.
*
* @param channel The channel whose security status is to be displayed.
* @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(
channel: Channel,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) = SecurityIcon(
isLowEntropyKey = channel.isLowEntropyKey,
isPreciseLocation = channel.isPreciseLocation,
isMqttEnabled = channel.isMqttEnabled,
baseContentDescription = baseContentDescription,
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.
*
* @param channelSet The set of channels.
* @param channelIndex The index of the channel within the set.
* @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(
channelSet: AppOnlyProtos.ChannelSet,
channelIndex: Int,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) {
channelSet.getChannel(channelIndex)?.let { channel ->
SecurityIcon(
channel = channel,
baseContentDescription = baseContentDescription,
externalOnClick = externalOnClick,
)
}
}
/**
* Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel name. If a channel with the given
* name is not found, nothing is rendered. This overload optimizes lookup by name by memoizing a map of channel names to
* settings.
*
* @param channelSet The set of channels.
* @param channelName The name of the channel to find.
* @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(
channelSet: AppOnlyProtos.ChannelSet,
channelName: String,
baseContentDescription: String = stringResource(id = R.string.security_icon_description),
externalOnClick: (() -> Unit)? = null,
) {
val channelByNameMap =
remember(channelSet) { channelSet.settingsList.associateBy { Channel(it, channelSet.loraConfig).name } }
channelByNameMap[channelName]?.let { channelSetting ->
SecurityIcon(
channel = Channel(channelSetting, channelSet.loraConfig),
baseContentDescription = baseContentDescription,
externalOnClick = externalOnClick,
)
}
}
/**
* Displays a help dialog explaining the meaning of different security icons. The dialog can show details for a specific
* [SecurityState] or a list of all states.
*
* @param securityState The initial security state to display contextually.
* @param onDismiss Lambda invoked when the dialog is dismissed.
*/
@Composable
private fun SecurityHelpDialog(securityState: SecurityState, onDismiss: () -> Unit) {
var showAll by rememberSaveable { mutableStateOf(false) }
AlertDialog(
modifier =
if (showAll) {
Modifier.fillMaxSize()
} else {
Modifier
},
onDismissRequest = onDismiss,
title = {
Text(
if (showAll) {
stringResource(R.string.security_icon_help_title_all)
} else {
stringResource(R.string.security_icon_help_title)
},
)
},
text = {
if (showAll) {
AllSecurityStates()
} else {
ContextualSecurityState(securityState)
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onClick = { showAll = !showAll }) {
Text(
if (showAll) {
stringResource(R.string.security_icon_help_show_less)
} else {
stringResource(R.string.security_icon_help_show_all)
},
)
}
TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) }
}
},
)
}
/**
* Displays details for a single, specific security state within the help dialog.
*
* @param securityState The state to display.
*/
@Composable
private fun ContextualSecurityState(securityState: SecurityState) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
SecurityIconDisplay(
icon = securityState.icon,
mainIconTint = securityState.color.invoke(),
contentDescription = stringResource(securityState.descriptionResId),
modifier = Modifier.size(48.dp),
badgeIcon = securityState.badgeIcon,
badgeIconColor = securityState.badgeIconColor.invoke(),
)
Spacer(Modifier.height(16.dp))
Text(text = stringResource(securityState.helpTextResId), style = MaterialTheme.typography.bodyMedium)
}
}
/**
* Displays a list of all possible security states with their icons and descriptions within the help dialog. Iterates
* over `SecurityState.entries` which is provided by the enum class.
*/
@Composable
private fun AllSecurityStates() {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
SecurityState.entries.forEach { state ->
// Uses enum entries
Row(verticalAlignment = Alignment.CenterVertically) {
SecurityIconDisplay(
icon = state.icon,
mainIconTint = state.color.invoke(),
contentDescription = stringResource(state.descriptionResId),
modifier = Modifier.size(48.dp),
badgeIcon = state.badgeIcon,
badgeIconColor = state.badgeIconColor.invoke(),
)
Column(modifier = Modifier.padding(start = 16.dp)) {
Text(text = stringResource(state.descriptionResId), style = MaterialTheme.typography.titleMedium)
Text(text = stringResource(state.helpTextResId), style = MaterialTheme.typography.bodyMedium)
}
}
if (state != SecurityState.entries.lastOrNull()) {
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
}
}
// Preview functions for development and testing
@Preview(name = "Secure Channel Icon")
@Composable
private fun PreviewSecureChannel() {
SecurityIcon(securityState = SecurityState.SECURE)
}
@Preview(name = "Insecure Precise Icon")
@Composable
private fun PreviewInsecureChannelWithPreciseLocation() {
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY)
}
@Preview(name = "Insecure Channel Icon")
@Composable
private fun PreviewInsecureChannelWithoutPreciseLocation() {
SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE)
}
@Preview(name = "MQTT Enabled Icon")
@Composable
private fun PreviewMqttEnabled() {
SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING)
}
@Preview(name = "All Security Icons with Dialog")
@Composable
private fun PreviewAllSecurityIconsWithDialog() {
var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
val stateLabels = remember {
// Using SecurityState.entries to build the map keys
mapOf(
SecurityState.SECURE to "Secure",
SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
)
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall)
SecurityState.entries.forEach { state ->
// Iterate over enum entries
val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state })
Text(label)
}
}
showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) }
}
}

View file

@ -64,7 +64,6 @@ import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.connections.components.BLEDevices
import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
@ -78,6 +77,7 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.TitledCard

View file

@ -51,8 +51,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.common.components.SecurityIcon
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SecurityIcon
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongMethod")

View file

@ -63,8 +63,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.common.components.MainAppBar
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import java.util.concurrent.TimeUnit
@OptIn(ExperimentalMaterial3ExpressiveApi::class)

View file

@ -80,13 +80,13 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import com.geeksville.mesh.ui.common.components.MainAppBar
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme

View file

@ -26,8 +26,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ui.common.components.MainAppBar
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.feature.map.MapViewModel
@Composable

View file

@ -95,7 +95,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -105,6 +104,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SecurityIcon
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.node.component.NodeKeyStatusIcon
import java.nio.charset.StandardCharsets

View file

@ -39,9 +39,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
@Composable
fun ReactionButton(onSendReaction: (String) -> Unit = {}) {

View file

@ -133,7 +133,6 @@ import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.model.MetricsState
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.geeksville.mesh.util.thenIf
import com.mikepenz.markdown.m3.Markdown
@ -156,6 +155,7 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.SettingsItemSwitch

View file

@ -58,13 +58,13 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.sharing.AddContactFAB
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.feature.node.component.NodeActionDialogs

View file

@ -59,7 +59,6 @@ import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
@ -71,6 +70,7 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.delay
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemDetail

View file

@ -29,9 +29,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -29,9 +29,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -35,9 +35,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -73,13 +73,13 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.PreferenceFooter
import org.meshtastic.core.ui.component.SecurityIcon
import org.meshtastic.core.ui.component.dragContainer
import org.meshtastic.core.ui.component.dragDropItemsIndexed
import org.meshtastic.core.ui.component.rememberDragDropState

View file

@ -32,9 +32,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -50,9 +50,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -29,9 +29,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -45,11 +45,11 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.EditBase64Preference
import com.geeksville.mesh.ui.common.components.PositionPrecisionPreference
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditBase64Preference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PositionPrecisionPreference
import org.meshtastic.core.ui.component.SwitchPreference
@Suppress("LongMethod")

View file

@ -35,15 +35,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.PreferenceDivider
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.ChannelOption
import org.meshtastic.core.model.RegionInfo
import org.meshtastic.core.model.numChannels
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceDivider
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard

View file

@ -40,12 +40,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.precisionBitsToMeters
import org.meshtastic.core.model.util.DistanceUnit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.precisionBitsToMeters
import kotlin.math.roundToInt
private const val POSITION_PRECISION_MIN = 12

View file

@ -43,11 +43,11 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditIPv4Preference
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference

View file

@ -43,7 +43,6 @@ import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
@ -51,6 +50,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.model.Position
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.BitwisePreference
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -29,10 +29,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.settings.radio.ResponseState
import com.google.protobuf.MessageLite
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.PreferenceFooter
@Composable

View file

@ -28,9 +28,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditListPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -46,15 +46,15 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.EditBase64Preference
import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.node.NodeActionButton
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 org.meshtastic.core.model.util.encodeToString
import org.meshtastic.core.model.util.toByteString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.EditBase64Preference
import org.meshtastic.core.ui.component.EditListPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import java.security.SecureRandom

View file

@ -29,9 +29,9 @@ import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference

View file

@ -1,25 +0,0 @@
/*
* 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.util
import android.util.Base64
import com.google.protobuf.ByteString
import com.google.protobuf.kotlin.toByteString
fun ByteString.encodeToString() = Base64.encodeToString(this.toByteArray(), Base64.NO_WRAP)
fun String.toByteString() = Base64.decode(this, Base64.NO_WRAP).toByteString()

View file

@ -1,52 +0,0 @@
/*
* 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.util
import androidx.emoji2.emojipicker.RecentEmojiAsyncProvider
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
/** Define a custom recent emoji provider which shows most frequently used emoji */
class CustomRecentEmojiProvider(
private val customEmojiFrequency: String?,
private val onUpdateCustomEmojiFrequency: (updatedValue: String) -> Unit,
) : RecentEmojiAsyncProvider {
private val emoji2Frequency: MutableMap<String, Int> by lazy {
customEmojiFrequency
?.split(SPLIT_CHAR)
?.associate { entry ->
entry.split(KEY_VALUE_DELIMITER, limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].toInt() }
?: ("" to 0)
}
?.toMutableMap() ?: mutableMapOf()
}
override fun getRecentEmojiListAsync(): ListenableFuture<List<String>> =
Futures.immediateFuture(emoji2Frequency.toList().sortedByDescending { it.second }.map { it.first })
override fun recordSelection(emoji: String) {
emoji2Frequency[emoji] = (emoji2Frequency[emoji] ?: 0) + 1
onUpdateCustomEmojiFrequency(emoji2Frequency.entries.joinToString(SPLIT_CHAR))
}
companion object {
private const val SPLIT_CHAR = ","
private const val KEY_VALUE_DELIMITER = "="
}
}