mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Modularize common composables (#3286)
This commit is contained in:
parent
81804500bd
commit
fe9491121c
46 changed files with 110 additions and 84 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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 }) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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 = "="
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue