Add :core:ui (#3203)

This commit is contained in:
Phil Oliver 2025-09-25 17:01:53 -04:00 committed by GitHub
parent b139c7edd7
commit c5360086b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 594 additions and 750 deletions

View file

@ -44,14 +44,14 @@ import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import javax.inject.Inject
@AndroidEntryPoint

View file

@ -19,16 +19,16 @@ package com.geeksville.mesh.model
import androidx.compose.ui.graphics.Color
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.ui.common.theme.GraphColors.Green
import com.geeksville.mesh.ui.common.theme.GraphColors.InfantryBlue
import com.geeksville.mesh.ui.common.theme.GraphColors.LightGreen
import com.geeksville.mesh.ui.common.theme.GraphColors.Magenta
import com.geeksville.mesh.ui.common.theme.GraphColors.Orange
import com.geeksville.mesh.ui.common.theme.GraphColors.Pink
import com.geeksville.mesh.ui.common.theme.GraphColors.Purple
import com.geeksville.mesh.ui.common.theme.GraphColors.Red
import com.geeksville.mesh.ui.common.theme.GraphColors.Yellow
import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.ui.theme.GraphColors.Green
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
import org.meshtastic.core.ui.theme.GraphColors.LightGreen
import org.meshtastic.core.ui.theme.GraphColors.Magenta
import org.meshtastic.core.ui.theme.GraphColors.Orange
import org.meshtastic.core.ui.theme.GraphColors.Pink
import org.meshtastic.core.ui.theme.GraphColors.Purple
import org.meshtastic.core.ui.theme.GraphColors.Red
import org.meshtastic.core.ui.theme.GraphColors.Yellow
@Suppress("MagicNumber")
enum class Environment(val color: Color) {

View file

@ -91,16 +91,7 @@ import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.icons.Conversations
import com.geeksville.mesh.ui.common.icons.Map
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.icons.Nodes
import com.geeksville.mesh.ui.common.icons.Settings
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusBlue
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@ -120,6 +111,15 @@ import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.icon.Conversations
import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import kotlin.reflect.KClass
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {

View file

@ -1,49 +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.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AdaptiveTwoPane(
first: @Composable ColumnScope.() -> Unit,
second: @Composable ColumnScope.() -> Unit,
) = BoxWithConstraints {
val compactWidth = maxWidth < 600.dp
Row {
Column(modifier = Modifier.weight(1f)) {
first()
if (compactWidth) {
second()
}
}
if (!compactWidth) {
Column(modifier = Modifier.weight(1f)) {
second()
}
}
}
}

View file

@ -1,108 +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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@Composable
fun SimpleAlertDialog(
title: String,
message: String?,
html: String? = null,
onDismissRequest: () -> Unit,
onConfirmRequest: () -> Unit = onDismissRequest, // Default confirm to dismiss
) {
val annotatedString =
html?.let {
AnnotatedString.fromHtml(
html,
linkStyles =
TextLinkStyles(
style =
SpanStyle(
textDecoration = TextDecoration.Underline,
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.primary,
),
),
)
}
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = title) },
text = {
if (annotatedString != null) {
Text(text = annotatedString)
} else {
Text(text = message.orEmpty())
}
},
confirmButton = { TextButton(onClick = onConfirmRequest) { Text(stringResource(id = R.string.okay)) } },
)
}
// For Rationale Dialogs
@Composable
fun MultipleChoiceAlertDialog(
title: String,
message: String?,
choices: Map<String, () -> Unit>,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = title) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
message?.let { Text(text = it, modifier = Modifier.padding(bottom = 8.dp)) }
choices.forEach { (choice, action) ->
Button(
modifier = Modifier.fillMaxWidth().padding(8.dp),
onClick = {
action()
onDismissRequest()
},
) {
Text(text = choice)
}
}
}
},
confirmButton = {},
)
}

View file

@ -1,92 +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.Row
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun BatteryInfo(modifier: Modifier = Modifier, batteryLevel: Int?, voltage: Float?) {
val infoString = "%d%% %.2fV".format(batteryLevel, voltage)
val (image, level) =
when (batteryLevel) {
in 0..4 -> R.drawable.ic_battery_alert to " $infoString"
in 5..14 -> R.drawable.ic_battery_outline to infoString
in 15..34 -> R.drawable.ic_battery_low to infoString
in 35..79 -> R.drawable.ic_battery_medium to infoString
in 80..100 -> R.drawable.ic_battery_high to infoString
101 -> R.drawable.ic_power_plug_24 to "%.2fV".format(voltage)
else -> R.drawable.ic_battery_unknown to (voltage?.let { "%.2fV".format(it) } ?: "")
}
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.height(18.dp),
imageVector = ImageVector.vectorResource(id = image),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
Text(
text = level,
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
}
}
@PreviewLightDark
@Composable
fun BatteryInfoPreview(@PreviewParameter(BatteryInfoPreviewParameterProvider::class) batteryInfo: Pair<Int?, Float?>) {
AppTheme { BatteryInfo(batteryLevel = batteryInfo.first, voltage = batteryInfo.second) }
}
@Composable
@Preview
fun BatteryInfoPreviewSimple() {
AppTheme { BatteryInfo(batteryLevel = 85, voltage = 3.7F) }
}
class BatteryInfoPreviewParameterProvider : PreviewParameterProvider<Pair<Int?, Float?>> {
override val values: Sequence<Pair<Int?, Float?>>
get() =
sequenceOf(
85 to 3.7F,
2 to 3.7F,
12 to 3.7F,
28 to 3.7F,
50 to 3.7F,
101 to 4.9F,
null to 4.5F,
null to null,
)
}

View file

@ -1,113 +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.foundation.layout.wrapContentWidth
import androidx.compose.material3.Checkbox
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BitwisePreference(
title: String,
value: Int,
enabled: Boolean,
items: List<Pair<Int, String>>,
onItemSelected: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
modifier = modifier.padding(vertical = 8.dp),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = value.toString(),
onValueChange = {},
label = { Text(title) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
enabled = enabled,
supportingText = { if (summary != null) Text(text = summary) },
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items.forEach { item ->
DropdownMenuItem(
text = {
Text(text = item.second, overflow = TextOverflow.Ellipsis)
Checkbox(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
checked = value and item.first != 0,
onCheckedChange = { onItemSelected(value xor item.first) },
enabled = enabled,
)
},
onClick = { onItemSelected(value xor item.first) },
)
}
PreferenceFooter(
enabled = enabled,
negativeText = R.string.clear,
onNegativeClicked = { onItemSelected(0) },
positiveText = R.string.close,
onPositiveClicked = { expanded = false },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun BitwisePreferencePreview() {
BitwisePreference(
title = "Settings",
value = 3,
summary = "This is a summary",
enabled = true,
items = listOf(1 to "TEST1", 2 to "TEST2"),
onItemSelected = {},
)
}

View file

@ -1,69 +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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@Composable
fun BottomSheetDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) = Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
.clickable(
onClick = onDismiss,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
) {
Column(
modifier = modifier
.align(Alignment.BottomCenter)
.background(
color = MaterialTheme.colorScheme.surface.copy(alpha = 1f),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.padding(16.dp),
content = content
)
}
}

View file

@ -1,58 +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.annotation.StringRes
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@Composable
fun ClickableTextField(
@StringRes label: Int,
enabled: Boolean,
trailingIcon: ImageVector,
value: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isError: Boolean = false,
) {
val source = remember { MutableInteractionSource() }
val isPressed by source.collectIsPressedAsState()
if (isPressed) onClick()
OutlinedTextField(
value,
onValueChange = {},
enabled = enabled,
readOnly = true,
label = { Text(stringResource(label)) },
trailingIcon = { Icon(trailingIcon, null) },
isError = isError,
interactionSource = source,
modifier = modifier,
)
}

View file

@ -1,54 +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 android.content.ClipData
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.launch
import org.meshtastic.core.strings.R
@Composable
fun CopyIconButton(
valueToCopy: String,
modifier: Modifier = Modifier,
label: String = stringResource(id = R.string.copy),
) {
val clipboardManager = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
IconButton(
modifier = modifier,
onClick = {
coroutineScope.launch {
val clipData = ClipData.newPlainText(label, valueToCopy)
val clipEntry = ClipEntry(clipData)
clipboardManager.setClipEntry(clipEntry)
}
},
) {
Icon(imageVector = Icons.TwoTone.ContentCopy, contentDescription = label)
}
}

View file

@ -1,83 +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.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun EditIPv4Preference(
title: String,
value: Int,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val pattern = """\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b""".toRegex()
fun convertIntToIpAddress(int: Int): String {
return "${int and 0xff}.${int shr 8 and 0xff}.${int shr 16 and 0xff}.${int shr 24 and 0xff}"
}
fun convertIpAddressToInt(ipAddress: String): Int? = ipAddress.split(".")
.map { it.toIntOrNull() }.reversed() // little-endian byte order
.fold(0) { total, next ->
if (next == null) return null else total shl 8 or next
}
var valueState by remember(value) { mutableStateOf(convertIntToIpAddress(value)) }
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
isError = convertIntToIpAddress(value) != valueState,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
keyboardActions = keyboardActions,
onValueChanged = {
valueState = it
if (pattern.matches(it)) convertIpAddressToInt(it)?.let { int -> onValueChanged(int) }
},
onFocusChanged = {},
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
private fun EditIPv4PreferencePreview() {
EditIPv4Preference(
title = "IP Address",
value = 16820416,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {}
)
}

View file

@ -46,6 +46,7 @@ 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

View file

@ -1,92 +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.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import org.meshtastic.core.strings.R
@Composable
fun EditPasswordPreference(
title: String,
value: String,
maxSize: Int,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
var isPasswordVisible by remember { mutableStateOf(false) }
EditTextPreference(
title = title,
value = value,
maxSize = maxSize,
enabled = enabled,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = { onValueChanged(it) },
onFocusChanged = {},
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
Icon(
imageVector = if (isPasswordVisible) Icons.TwoTone.VisibilityOff else Icons.TwoTone.VisibilityOff,
contentDescription =
if (isPasswordVisible) {
stringResource(R.string.hide_password)
} else {
stringResource(R.string.show_password)
},
)
}
},
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
private fun EditPasswordPreferencePreview() {
EditPasswordPreference(
title = "Password",
value = "top secret",
maxSize = 63,
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
}

View file

@ -1,291 +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.Box
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.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@Composable
fun SignedIntegerEditTextPreference(
title: String,
value: Int,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
) {
var valueState by remember(value) { mutableStateOf(value.toString()) }
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = valueState.toIntOrNull() == null,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
valueState = it
it.toIntOrNull()?.let { int -> onValueChanged(int) }
},
onFocusChanged = onFocusChanged,
modifier = modifier,
trailingIcon = trailingIcon,
)
}
@Composable
fun EditTextPreference(
title: String,
value: Int,
enabled: Boolean,
isError: Boolean = false,
keyboardActions: KeyboardActions,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
) {
var valueState by remember(value) { mutableStateOf(value.toUInt().toString()) }
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toUInt().toString() != valueState || isError,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.isEmpty()) {
valueState = it
} else {
it.toUIntOrNull()?.toInt()?.let { int ->
valueState = it
onValueChanged(int)
}
}
},
onFocusChanged = onFocusChanged,
modifier = modifier,
trailingIcon = trailingIcon,
)
}
@Composable
fun EditTextPreference(
title: String,
value: Float,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (Float) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
) {
var valueState by remember(value) { mutableStateOf(value.toString()) }
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toString() != valueState,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.isEmpty()) {
valueState = it
} else {
it.toFloatOrNull()?.let { float ->
valueState = it
onValueChanged(float)
}
}
},
onFocusChanged = onFocusChanged,
modifier = modifier,
)
}
@Composable
fun EditTextPreference(
title: String,
value: Double,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (Double) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
var valueState by remember(value) { mutableStateOf(value.toString()) }
val decimalSeparators = setOf('.', ',', '٫', '、', '·') // set of possible decimal separators
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toString() != valueState,
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.length <= 1 || it.first() in decimalSeparators) {
valueState = it
} else {
it.toDoubleOrNull()?.let { double ->
valueState = it
onValueChanged(double)
}
}
},
onFocusChanged = {},
modifier = modifier,
)
}
@Composable
fun EditTextPreference(
title: String,
value: String,
enabled: Boolean,
isError: Boolean,
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
maxSize: Int = 0, // max_size - 1 (in bytes)
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
) {
var isFocused by remember { mutableStateOf(false) }
Column(modifier = modifier.padding(vertical = 8.dp)) {
OutlinedTextField(
value = value,
singleLine = true,
modifier =
Modifier.fillMaxWidth().onFocusEvent {
isFocused = it.isFocused
onFocusChanged(it)
},
enabled = enabled,
isError = isError,
onValueChange = {
if (maxSize > 0) {
if (it.toByteArray().size <= maxSize) {
onValueChanged(it)
}
} else {
onValueChanged(it)
}
},
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
trailingIcon = {
if (trailingIcon != null) {
trailingIcon()
} else if (isError) {
Icon(
imageVector = Icons.TwoTone.Info,
contentDescription = stringResource(id = R.string.error),
tint = MaterialTheme.colorScheme.error,
)
}
},
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
if (maxSize > 0 && isFocused) {
Box(contentAlignment = Alignment.BottomEnd, modifier = Modifier.fillMaxWidth()) {
Text(
text = "${value.toByteArray().size}/$maxSize",
style = MaterialTheme.typography.bodySmall,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 8.dp, bottom = 4.dp),
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun EditTextPreferencePreview() {
Column {
EditTextPreference(
title = "String",
value = "Meshtastic",
summary = "This is a summary",
maxSize = 39,
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
EditTextPreference(
title = "Advanced Settings",
value = UInt.MAX_VALUE.toInt(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {},
)
}
}

View file

@ -31,6 +31,7 @@ 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(

View file

@ -1,307 +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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQDangerouslyPolluted
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQExcellent
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQExtremelyPolluted
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQGood
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQHeavilyPolluted
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQLightlyPolluted
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQModeratelyPolluted
import com.geeksville.mesh.ui.common.theme.IAQColors.IAQSeverelyPolluted
import org.meshtastic.core.strings.R
@Suppress("MagicNumber")
enum class Iaq(val color: Color, val description: String, val range: IntRange) {
Excellent(IAQExcellent, "Excellent", 0..50),
Good(IAQGood, "Good", 51..100),
LightlyPolluted(IAQLightlyPolluted, "Lightly Polluted", 101..150),
ModeratelyPolluted(IAQModeratelyPolluted, "Moderately Polluted", 151..200),
HeavilyPolluted(IAQHeavilyPolluted, "Heavily Polluted", 201..300),
SeverelyPolluted(IAQSeverelyPolluted, "Severely Polluted", 301..400),
ExtremelyPolluted(IAQExtremelyPolluted, "Extremely Polluted", 401..500),
DangerouslyPolluted(IAQDangerouslyPolluted, "Dangerously Polluted", 501..Int.MAX_VALUE),
}
fun getIaq(iaq: Int): Iaq? = when {
iaq == Int.MIN_VALUE -> null
iaq in Iaq.Excellent.range -> Iaq.Excellent
iaq in Iaq.Good.range -> Iaq.Good
iaq in Iaq.LightlyPolluted.range -> Iaq.LightlyPolluted
iaq in Iaq.ModeratelyPolluted.range -> Iaq.ModeratelyPolluted
iaq in Iaq.HeavilyPolluted.range -> Iaq.HeavilyPolluted
iaq in Iaq.SeverelyPolluted.range -> Iaq.SeverelyPolluted
iaq in Iaq.ExtremelyPolluted.range -> Iaq.ExtremelyPolluted
else -> Iaq.DangerouslyPolluted
}
private fun getIaqDescriptionWithRange(iaqEnum: Iaq): String = if (iaqEnum.range.last == Int.MAX_VALUE) {
"${iaqEnum.description} (${iaqEnum.range.first}+)"
} else {
"${iaqEnum.description} (${iaqEnum.range.first}-${iaqEnum.range.last})"
}
enum class IaqDisplayMode {
Pill,
Dot,
Text,
Gauge,
Gradient,
}
@Suppress("LongMethod", "UnusedPrivateProperty")
@Composable
fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pill) {
if (iaq == null || iaq == Int.MIN_VALUE) {
return
}
var isLegendOpen by remember { mutableStateOf(false) }
val iaqEnum = if (iaq != null) getIaq(iaq) else null
val gradient = Brush.linearGradient(colors = Iaq.entries.map { it.color })
if (iaqEnum != null) {
Column {
when (displayMode) {
IaqDisplayMode.Pill -> {
Box(
modifier =
Modifier.clip(RoundedCornerShape(10.dp))
.background(iaqEnum.color)
.width(125.dp)
.height(30.dp)
.clickable { isLegendOpen = true },
) {
Row(
modifier = Modifier.padding(4.dp).align(Alignment.CenterStart),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = "IAQ $iaq", color = Color.White, fontWeight = FontWeight.Bold)
Icon(
imageVector =
if (iaqEnum.range.first < 100) Icons.Default.ThumbUp else Icons.Filled.Warning,
contentDescription = stringResource(R.string.air_quality_icon),
tint = Color.White,
)
}
}
}
IaqDisplayMode.Dot -> {
Column(modifier = Modifier.clickable { isLegendOpen = true }) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "$iaq")
Spacer(modifier = Modifier.width(4.dp))
Box(modifier = Modifier.size(10.dp).background(iaqEnum.color, shape = CircleShape))
}
}
}
IaqDisplayMode.Text -> {
Text(
text = getIaqDescriptionWithRange(iaqEnum),
fontSize = 12.sp,
modifier = Modifier.clickable { isLegendOpen = true },
)
}
IaqDisplayMode.Gauge -> {
CircularProgressIndicator(
progress = iaq / 500f,
modifier = Modifier.size(60.dp).clickable { isLegendOpen = true },
strokeWidth = 8.dp,
color = iaqEnum.color,
)
Text(text = "${iaqEnum.description}")
}
IaqDisplayMode.Gradient -> {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.clickable { isLegendOpen = true },
) {
LinearProgressIndicator(
progress = iaq / 500f,
modifier = Modifier.fillMaxWidth().height(20.dp),
color = iaqEnum.color,
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = iaqEnum.description, fontSize = 12.sp)
}
}
}
if (isLegendOpen) {
AlertDialog(
onDismissRequest = { isLegendOpen = false },
shape = RoundedCornerShape(16.dp),
text = { IAQScale() },
confirmButton = {
TextButton(onClick = { isLegendOpen = false }) {
Text(text = stringResource(id = R.string.close))
}
},
)
}
}
}
}
// Assuming Iaq is an enum class with color and description properties
// and that it conforms to CaseIterable.
// Replace with your actual implementation
@Composable
fun IAQScale(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.Start) {
Text(
text = stringResource(R.string.indoor_air_quality_iaq),
style =
MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold, textAlign = TextAlign.Center),
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
for (iaq in Iaq.entries) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(20.dp, 15.dp).clip(RoundedCornerShape(5.dp)).background(iaq.color))
Spacer(modifier = Modifier.width(8.dp))
Text(getIaqDescriptionWithRange(iaq), style = MaterialTheme.typography.bodyMedium)
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Preview(showBackground = true)
@Composable
fun IAQScalePreview() {
IAQScale()
}
@Suppress("LongMethod")
@Preview(showBackground = true)
@Composable
private fun IndoorAirQualityPreview() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Pill", style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6)
IndoorAirQuality(iaq = 51)
}
Row {
IndoorAirQuality(iaq = 101)
IndoorAirQuality(iaq = 201)
}
Row {
IndoorAirQuality(iaq = 350)
IndoorAirQuality(iaq = 351)
}
Text("Dot", style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Dot)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
}
Text("Text", style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Text)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Text)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
}
Text("Gauge", style = MaterialTheme.typography.titleLarge)
Row {
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 151, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 251, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 301, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gauge)
}
Row {
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gauge)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
}
Text("Gradient", style = MaterialTheme.typography.titleLarge)
IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gradient)
IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gradient)
}
}

View file

@ -1,285 +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.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
// Derived in part from: https://github.com/androidx/androidx/blob/c92ad2941368202b2d78b8d14c71bf81e9525944/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@Preview
@Composable
fun LazyColumnDragAndDropDemo() {
var list by remember { mutableStateOf(List(50) { it }) }
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (fromIndex in list.indices && toIndex in list.indices) {
list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
}
}
LazyColumn(
modifier = Modifier.dragContainer(
dragDropState = dragDropState,
haptics = LocalHapticFeedback.current,
),
state = listState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text("Header", Modifier.fillMaxWidth().padding(20.dp))
}
itemsIndexed(list, key = { _, item -> item }) { index, item ->
DraggableItem(dragDropState, index + 1) { isDragging ->
Card {
Text("Item $item", Modifier.fillMaxWidth().padding(20.dp))
}
}
}
item {
Text("Footer", Modifier.fillMaxWidth().padding(20.dp))
}
}
}
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
headerCount: Int = 0,
onMove: (Int, Int) -> Unit
): DragDropState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) { DragDropState(lazyListState, headerCount, scope, onMove) }
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
lazyListState.scrollBy(diff)
}
}
return state
}
class DragDropState
internal constructor(
private val state: LazyListState,
private val headerCount: Int,
private val scope: CoroutineScope,
private val onMove: (Int, Int) -> Unit
) {
private var draggingItemIndex by mutableStateOf<Int?>(null)
val adjustedItemIndex get() = draggingItemIndex?.minus(headerCount)
internal val scrollChannel = Channel<Float>()
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableIntStateOf(0)
internal val draggingItemOffset: Float
get() = draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
} ?: 0f
private val draggingItemLayoutInfo: LazyListItemInfo?
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(0f)
private set
internal fun onDragStart(offset: Offset): LazyListItemInfo? = state.layoutInfo.visibleItemsInfo
.filter { it.contentType == DragDropContentType }
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
?.also {
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
}
internal fun onDragInterrupted() {
if (draggingItemIndex != null) {
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
0f,
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
)
previousIndexOfDraggedItem = null
}
}
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0
}
internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset.y
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
val endOffset = startOffset + draggingItem.size
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem = state.layoutInfo.visibleItemsInfo
.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
}
if (targetItem != null) {
if (
draggingItem.index == state.firstVisibleItemIndex ||
targetItem.index == state.firstVisibleItemIndex
) {
state.requestScrollToItem(
state.firstVisibleItemIndex,
state.firstVisibleItemScrollOffset
)
}
onMove.invoke(draggingItem.index - headerCount, targetItem.index - headerCount)
draggingItemIndex = targetItem.index
} else {
val overscroll = when {
draggingItemDraggedDelta > 0 ->
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
draggingItemDraggedDelta < 0 ->
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
else -> 0f
}
if (overscroll != 0f) {
scrollChannel.trySend(overscroll)
}
}
}
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
}
fun Modifier.dragContainer(
dragDropState: DragDropState,
haptics: HapticFeedback,
): Modifier {
return this.pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset ->
dragDropState.onDragStart(offset) ?: return@detectDragGesturesAfterLongPress
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() }
)
}
}
@Composable
fun LazyItemScope.DraggableItem(
dragDropState: DragDropState,
index: Int,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
) {
val dragging = index == dragDropState.adjustedItemIndex
val draggingModifier = if (dragging) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = dragDropState.draggingItemOffset }
} else if (index == dragDropState.previousIndexOfDraggedItem) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = dragDropState.previousItemOffset.value }
} else {
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
}
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
}
const val DragDropContentType = "drag-and-drop"
/**
* Extension function for [LazyListScope] with drag-and-drop functionality for indexed items.
*
* Wraps [itemsIndexed] function with [detectDragGesturesAfterLongPress] to enable long-press
* drag gestures and allow items in the list to be reordered using the provided [DragDropState].
*/
inline fun <T> LazyListScope.dragDropItemsIndexed(
items: List<T>,
dragDropState: DragDropState,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T, isDragging: Boolean) -> Unit
) = itemsIndexed(
items = items,
key = key,
contentType = { _, _ -> DragDropContentType },
itemContent = { index, item ->
DraggableItem(
dragDropState = dragDropState,
index = index,
content = { isDragging -> itemContent(index, item, isDragging) }
)
}
)

View file

@ -1,162 +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("MagicNumber")
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SignalCellular4Bar
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.SignalCellularAlt1Bar
import androidx.compose.material.icons.filled.SignalCellularAlt2Bar
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import org.meshtastic.core.strings.R
private const val SNR_GOOD_THRESHOLD = -7f
private const val SNR_FAIR_THRESHOLD = -15f
private const val RSSI_GOOD_THRESHOLD = -115
private const val RSSI_FAIR_THRESHOLD = -126
@Stable
private enum class Quality(
@Stable val nameRes: Int,
@Stable val imageVector: ImageVector,
@Stable val color: @Composable () -> Color,
) {
NONE(R.string.none_quality, Icons.Default.SignalCellularAlt1Bar, { colorScheme.StatusRed }),
BAD(R.string.bad, Icons.Default.SignalCellularAlt2Bar, { colorScheme.StatusOrange }),
FAIR(R.string.fair, Icons.Default.SignalCellularAlt, { colorScheme.StatusYellow }),
GOOD(R.string.good, Icons.Default.SignalCellular4Bar, { colorScheme.StatusGreen }),
}
/**
* Displays the `snr` and `rssi` color coded based on the signal quality, along with a human readable description and
* related icon.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) {
val quality = determineSignalQuality(snr, rssi)
FlowRow(modifier = modifier, maxLines = 1) {
Snr(snr)
Spacer(Modifier.width(8.dp))
Rssi(rssi)
Spacer(Modifier.width(8.dp))
Text(
text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}",
fontSize = MaterialTheme.typography.labelLarge.fontSize,
maxLines = 1,
)
Spacer(Modifier.width(8.dp))
Icon(
imageVector = quality.imageVector,
contentDescription = stringResource(R.string.signal_quality),
tint = quality.color.invoke(),
)
}
}
/** Displays the `snr` and `rssi` with color depending on the values respectively. */
@Composable
fun SnrAndRssi(snr: Float, rssi: Int) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Snr(snr)
Rssi(rssi)
}
}
/** Displays a human readable description and icon representing the signal quality. */
@Composable
fun LoraSignalIndicator(snr: Float, rssi: Int) {
val quality = determineSignalQuality(snr, rssi)
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize().padding(8.dp),
) {
Icon(
imageVector = quality.imageVector,
contentDescription = stringResource(R.string.signal_quality),
tint = quality.color.invoke(),
)
Text(text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}")
}
}
@Composable
fun Snr(snr: Float, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) {
val color: Color =
if (snr > SNR_GOOD_THRESHOLD) {
Quality.GOOD.color.invoke()
} else if (snr > SNR_FAIR_THRESHOLD) {
Quality.FAIR.color.invoke()
} else {
Quality.BAD.color.invoke()
}
Text(text = "%s %.2fdB".format(stringResource(id = R.string.snr), snr), color = color, fontSize = fontSize)
}
@Composable
fun Rssi(rssi: Int, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) {
val color: Color =
if (rssi > RSSI_GOOD_THRESHOLD) {
Quality.GOOD.color.invoke()
} else if (rssi > RSSI_FAIR_THRESHOLD) {
Quality.FAIR.color.invoke()
} else {
Quality.BAD.color.invoke()
}
Text(text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi), color = color, fontSize = fontSize)
}
private fun determineSignalQuality(snr: Float, rssi: Int): Quality = when {
snr > SNR_GOOD_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> Quality.GOOD
snr > SNR_GOOD_THRESHOLD && rssi > RSSI_FAIR_THRESHOLD -> Quality.FAIR
snr > SNR_FAIR_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> Quality.FAIR
snr <= SNR_FAIR_THRESHOLD && rssi <= RSSI_FAIR_THRESHOLD -> Quality.NONE
else -> Quality.BAD
}

View file

@ -1,102 +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.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.ui.common.theme.HyperlinkBlue
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.model.DefaultMarkdownColors
import com.mikepenz.markdown.model.DefaultMarkdownTypography
@Composable
fun MDText(
text: String,
modifier: Modifier = Modifier,
style: TextStyle = MaterialTheme.typography.bodyMedium,
color: Color = Color.Unspecified,
) {
val colors =
DefaultMarkdownColors(
text = color,
codeBackground = MaterialTheme.colorScheme.surfaceContainerHigh,
inlineCodeBackground = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.onSurface,
tableBackground = MaterialTheme.colorScheme.surfaceContainer,
)
val typography =
DefaultMarkdownTypography(
// Restrict max size of the text
h1 = MaterialTheme.typography.headlineMedium.copy(color = color),
h2 = MaterialTheme.typography.headlineMedium.copy(color = color),
h3 = MaterialTheme.typography.headlineSmall.copy(color = color),
h4 = MaterialTheme.typography.titleLarge.copy(color = color),
h5 = MaterialTheme.typography.titleMedium.copy(color = color),
h6 = MaterialTheme.typography.titleSmall.copy(color = color),
text = style,
code =
MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
),
inlineCode =
MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
background = MaterialTheme.colorScheme.surfaceContainerHigh,
),
quote = MaterialTheme.typography.bodyLarge.copy(color = color),
paragraph = MaterialTheme.typography.bodyMedium.copy(color = color),
ordered = MaterialTheme.typography.bodyMedium.copy(color = color),
bullet = MaterialTheme.typography.bodyMedium.copy(color = color),
list = MaterialTheme.typography.bodyMedium.copy(color = color),
textLink =
TextLinkStyles(style = SpanStyle(color = HyperlinkBlue, textDecoration = TextDecoration.Underline)),
table = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
)
// Custom Markdown components to disable image rendering
val customComponents = markdownComponents(image = { /* Empty composable to disable image rendering */ })
Markdown(
content = text,
modifier = modifier,
colors = colors,
typography = typography,
components = customComponents, // Use custom components
)
}
@Preview(showBackground = true)
@Composable
private fun AutoLinkTextPreview() {
MDText(
"A text containing a link https://example.com **bold** _Italics_" +
"\n # hello \n ## hello \n ### hello \n #### hello \n ##### hello \n ###### hello \n ```code```",
)
}

View file

@ -50,7 +50,6 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.isConfigRoute
import com.geeksville.mesh.navigation.isNodeDetailRoute
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.debug.DebugMenuActions
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@ -59,6 +58,7 @@ import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("CyclomaticComplexMethod")
@Composable

View file

@ -1,130 +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.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.BatteryUnknown
import androidx.compose.material.icons.rounded.BatteryUnknown
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.icons.BatteryEmpty
import com.geeksville.mesh.ui.common.icons.BatteryUnknown
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
private const val FORMAT = "%d%%"
private const val SIZE_ICON = 20
@Suppress("MagicNumber")
@Composable
fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) {
val levelString = FORMAT.format(level)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
if (level > 100) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp).rotate(90f),
imageVector = Icons.Rounded.Power,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
Text(text = "PWD", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge)
} else if (level < 0) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = MeshtasticIcons.BatteryUnknown,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
} else {
// Map battery percentage to color
val fillColor =
when (level) {
in 0..19 -> MaterialTheme.colorScheme.StatusRed
in 20..39 -> MaterialTheme.colorScheme.StatusOrange
else -> MaterialTheme.colorScheme.StatusGreen
}
Icon(
modifier =
Modifier.size(SIZE_ICON.dp).drawBehind {
val insetVertical = size.height * .28f
val insetLeft = size.width * .11f
val insetRight = size.width * .22f
val availableWidth = size.width - (insetLeft + insetRight)
val availableHeight = size.height - (insetVertical * 2)
// Fill (grow from left to right)
val fillWidth = availableWidth * (level / 100f)
drawRect(
color = fillColor,
topLeft = Offset(insetLeft, insetVertical),
size = Size(fillWidth, availableHeight),
)
},
imageVector = MeshtasticIcons.BatteryEmpty,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
Text(
text = levelString,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelLarge,
)
}
}
}
class BatteryLevelProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int> = sequenceOf(-1, 19, 39, 90, 101)
}
@PreviewLightDark
@Composable
fun MaterialBatteryInfoPreview(@PreviewParameter(BatteryLevelProvider::class) batteryLevel: Int) {
AppTheme { MaterialBatteryInfo(level = batteryLevel) }
}

View file

@ -34,6 +34,7 @@ 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

View file

@ -1,69 +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.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceCategory(
text: String,
modifier: Modifier = Modifier,
content: (@Composable ColumnScope.() -> Unit)? = null
) {
Text(
text,
modifier = modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp, end = 16.dp),
style = MaterialTheme.typography.titleLarge,
)
if (content != null) {
Card(
modifier = modifier.padding(bottom = 8.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
content()
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun PreferenceCategoryPreview() {
PreferenceCategory(
text = "Advanced settings"
)
}

View file

@ -1,79 +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.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@Composable
fun PreferenceFooter(
enabled: Boolean,
onCancelClicked: () -> Unit,
onSaveClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceFooter(
enabled = enabled,
negativeText = R.string.clear_changes,
onNegativeClicked = onCancelClicked,
positiveText = R.string.send,
onPositiveClicked = onSaveClicked,
modifier = modifier,
)
}
@Composable
fun PreferenceFooter(
enabled: Boolean,
@StringRes negativeText: Int,
onNegativeClicked: () -> Unit,
@StringRes positiveText: Int,
onPositiveClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth().height(64.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(modifier = Modifier.height(48.dp).weight(1f), onClick = onNegativeClicked) {
Text(text = stringResource(id = negativeText))
}
OutlinedButton(modifier = Modifier.height(48.dp).weight(1f), enabled = enabled, onClick = onPositiveClicked) {
Text(text = stringResource(id = positiveText))
}
}
}
@Preview(showBackground = true)
@Composable
private fun PreferenceFooterPreview() {
PreferenceFooter(enabled = true, onCancelClicked = {}, onSaveClicked = {})
}

View file

@ -1,145 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun RegularPreference(
title: String,
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
summary: String? = null,
trailingIcon: ImageVector? = null,
dropdownMenu: @Composable () -> Unit = {},
) {
RegularPreference(
title = title,
subtitle = AnnotatedString(text = subtitle),
onClick = onClick,
modifier = modifier,
enabled = enabled,
summary = summary,
trailingIcon = trailingIcon,
dropdownMenu = dropdownMenu,
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RegularPreference(
title: String,
subtitle: AnnotatedString,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
summary: String? = null,
trailingIcon: ImageVector? = null,
dropdownMenu: @Composable () -> Unit = {},
) {
val color = if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
}
Column(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick)
.padding(all = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
FlowRow(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = if (enabled) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
},
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyLarge,
color = color,
)
}
if (trailingIcon != null) {
Box {
Icon(
imageVector = trailingIcon,
contentDescription = "trailingIcon",
modifier = Modifier
.padding(start = 8.dp)
.wrapContentWidth(Alignment.End),
tint = color,
)
dropdownMenu()
}
}
}
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = color,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun RegularPreferencePreview() {
RegularPreference(
title = "Advanced settings",
subtitle = "Text2",
onClick = { },
)
}

View file

@ -64,11 +64,11 @@ import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.model.getChannel
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import org.meshtastic.core.model.Channel
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

View file

@ -27,9 +27,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.NodeSignalQuality
import org.meshtastic.core.ui.theme.AppTheme
const val MAX_VALID_SNR = 100F
const val MAX_VALID_RSSI = 0

View file

@ -1,107 +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.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.strings.R
@Composable
fun SimpleAlertDialog(
@StringRes title: Int,
text: @Composable (() -> Unit)? = null,
confirmText: String? = null,
onConfirm: (() -> Unit)? = null,
dismissText: String? = null,
onDismiss: () -> Unit = {},
) = AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
TextButton(
onClick = onDismiss,
modifier = Modifier.padding(horizontal = 16.dp),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface),
) {
Text(text = dismissText ?: stringResource(id = R.string.cancel))
}
},
confirmButton = {
onConfirm?.let {
TextButton(
onClick = onConfirm,
modifier = Modifier.padding(horizontal = 16.dp),
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface),
) {
Text(text = confirmText ?: stringResource(id = R.string.okay))
}
}
},
title = {
Text(text = stringResource(id = title), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
},
text = text,
shape = RoundedCornerShape(16.dp),
)
@Composable
fun SimpleAlertDialog(
@StringRes title: Int,
@StringRes text: Int,
onConfirm: (() -> Unit)? = null,
onDismiss: () -> Unit = {},
) = SimpleAlertDialog(
onConfirm = onConfirm,
onDismiss = onDismiss,
title = title,
text = {
Text(text = stringResource(id = text), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
},
)
@Composable
fun SimpleAlertDialog(
@StringRes title: Int,
text: String,
onConfirm: (() -> Unit)? = null,
onDismiss: () -> Unit = {},
) = SimpleAlertDialog(
onConfirm = onConfirm,
onDismiss = onDismiss,
title = title,
text = { Text(text = text, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) },
)
@PreviewLightDark
@Composable
private fun SimpleAlertDialogPreview() {
AppTheme { SimpleAlertDialog(title = R.string.message, text = R.string.sample_message) }
}

View file

@ -1,429 +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 android.annotation.SuppressLint
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.TimeFrame
private const val NO_OPTION_INDEX = -1
private val TRACK_PADDING = 2.dp
private val TRACK_COLOR = Color.LightGray.copy(alpha = .5f)
private val PRESSED_TRACK_PADDING = 1.dp
private val OPTION_PADDING = 5.dp
private const val PRESSED_UNSELECTED_ALPHA = .6f
private val BACKGROUND_SHAPE = RoundedCornerShape(8.dp)
/**
* Provides the user with a set of options they can choose from.
*
* (Inspired by https://gist.github.com/zach-klippenstein/7ae8874db304f957d6bb91263e292117)
*/
@Composable
fun <T : Any> SlidingSelector(
options: List<T>,
selectedOption: T,
onOptionSelected: (T) -> Unit,
modifier: Modifier = Modifier,
content: @Composable (T) -> Unit
) {
val state = remember { SelectorState() }
state.optionCount = options.size
state.selectedOption = options.indexOf(selectedOption)
state.onOptionSelected = { onOptionSelected(options[it]) }
/* Animate between whole-number indices so we don't need to do pixel calculations. */
val selectedIndexOffset by animateFloatAsState(
state.selectedOption.toFloat(),
label = "Selected Index Offset"
)
Layout(
content = {
SelectedIndicator(state)
Dividers(state)
Options(state, options, content)
},
modifier = modifier
.fillMaxWidth()
.then(state.inputModifier)
.background(TRACK_COLOR, BACKGROUND_SHAPE)
.padding(TRACK_PADDING)
) { measurables, constraints ->
val (indicatorMeasurable, dividersMeasurable, optionsMeasurable) = measurables
/* Measure the options first so we know how tall to make the indicator. */
val optionsPlaceable = optionsMeasurable.measure(constraints)
state.updatePressedScale(optionsPlaceable.height, this)
/* Measure the indicator and dividers to be the right size. */
val indicatorPlaceable = indicatorMeasurable.measure(
Constraints.fixed(
width = optionsPlaceable.width / options.size,
height = optionsPlaceable.height
)
)
val dividersPlaceable = dividersMeasurable.measure(
Constraints.fixed(
width = optionsPlaceable.width,
height = optionsPlaceable.height
)
)
layout(optionsPlaceable.width, optionsPlaceable.height) {
val optionWidth = optionsPlaceable.width / options.size
/* Place the indicator first so that it's below the option labels. */
indicatorPlaceable.placeRelative(
x = (selectedIndexOffset * optionWidth).toInt(),
y = 0
)
dividersPlaceable.placeRelative(IntOffset.Zero)
optionsPlaceable.placeRelative(IntOffset.Zero)
}
}
}
/**
* Visual representation of the option the user may select.
*/
@Composable
fun OptionLabel(text: String) {
Text(text, maxLines = 1, overflow = Ellipsis)
}
/**
* Draws the selected indicator on the [SlidingSelector] track.
*/
@Composable
private fun SelectedIndicator(state: SelectorState) {
Box(
Modifier
.then(
state.optionScaleModifier(
pressed = state.pressedOption == state.selectedOption,
option = state.selectedOption
)
)
.shadow(4.dp, BACKGROUND_SHAPE)
.background(MaterialTheme.colorScheme.background, BACKGROUND_SHAPE)
)
}
/**
* Draws dividers between [OptionLabel]s.
*/
@Composable
private fun Dividers(state: SelectorState) {
/* Animate each divider independently. */
val alphas = (0 until state.optionCount).map { i ->
val selectionAdjacent = i == state.selectedOption || i - 1 == state.selectedOption
animateFloatAsState(if (selectionAdjacent) 0f else 1f, label = "Dividers")
}
Canvas(Modifier.fillMaxSize()) {
val optionWidth = size.width / state.optionCount
val dividerPadding = TRACK_PADDING + PRESSED_TRACK_PADDING
alphas.forEachIndexed { i, alpha ->
val x = i * optionWidth
drawLine(
Color.White,
alpha = alpha.value,
start = Offset(x, dividerPadding.toPx()),
end = Offset(x, size.height - dividerPadding.toPx())
)
}
}
}
/**
* Draws the options available to the user.
*/
@Composable
private fun <T> Options(
state: SelectorState,
options: List<T>,
content: @Composable (T) -> Unit
) {
CompositionLocalProvider(
LocalTextStyle provides TextStyle(fontWeight = FontWeight.Medium)
) {
Row(
horizontalArrangement = spacedBy(TRACK_PADDING),
modifier = Modifier
.fillMaxWidth()
.selectableGroup()
) {
options.forEachIndexed { i, timeFrame ->
val isSelected = i == state.selectedOption
val isPressed = i == state.pressedOption
/* Unselected presses are represented by fading. */
val alpha by animateFloatAsState(
if (!isSelected && isPressed) PRESSED_UNSELECTED_ALPHA else 1f,
label = "Unselected"
)
val semanticsModifier = Modifier.semantics(mergeDescendants = true) {
selected = isSelected
role = Role.Button
onClick { state.onOptionSelected(i); true }
stateDescription = if (isSelected) "Selected" else "Not selected"
}
Box(
Modifier
/* Divide space evenly between all options. */
.weight(1f)
.then(semanticsModifier)
.padding(OPTION_PADDING)
/* Draw pressed indication when not selected. */
.alpha(alpha)
/* Selected presses are represented by scaling. */
.then(state.optionScaleModifier(isPressed && isSelected, i))
/* Center the option content. */
.wrapContentWidth()
) {
content(timeFrame)
}
}
}
}
}
/**
* Contains and handles the state necessary to present the [SlidingSelector] to the user.
*/
private class SelectorState {
var optionCount by mutableIntStateOf(0)
var selectedOption by mutableIntStateOf(0)
var onOptionSelected: (Int) -> Unit by mutableStateOf({})
var pressedOption by mutableIntStateOf(NO_OPTION_INDEX)
/**
* Scale factor that should be used to scale pressed option. When this scale is applied,
* exactly [PRESSED_TRACK_PADDING] will be added around the element's usual size.
*/
var pressedSelectedScale by mutableFloatStateOf(1f)
private set
/**
* Calculates the scale factor we need to use for pressed options to get the desired padding.
*/
fun updatePressedScale(controlHeight: Int, density: Density) {
with(density) {
val pressedPadding = PRESSED_TRACK_PADDING * 2
val pressedHeight = controlHeight - pressedPadding.toPx()
pressedSelectedScale = pressedHeight / controlHeight
}
}
/**
* Returns a [Modifier] that will scale an element so that it gets [PRESSED_TRACK_PADDING] extra
* padding around it. The scale will be animated.
*
* The scale is also performed around either the left or right edge of the element if the option
* is the first or last option, respectively. In those cases, the scale will also be translated so
* that [PRESSED_TRACK_PADDING] will be added on the left or right edge.
*/
@SuppressLint("ModifierFactoryExtensionFunction")
fun optionScaleModifier(
pressed: Boolean,
option: Int,
): Modifier = Modifier.composed {
val scale by animateFloatAsState(if (pressed) pressedSelectedScale else 1f, label = "Scale")
val xOffset by animateDpAsState(
if (pressed) PRESSED_TRACK_PADDING else 0.dp,
label = "x Offset"
)
graphicsLayer {
this.scaleX = scale
this.scaleY = scale
/* Scales on the ends should gravitate to that edge. */
this.transformOrigin = TransformOrigin(
pivotFractionX = when (option) {
0 -> 0f
optionCount - 1 -> 1f
else -> .5f
},
pivotFractionY = .5f
)
/* But should still move inwards to keep the pressed padding consistent with top and bottom. */
this.translationX = when (option) {
0 -> xOffset.toPx()
optionCount - 1 -> -xOffset.toPx()
else -> 0f
}
}
}
/**
* A [Modifier] that will listen for touch gestures and update the selected and pressed properties
* of this state appropriately.
*/
val inputModifier = Modifier.pointerInput(optionCount) {
val optionWidth = size.width / optionCount
/* Helper to calculate which option an event occurred in. */
fun optionIndex(change: PointerInputChange): Int =
((change.position.x / size.width.toFloat()) * optionCount)
.toInt()
.coerceIn(0, optionCount - 1)
awaitEachGesture {
val down = awaitFirstDown()
pressedOption = optionIndex(down)
val downOnSelected = pressedOption == selectedOption
val optionBounds = Rect(
left = pressedOption * optionWidth.toFloat(),
right = (pressedOption + 1) * optionWidth.toFloat(),
top = 0f,
bottom = size.height.toFloat()
)
if (downOnSelected) {
horizontalDrag(down.id) { change ->
pressedOption = optionIndex(change)
if (pressedOption != selectedOption) {
onOptionSelected(pressedOption)
}
}
} else {
waitForUpOrCancellation(inBounds = optionBounds)
/* Null means the gesture was cancelled (e.g. dragged out of bounds). */
?.let { onOptionSelected(pressedOption) }
}
pressedOption = NO_OPTION_INDEX
}
}
}
/**
* Works with bounds that may not be at 0,0.
*/
@Suppress("ReturnCount")
private suspend fun AwaitPointerEventScope.waitForUpOrCancellation(inBounds: Rect): PointerInputChange? {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.all { it.changedToUp() }) {
/* All pointers are up */
return event.changes[0]
}
if (event.changes.any { it.isConsumed || !inBounds.contains(it.position) }) {
/* Canceled */
return null
}
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.any { it.isConsumed }) {
return null
}
}
}
@Preview
@Composable
fun SlidingSelectorPreview() {
MaterialTheme {
Surface {
Column(Modifier.padding(8.dp)) {
var selectedOption by remember { mutableStateOf(TimeFrame.TWENTY_FOUR_HOURS) }
SlidingSelector(
TimeFrame.entries.toList(),
selectedOption,
onOptionSelected = { selectedOption = it }
) {
OptionLabel(stringResource(it.strRes))
}
}
}
}
}

View file

@ -1,96 +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.AnimatedContent
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
title: String,
summary: String = "",
checked: Boolean,
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
padding: PaddingValues? = null,
containerColor: Color? = null,
loading: Boolean = false,
) {
ListItem(
colors =
ListItemDefaults.colors()
.copy(
headlineColor =
if (enabled) {
ListItemDefaults.colors().headlineColor
} else {
ListItemDefaults.colors().headlineColor.copy(alpha = 0.5f)
},
supportingTextColor =
if (enabled) {
ListItemDefaults.colors().supportingTextColor
} else {
ListItemDefaults.colors().supportingTextColor.copy(alpha = 0.5f)
},
containerColor = containerColor ?: ListItemDefaults.colors().containerColor,
),
modifier =
(padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
value = checked,
enabled = enabled,
onValueChange = onCheckedChange,
),
trailingContent = {
AnimatedContent(targetState = loading) { loading ->
if (loading) {
CircularWavyProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Switch(enabled = enabled, checked = checked, onCheckedChange = null)
}
}
},
supportingContent = {
if (summary.isNotEmpty()) {
Text(text = summary, modifier = Modifier.padding(bottom = 16.dp))
}
},
headlineContent = { Text(text = title) },
)
}
@Preview(showBackground = true)
@Composable
private fun SwitchPreferencePreview() {
SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {})
}

View file

@ -1,93 +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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun TextDividerPreference(
title: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
trailingIcon: ImageVector? = null,
) {
TextDividerPreference(
title = AnnotatedString(text = title),
enabled = enabled,
modifier = modifier,
trailingIcon = trailingIcon,
)
}
@Composable
fun TextDividerPreference(
title: AnnotatedString,
modifier: Modifier = Modifier,
enabled: Boolean = true,
trailingIcon: ImageVector? = null,
) {
Card(
modifier = modifier.fillMaxWidth(),
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = if (!enabled) {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
} else {
Color.Unspecified
},
)
if (trailingIcon != null) {
Icon(
trailingIcon, "trailingIcon",
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun TextDividerPreferencePreview() {
TextDividerPreference(title = "Advanced settings")
}

View file

@ -1,73 +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 android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LifecycleResumeEffect
@Composable
fun rememberTimeTickWithLifecycle(): Long {
val context = LocalContext.current
var value by remember { mutableLongStateOf(System.currentTimeMillis()) }
val receiver = TimeBroadcastReceiver { value = System.currentTimeMillis() }
LifecycleResumeEffect(Unit) {
receiver.register(context)
value = System.currentTimeMillis()
onPauseOrDispose {
receiver.unregister(context)
}
}
return value
}
private class TimeBroadcastReceiver(
val onTimeChanged: () -> Unit,
) : BroadcastReceiver() {
private var registered = false
override fun onReceive(context: Context, intent: Intent) {
onTimeChanged()
}
fun register(context: Context) {
if (!registered) {
val filter = IntentFilter(Intent.ACTION_TIME_TICK)
context.registerReceiver(this, filter)
registered = true
}
}
fun unregister(context: Context) {
if (registered) {
context.unregisterReceiver(this)
registered = false
}
}
}

View file

@ -1,56 +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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun TitledCard(title: String?, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
title?.let {
Text(
text = it,
modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(),
style = MaterialTheme.typography.titleLarge,
)
}
Card(content = content)
}
}
@PreviewLightDark
@Composable
private fun TitledCardPreview() {
AppTheme { Surface { TitledCard(title = "Title") { Box(modifier = Modifier.fillMaxWidth().height(100.dp)) {} } } }
}

View file

@ -1,184 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [battery_android_0](https://fonts.google.com/icons?icon.query=battery+android+0&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.BatteryEmpty: ImageVector
get() {
if (batteryEmpty != null) {
return batteryEmpty!!
}
batteryEmpty =
ImageVector.Builder(
name = "BatteryEmpty",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(160f, 720f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
verticalLineToRelative(-240f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
horizontalLineToRelative(540f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
verticalLineToRelative(240f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
lineTo(160f, 720f)
close()
moveTo(160f, 640f)
horizontalLineToRelative(540f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(740f, 600f)
verticalLineToRelative(-240f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(700f, 320f)
lineTo(160f, 320f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(120f, 360f)
verticalLineToRelative(240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(160f, 640f)
close()
moveTo(860f, 580f)
verticalLineToRelative(-200f)
horizontalLineToRelative(20f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(920f, 420f)
verticalLineToRelative(120f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(880f, 580f)
horizontalLineToRelative(-20f)
close()
moveTo(120f, 640f)
verticalLineToRelative(-320f)
verticalLineToRelative(320f)
close()
}
}
.build()
return batteryEmpty!!
}
private var batteryEmpty: ImageVector? = null
/**
* This is from Material Symbols.
*
* @see
* [battery_android_question](https://fonts.google.com/icons?icon.query=battery+android+question&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.BatteryUnknown: ImageVector
get() {
if (batteryUnknown != null) {
return batteryUnknown!!
}
batteryUnknown =
ImageVector.Builder(
name = "BatteryUnknown",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(120f, 640f)
verticalLineToRelative(-320f)
verticalLineToRelative(320f)
close()
moveTo(726f, 720f)
lineTo(160f, 720f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
verticalLineToRelative(-240f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
horizontalLineToRelative(521f)
quadToRelative(-20f, 16f, -35f, 36f)
reflectiveQuadToRelative(-25f, 44f)
lineTo(160f, 320f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(120f, 360f)
verticalLineToRelative(240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(160f, 640f)
horizontalLineToRelative(520f)
quadToRelative(2f, 25f, 14.5f, 45.5f)
reflectiveQuadTo(726f, 720f)
close()
moveTo(800f, 660f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(840f, 620f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(800f, 580f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(760f, 620f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(800f, 660f)
close()
moveTo(772f, 538f)
horizontalLineToRelative(57f)
verticalLineToRelative(-21f)
quadToRelative(0f, -10f, 5f, -19f)
quadToRelative(6f, -13f, 15.5f, -22f)
reflectiveQuadToRelative(19.5f, -19f)
quadToRelative(17f, -17f, 28.5f, -37f)
reflectiveQuadToRelative(11.5f, -43f)
quadToRelative(0f, -42f, -32.5f, -69.5f)
reflectiveQuadTo(800f, 280f)
quadToRelative(-38f, 0f, -68f, 22f)
reflectiveQuadToRelative(-40f, 58f)
lineToRelative(51f, 21f)
quadToRelative(6f, -20f, 21.5f, -33f)
reflectiveQuadToRelative(35.5f, -13f)
quadToRelative(21f, 0f, 36.5f, 12f)
reflectiveQuadToRelative(15.5f, 32f)
quadToRelative(0f, 17f, -10f, 30.5f)
reflectiveQuadTo(820f, 434f)
quadToRelative(-11f, 11f, -22.5f, 21.5f)
reflectiveQuadTo(779f, 480f)
quadToRelative(-6f, 14f, -6.5f, 28.5f)
reflectiveQuadTo(772f, 538f)
close()
}
}
.build()
return batteryUnknown!!
}
private var batteryUnknown: ImageVector? = null

View file

@ -1,151 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [router](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:router:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=router&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Device: ImageVector
get() {
if (device != null) {
return device!!
}
device =
ImageVector.Builder(
name = "Outlined.Device",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color(0xFFE3E3E3))) {
moveTo(200f, 840f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(120f, 760f)
verticalLineToRelative(-160f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(200f, 520f)
horizontalLineToRelative(400f)
verticalLineToRelative(-120f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(640f, 360f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(680f, 400f)
verticalLineToRelative(120f)
horizontalLineToRelative(80f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(840f, 600f)
verticalLineToRelative(160f)
quadToRelative(0f, 33f, -23.5f, 56.5f)
reflectiveQuadTo(760f, 840f)
lineTo(200f, 840f)
close()
moveTo(200f, 760f)
horizontalLineToRelative(560f)
verticalLineToRelative(-160f)
lineTo(200f, 600f)
verticalLineToRelative(160f)
close()
moveTo(280f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(320f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(280f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(240f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(280f, 720f)
close()
moveTo(420f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(460f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(420f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(380f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(420f, 720f)
close()
moveTo(560f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(600f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(560f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(520f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(560f, 720f)
close()
moveTo(640f, 300f)
quadToRelative(-11f, 0f, -20f, 2f)
reflectiveQuadToRelative(-18f, 6f)
quadToRelative(-16f, 7f, -32.5f, 6f)
reflectiveQuadTo(541f, 301f)
quadToRelative(-12f, -12f, -11.5f, -29f)
reflectiveQuadToRelative(14.5f, -25f)
quadToRelative(21f, -13f, 45.5f, -20f)
reflectiveQuadToRelative(50.5f, -7f)
quadToRelative(27f, 0f, 51f, 7f)
reflectiveQuadToRelative(45f, 20f)
quadToRelative(14f, 8f, 14.5f, 25f)
reflectiveQuadTo(739f, 301f)
quadToRelative(-12f, 12f, -29f, 13f)
reflectiveQuadToRelative(-33f, -6f)
quadToRelative(-8f, -4f, -17.5f, -6f)
reflectiveQuadToRelative(-19.5f, -2f)
close()
moveTo(640f, 160f)
quadToRelative(-39f, 0f, -74.5f, 11.5f)
reflectiveQuadTo(500f, 205f)
quadToRelative(-14f, 10f, -30.5f, 9f)
reflectiveQuadTo(442f, 202f)
quadToRelative(-12f, -12f, -12f, -28f)
reflectiveQuadToRelative(13f, -26f)
quadToRelative(41f, -32f, 91f, -50f)
reflectiveQuadToRelative(106f, -18f)
quadToRelative(56f, 0f, 106f, 18f)
reflectiveQuadToRelative(91f, 50f)
quadToRelative(13f, 10f, 13f, 26f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-11f, 11f, -27.5f, 12f)
reflectiveQuadToRelative(-30.5f, -9f)
quadToRelative(-30f, -22f, -65.5f, -33.5f)
reflectiveQuadTo(640f, 160f)
close()
moveTo(200f, 760f)
verticalLineToRelative(-160f)
verticalLineToRelative(160f)
close()
}
}
.build()
return device!!
}
private var device: ImageVector? = null

View file

@ -1,110 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [map](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:map:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=map&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Map: ImageVector
get() {
if (map != null) {
return map!!
}
map =
ImageVector.Builder(
name = "Outlined.Map",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveToRelative(574f, 831f)
lineToRelative(-214f, -75f)
lineToRelative(-186f, 72f)
quadToRelative(-10f, 4f, -19.5f, 2.5f)
reflectiveQuadTo(137f, 824f)
quadToRelative(-8f, -5f, -12.5f, -13.5f)
reflectiveQuadTo(120f, 791f)
verticalLineToRelative(-561f)
quadToRelative(0f, -13f, 7.5f, -23f)
reflectiveQuadToRelative(20.5f, -15f)
lineToRelative(186f, -63f)
quadToRelative(6f, -2f, 12.5f, -3f)
reflectiveQuadToRelative(13.5f, -1f)
quadToRelative(7f, 0f, 13.5f, 1f)
reflectiveQuadToRelative(12.5f, 3f)
lineToRelative(214f, 75f)
lineToRelative(186f, -72f)
quadToRelative(10f, -4f, 19.5f, -2.5f)
reflectiveQuadTo(823f, 136f)
quadToRelative(8f, 5f, 12.5f, 13.5f)
reflectiveQuadTo(840f, 169f)
verticalLineToRelative(561f)
quadToRelative(0f, 13f, -7.5f, 23f)
reflectiveQuadTo(812f, 768f)
lineToRelative(-186f, 63f)
quadToRelative(-6f, 2f, -12.5f, 3f)
reflectiveQuadToRelative(-13.5f, 1f)
quadToRelative(-7f, 0f, -13.5f, -1f)
reflectiveQuadToRelative(-12.5f, -3f)
close()
moveTo(560f, 742f)
verticalLineToRelative(-468f)
lineToRelative(-160f, -56f)
verticalLineToRelative(468f)
lineToRelative(160f, 56f)
close()
moveTo(640f, 742f)
lineTo(760f, 702f)
verticalLineToRelative(-474f)
lineToRelative(-120f, 46f)
verticalLineToRelative(468f)
close()
moveTo(200f, 732f)
lineTo(320f, 686f)
verticalLineToRelative(-468f)
lineToRelative(-120f, 40f)
verticalLineToRelative(474f)
close()
moveTo(640f, 274f)
verticalLineToRelative(468f)
verticalLineToRelative(-468f)
close()
moveTo(320f, 218f)
verticalLineToRelative(468f)
verticalLineToRelative(-468f)
close()
}
}
.build()
return map!!
}
private var map: ImageVector? = null

View file

@ -1,20 +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.icons
object MeshtasticIcons

View file

@ -1,101 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [forum](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:forum:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=forum&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Conversations: ImageVector
get() {
if (conversations != null) {
return conversations!!
}
conversations =
ImageVector.Builder(
name = "Outlined.Conversations",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(840f, 824f)
quadToRelative(-8f, 0f, -15f, -3f)
reflectiveQuadToRelative(-13f, -9f)
lineToRelative(-92f, -92f)
lineTo(320f, 720f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(240f, 640f)
verticalLineToRelative(-40f)
horizontalLineToRelative(440f)
quadToRelative(33f, 0f, 56.5f, -23.5f)
reflectiveQuadTo(760f, 520f)
verticalLineToRelative(-280f)
horizontalLineToRelative(40f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(880f, 320f)
verticalLineToRelative(463f)
quadToRelative(0f, 18f, -12f, 29.5f)
reflectiveQuadTo(840f, 824f)
close()
moveTo(160f, 487f)
lineToRelative(47f, -47f)
horizontalLineToRelative(393f)
verticalLineToRelative(-280f)
lineTo(160f, 160f)
verticalLineToRelative(327f)
close()
moveTo(120f, 624f)
quadToRelative(-16f, 0f, -28f, -11.5f)
reflectiveQuadTo(80f, 583f)
verticalLineToRelative(-423f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(160f, 80f)
horizontalLineToRelative(440f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(680f, 160f)
verticalLineToRelative(280f)
quadToRelative(0f, 33f, -23.5f, 56.5f)
reflectiveQuadTo(600f, 520f)
lineTo(240f, 520f)
lineToRelative(-92f, 92f)
quadToRelative(-6f, 6f, -13f, 9f)
reflectiveQuadToRelative(-15f, 3f)
close()
moveTo(160f, 440f)
verticalLineToRelative(-280f)
verticalLineToRelative(280f)
close()
}
}
.build()
return conversations!!
}
private var conversations: ImageVector? = null

View file

@ -1,165 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [router_off](https://fonts.google.com/icons?icon.query=router+off&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.NoDevice: ImageVector
get() {
if (noDevice != null) {
return noDevice!!
}
noDevice =
ImageVector.Builder(
name = "Outlined.NoDevice",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(806f, 692f)
lineTo(600f, 486f)
verticalLineToRelative(-86f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(640f, 360f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(680f, 400f)
verticalLineToRelative(120f)
horizontalLineToRelative(80f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(840f, 600f)
verticalLineToRelative(78f)
quadToRelative(0f, 14f, -12f, 19f)
reflectiveQuadToRelative(-22f, -5f)
close()
moveTo(200f, 760f)
horizontalLineToRelative(446f)
lineTo(486f, 600f)
lineTo(200f, 600f)
verticalLineToRelative(160f)
close()
moveTo(200f, 840f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(120f, 760f)
verticalLineToRelative(-160f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(200f, 520f)
horizontalLineToRelative(206f)
lineTo(83f, 197f)
quadToRelative(-12f, -12f, -12f, -28.5f)
reflectiveQuadTo(83f, 140f)
quadToRelative(12f, -12f, 28.5f, -12f)
reflectiveQuadToRelative(28.5f, 12f)
lineToRelative(680f, 680f)
quadToRelative(12f, 12f, 12f, 28f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-12f, 12f, -28.5f, 12f)
reflectiveQuadTo(763f, 876f)
lineToRelative(-37f, -36f)
lineTo(200f, 840f)
close()
moveTo(280f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(240f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(280f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(320f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(280f, 720f)
close()
moveTo(420f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(380f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(420f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(460f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(420f, 720f)
close()
moveTo(560f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(520f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(560f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(600f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(560f, 720f)
close()
moveTo(200f, 760f)
verticalLineToRelative(-160f)
verticalLineToRelative(160f)
close()
moveTo(640f, 300f)
quadToRelative(-11f, 0f, -20f, 2f)
reflectiveQuadToRelative(-18f, 6f)
quadToRelative(-16f, 7f, -32.5f, 6f)
reflectiveQuadTo(541f, 301f)
quadToRelative(-12f, -12f, -11.5f, -29f)
reflectiveQuadToRelative(14.5f, -25f)
quadToRelative(21f, -13f, 45.5f, -20f)
reflectiveQuadToRelative(50.5f, -7f)
quadToRelative(27f, 0f, 51f, 7f)
reflectiveQuadToRelative(45f, 20f)
quadToRelative(14f, 8f, 14.5f, 25f)
reflectiveQuadTo(739f, 301f)
quadToRelative(-12f, 12f, -29f, 13f)
reflectiveQuadToRelative(-33f, -6f)
quadToRelative(-8f, -4f, -17.5f, -6f)
reflectiveQuadToRelative(-19.5f, -2f)
close()
moveTo(640f, 160f)
quadToRelative(-39f, 0f, -74.5f, 11.5f)
reflectiveQuadTo(500f, 205f)
quadToRelative(-14f, 10f, -30.5f, 9f)
reflectiveQuadTo(442f, 202f)
quadToRelative(-12f, -12f, -12f, -28f)
reflectiveQuadToRelative(13f, -26f)
quadToRelative(41f, -32f, 91f, -50f)
reflectiveQuadToRelative(106f, -18f)
quadToRelative(56f, 0f, 106f, 18f)
reflectiveQuadToRelative(91f, 50f)
quadToRelative(13f, 10f, 13f, 26f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-11f, 11f, -27.5f, 12f)
reflectiveQuadToRelative(-30.5f, -9f)
quadToRelative(-30f, -22f, -65.5f, -33.5f)
reflectiveQuadTo(640f, 160f)
close()
}
}
.build()
return noDevice!!
}
private var noDevice: ImageVector? = null

View file

@ -1,166 +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.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [graph_3](https://fonts.google.com/icons?icon.query=graph+3&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.Nodes: ImageVector
get() {
if (nodes != null) {
return nodes!!
}
nodes =
ImageVector.Builder(
name = "Outlined.Nodes",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(480f, 880f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -5f, 0.5f, -11f)
reflectiveQuadToRelative(1.5f, -11f)
lineToRelative(-83f, -47f)
quadToRelative(-16f, 14f, -36f, 21.5f)
reflectiveQuadToRelative(-43f, 7.5f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(24f, 0f, 45f, 9f)
reflectiveQuadToRelative(38f, 25f)
lineToRelative(119f, -60f)
quadToRelative(-3f, -23f, 2.5f, -45f)
reflectiveQuadToRelative(19.5f, -41f)
lineToRelative(-34f, -52f)
quadToRelative(-7f, 2f, -14.5f, 3f)
reflectiveQuadToRelative(-15.5f, 1f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 20f, -6.5f, 38.5f)
reflectiveQuadTo(456f, 272f)
lineToRelative(35f, 52f)
quadToRelative(8f, -2f, 15f, -3f)
reflectiveQuadToRelative(15f, -1f)
quadToRelative(17f, 0f, 32f, 4f)
reflectiveQuadToRelative(29f, 12f)
lineToRelative(66f, -54f)
quadToRelative(-4f, -10f, -6f, -20.5f)
reflectiveQuadToRelative(-2f, -21.5f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
quadToRelative(-17f, 0f, -32f, -4.5f)
reflectiveQuadTo(699f, 343f)
lineToRelative(-66f, 55f)
quadToRelative(4f, 10f, 6f, 20.5f)
reflectiveQuadToRelative(2f, 21.5f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
quadToRelative(-24f, 0f, -45.5f, -9f)
reflectiveQuadTo(437f, 526f)
lineToRelative(-118f, 59f)
quadToRelative(2f, 9f, 1.5f, 18f)
reflectiveQuadToRelative(-2.5f, 18f)
lineToRelative(84f, 48f)
quadToRelative(16f, -14f, 35.5f, -21.5f)
reflectiveQuadTo(480f, 640f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
close()
moveTo(200f, 640f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(240f, 600f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(200f, 560f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(160f, 600f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(200f, 640f)
close()
moveTo(360f, 240f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(400f, 200f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(360f, 160f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(320f, 200f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(360f, 240f)
close()
moveTo(480f, 800f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(520f, 760f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(480f, 720f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(440f, 760f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(480f, 800f)
close()
moveTo(520f, 480f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(560f, 440f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(520f, 400f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(480f, 440f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(520f, 480f)
close()
moveTo(760f, 280f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(800f, 240f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(760f, 200f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(720f, 240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(760f, 280f)
close()
}
}
.build()
return nodes!!
}
private var nodes: ImageVector? = null

View file

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

View file

@ -1,236 +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.theme
import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF306A42)
val onPrimaryLight = Color(0xFFFFFFFF)
val primaryContainerLight = Color(0xFFB3F1BF)
val onPrimaryContainerLight = Color(0xFF00210D)
val secondaryLight = Color(0xFF506353)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFD2E8D3)
val onSecondaryContainerLight = Color(0xFF0D1F12)
val tertiaryLight = Color(0xFF3A656E)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFBEEAF6)
val onTertiaryContainerLight = Color(0xFF001F25)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF410002)
val backgroundLight = Color(0xFFF6FBF3)
val onBackgroundLight = Color(0xFF181D18)
val surfaceLight = Color(0xFFF6FBF3)
val onSurfaceLight = Color(0xFF181D18)
val surfaceVariantLight = Color(0xFFDDE5DA)
val onSurfaceVariantLight = Color(0xFF414941)
val outlineLight = Color(0xFF717971)
val outlineVariantLight = Color(0xFFC1C9BF)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2D322D)
val inverseOnSurfaceLight = Color(0xFFEEF2EA)
val inversePrimaryLight = Color(0xFF97D5A5)
val surfaceDimLight = Color(0xFFD7DBD4)
val surfaceBrightLight = Color(0xFFF6FBF3)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFF0F5ED)
val surfaceContainerLight = Color(0xFFEBEFE7)
val surfaceContainerHighLight = Color(0xFFE5EAE2)
val surfaceContainerHighestLight = Color(0xFFDFE4DC)
val primaryLightMediumContrast = Color(0xFF0F4D29)
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
val primaryContainerLightMediumContrast = Color(0xFF478157)
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val secondaryLightMediumContrast = Color(0xFF344738)
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
val secondaryContainerLightMediumContrast = Color(0xFF657A68)
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryLightMediumContrast = Color(0xFF1C4952)
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightMediumContrast = Color(0xFF517B85)
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val errorLightMediumContrast = Color(0xFF8C0009)
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
val errorContainerLightMediumContrast = Color(0xFFDA342E)
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
val backgroundLightMediumContrast = Color(0xFFF6FBF3)
val onBackgroundLightMediumContrast = Color(0xFF181D18)
val surfaceLightMediumContrast = Color(0xFFF6FBF3)
val onSurfaceLightMediumContrast = Color(0xFF181D18)
val surfaceVariantLightMediumContrast = Color(0xFFDDE5DA)
val onSurfaceVariantLightMediumContrast = Color(0xFF3D453D)
val outlineLightMediumContrast = Color(0xFF596159)
val outlineVariantLightMediumContrast = Color(0xFF757D74)
val scrimLightMediumContrast = Color(0xFF000000)
val inverseSurfaceLightMediumContrast = Color(0xFF2D322D)
val inverseOnSurfaceLightMediumContrast = Color(0xFFEEF2EA)
val inversePrimaryLightMediumContrast = Color(0xFF97D5A5)
val surfaceDimLightMediumContrast = Color(0xFFD7DBD4)
val surfaceBrightLightMediumContrast = Color(0xFFF6FBF3)
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightMediumContrast = Color(0xFFF0F5ED)
val surfaceContainerLightMediumContrast = Color(0xFFEBEFE7)
val surfaceContainerHighLightMediumContrast = Color(0xFFE5EAE2)
val surfaceContainerHighestLightMediumContrast = Color(0xFFDFE4DC)
val primaryLightHighContrast = Color(0xFF002911)
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
val primaryContainerLightHighContrast = Color(0xFF0F4D29)
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
val secondaryLightHighContrast = Color(0xFF142619)
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
val secondaryContainerLightHighContrast = Color(0xFF344738)
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
val tertiaryLightHighContrast = Color(0xFF00262E)
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightHighContrast = Color(0xFF1C4952)
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
val errorLightHighContrast = Color(0xFF4E0002)
val onErrorLightHighContrast = Color(0xFFFFFFFF)
val errorContainerLightHighContrast = Color(0xFF8C0009)
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
val backgroundLightHighContrast = Color(0xFFF6FBF3)
val onBackgroundLightHighContrast = Color(0xFF181D18)
val surfaceLightHighContrast = Color(0xFFF6FBF3)
val onSurfaceLightHighContrast = Color(0xFF000000)
val surfaceVariantLightHighContrast = Color(0xFFDDE5DA)
val onSurfaceVariantLightHighContrast = Color(0xFF1E261F)
val outlineLightHighContrast = Color(0xFF3D453D)
val outlineVariantLightHighContrast = Color(0xFF3D453D)
val scrimLightHighContrast = Color(0xFF000000)
val inverseSurfaceLightHighContrast = Color(0xFF2D322D)
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
val inversePrimaryLightHighContrast = Color(0xFFBCFBC8)
val surfaceDimLightHighContrast = Color(0xFFD7DBD4)
val surfaceBrightLightHighContrast = Color(0xFFF6FBF3)
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightHighContrast = Color(0xFFF0F5ED)
val surfaceContainerLightHighContrast = Color(0xFFEBEFE7)
val surfaceContainerHighLightHighContrast = Color(0xFFE5EAE2)
val surfaceContainerHighestLightHighContrast = Color(0xFFDFE4DC)
val primaryDark = Color(0xFF97D5A5)
val onPrimaryDark = Color(0xFF00391A)
val primaryContainerDark = Color(0xFF15512C)
val onPrimaryContainerDark = Color(0xFFB3F1BF)
val secondaryDark = Color(0xFFB6CCB8)
val onSecondaryDark = Color(0xFF223526)
val secondaryContainerDark = Color(0xFF384B3C)
val onSecondaryContainerDark = Color(0xFFD2E8D3)
val tertiaryDark = Color(0xFFA2CED9)
val onTertiaryDark = Color(0xFF01363F)
val tertiaryContainerDark = Color(0xFF204D56)
val onTertiaryContainerDark = Color(0xFFBEEAF6)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF101510)
val onBackgroundDark = Color(0xFFDFE4DC)
val surfaceDark = Color(0xFF101510)
val onSurfaceDark = Color(0xFFDFE4DC)
val surfaceVariantDark = Color(0xFF414941)
val onSurfaceVariantDark = Color(0xFFC1C9BF)
val outlineDark = Color(0xFF8B938A)
val outlineVariantDark = Color(0xFF414941)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFDFE4DC)
val inverseOnSurfaceDark = Color(0xFF2D322D)
val inversePrimaryDark = Color(0xFF306A42)
val surfaceDimDark = Color(0xFF101510)
val surfaceBrightDark = Color(0xFF353A35)
val surfaceContainerLowestDark = Color(0xFF0A0F0B)
val surfaceContainerLowDark = Color(0xFF181D18)
val surfaceContainerDark = Color(0xFF1C211C)
val surfaceContainerHighDark = Color(0xFF262B26)
val surfaceContainerHighestDark = Color(0xFF313631)
val primaryDarkMediumContrast = Color(0xFF9BD9A9)
val onPrimaryDarkMediumContrast = Color(0xFF001B09)
val primaryContainerDarkMediumContrast = Color(0xFF639D72)
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
val secondaryDarkMediumContrast = Color(0xFFBBD0BC)
val onSecondaryDarkMediumContrast = Color(0xFF081A0D)
val secondaryContainerDarkMediumContrast = Color(0xFF819683)
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
val tertiaryDarkMediumContrast = Color(0xFFA6D2DD)
val onTertiaryDarkMediumContrast = Color(0xFF00191F)
val tertiaryContainerDarkMediumContrast = Color(0xFF6D97A2)
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
val errorDarkMediumContrast = Color(0xFFFFBAB1)
val onErrorDarkMediumContrast = Color(0xFF370001)
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
val backgroundDarkMediumContrast = Color(0xFF101510)
val onBackgroundDarkMediumContrast = Color(0xFFDFE4DC)
val surfaceDarkMediumContrast = Color(0xFF101510)
val onSurfaceDarkMediumContrast = Color(0xFFF8FCF4)
val surfaceVariantDarkMediumContrast = Color(0xFF414941)
val onSurfaceVariantDarkMediumContrast = Color(0xFFC5CDC3)
val outlineDarkMediumContrast = Color(0xFF9DA59C)
val outlineVariantDarkMediumContrast = Color(0xFF7D857D)
val scrimDarkMediumContrast = Color(0xFF000000)
val inverseSurfaceDarkMediumContrast = Color(0xFFDFE4DC)
val inverseOnSurfaceDarkMediumContrast = Color(0xFF262B26)
val inversePrimaryDarkMediumContrast = Color(0xFF16522E)
val surfaceDimDarkMediumContrast = Color(0xFF101510)
val surfaceBrightDarkMediumContrast = Color(0xFF353A35)
val surfaceContainerLowestDarkMediumContrast = Color(0xFF0A0F0B)
val surfaceContainerLowDarkMediumContrast = Color(0xFF181D18)
val surfaceContainerDarkMediumContrast = Color(0xFF1C211C)
val surfaceContainerHighDarkMediumContrast = Color(0xFF262B26)
val surfaceContainerHighestDarkMediumContrast = Color(0xFF313631)
val primaryDarkHighContrast = Color(0xFFEFFFEE)
val onPrimaryDarkHighContrast = Color(0xFF000000)
val primaryContainerDarkHighContrast = Color(0xFF9BD9A9)
val onPrimaryContainerDarkHighContrast = Color(0xFF000000)
val secondaryDarkHighContrast = Color(0xFFEFFFEE)
val onSecondaryDarkHighContrast = Color(0xFF000000)
val secondaryContainerDarkHighContrast = Color(0xFFBBD0BC)
val onSecondaryContainerDarkHighContrast = Color(0xFF000000)
val tertiaryDarkHighContrast = Color(0xFFF3FCFF)
val onTertiaryDarkHighContrast = Color(0xFF000000)
val tertiaryContainerDarkHighContrast = Color(0xFFA6D2DD)
val onTertiaryContainerDarkHighContrast = Color(0xFF000000)
val errorDarkHighContrast = Color(0xFFFFF9F9)
val onErrorDarkHighContrast = Color(0xFF000000)
val errorContainerDarkHighContrast = Color(0xFFFFBAB1)
val onErrorContainerDarkHighContrast = Color(0xFF000000)
val backgroundDarkHighContrast = Color(0xFF101510)
val onBackgroundDarkHighContrast = Color(0xFFDFE4DC)
val surfaceDarkHighContrast = Color(0xFF101510)
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkHighContrast = Color(0xFF414941)
val onSurfaceVariantDarkHighContrast = Color(0xFFF5FDF2)
val outlineDarkHighContrast = Color(0xFFC5CDC3)
val outlineVariantDarkHighContrast = Color(0xFFC5CDC3)
val scrimDarkHighContrast = Color(0xFF000000)
val inverseSurfaceDarkHighContrast = Color(0xFFDFE4DC)
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
val inversePrimaryDarkHighContrast = Color(0xFF003216)
val surfaceDimDarkHighContrast = Color(0xFF101510)
val surfaceBrightDarkHighContrast = Color(0xFF353A35)
val surfaceContainerLowestDarkHighContrast = Color(0xFF0A0F0B)
val surfaceContainerLowDarkHighContrast = Color(0xFF181D18)
val surfaceContainerDarkHighContrast = Color(0xFF1C211C)
val surfaceContainerHighDarkHighContrast = Color(0xFF262B26)
val surfaceContainerHighestDarkHighContrast = Color(0xFF313631)

View file

@ -1,105 +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.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
val MeshtasticGreen = Color(0xFF67EA94)
val MeshtasticAlt = Color(0xFF2C2D3C)
val HyperlinkBlue = Color(0xFF43C3B0)
val AnnotationColor = Color(0xFF039BE5)
object IAQColors {
val IAQExcellent = Color(0xFF00E400)
val IAQGood = Color(0xFF92D050)
val IAQLightlyPolluted = Color(0xFFFFFF00)
val IAQModeratelyPolluted = Color(0xFFFF7300)
val IAQHeavilyPolluted = Color(0xFFFF0000)
val IAQSeverelyPolluted = Color(0xFF99004C)
val IAQExtremelyPolluted = Color(0xFF663300)
val IAQDangerouslyPolluted = Color(0xFF663300)
}
object GraphColors {
val InfantryBlue = Color(red = 75, green = 119, blue = 190)
val LightGreen = Color(0xFF4BF0BE)
val Purple = Color(0xFF9C27B0)
val Pink = Color(red = 255, green = 102, blue = 204)
val Orange = Color(0xFFFF8800)
val Green = Color.Green
val Red = Color.Red
val Blue = Color.Blue
val Yellow = Color.Yellow
val Magenta = Color.Magenta
val Cyan = Color.Cyan
}
object StatusColors {
val ColorScheme.StatusGreen: Color
@Composable
get() = // If it might change based on theme
if (isSystemInDarkTheme()) {
Color(0xFF28A03B) // Example dark green
} else {
Color(0xFF30C047)
}
val ColorScheme.StatusYellow: Color
@Composable
get() =
if (isSystemInDarkTheme()) {
Color(0xFFFFC107)
} else {
Color(0xFFFFD54F)
}
val ColorScheme.StatusOrange: Color
@Composable
get() =
if (isSystemInDarkTheme()) {
Color(0xFFE07000)
} else {
Color(0xFFFF8800)
}
val ColorScheme.StatusRed: Color
@Composable
get() = // If it might change based on theme
if (isSystemInDarkTheme()) {
Color(0xFFB00020)
} else {
Color(0xFFF44336)
}
val ColorScheme.StatusBlue: Color
@Composable
get() = // If it might change based on theme
if (isSystemInDarkTheme()) {
Color(0xFF2196F3)
} else {
Color(0xFF42A5F5)
}
}
object MessageItemColors {
val Red = Color(0x4DFF0000)
}

View file

@ -1,303 +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("UnusedPrivateProperty")
package com.geeksville.mesh.ui.common.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MotionScheme.Companion.expressive
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val lightScheme =
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme =
darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val mediumContrastLightColorScheme =
lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val highContrastLightColorScheme =
lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val mediumContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val highContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
@Immutable
data class ColorFamily(val color: Color, val onColor: Color, val colorContainer: Color, val onColorContainer: Color)
val unspecified_scheme = ColorFamily(Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content:
@Composable()
() -> Unit,
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkScheme
else -> lightScheme
}
MaterialExpressiveTheme(
colorScheme = colorScheme,
motionScheme = expressive(),
typography = AppTypography,
content = content,
)
}
const val MODE_DYNAMIC = 6969420

View file

@ -1,22 +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.theme
import androidx.compose.material3.Typography
val AppTypography = Typography()

View file

@ -66,7 +66,6 @@ import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.connections.components.BLEDevices
import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
@ -83,6 +82,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
fun String?.isIPAddress(): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")

View file

@ -49,11 +49,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
/**
* Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth

View file

@ -33,9 +33,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.connections.DeviceType
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LambdaParameterEventTrailing")
@Composable

View file

@ -43,13 +43,13 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.ui.common.components.MaterialBatteryInfo
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@Composable
fun CurrentlyConnectedInfo(

View file

@ -36,7 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun EmptyStateContent(imageVector: ImageVector? = null, text: String, actionButton: @Composable (() -> Unit)? = null) {

View file

@ -55,11 +55,11 @@ import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.connections.isIPAddress
import kotlinx.coroutines.launch
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("MagicNumber", "LongMethod")

View file

@ -38,14 +38,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.TopLevelDestination
import com.geeksville.mesh.ui.common.icons.Device
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.icons.NoDevice
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.connections.DeviceType
import org.meshtastic.core.ui.icon.Device
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.NoDevice
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@Composable
fun TopLevelNavIcon(destination: TopLevelDestination, connectionState: ConnectionState, deviceType: DeviceType?) {

View file

@ -26,9 +26,9 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun UsbDevices(

View file

@ -52,8 +52,8 @@ 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 com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongMethod")
@Composable

View file

@ -73,7 +73,6 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.size
import androidx.compose.ui.unit.sp
import androidx.datastore.core.IOException
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -81,15 +80,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import com.geeksville.mesh.ui.common.components.CopyIconButton
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.theme.AnnotationColor
import com.geeksville.mesh.ui.common.theme.AppTheme
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.SimpleAlertDialog
import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat

View file

@ -57,8 +57,8 @@ import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import com.geeksville.mesh.model.LogSearchManager.SearchMatch
import com.geeksville.mesh.model.LogSearchManager.SearchState
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
@Composable
internal fun DebugSearchNavigation(

View file

@ -98,7 +98,6 @@ import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannel
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.SharedContactDialog
@ -109,6 +108,7 @@ import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
import java.nio.charset.StandardCharsets
private const val MESSAGE_CHARACTER_LIMIT_BYTES = 200

View file

@ -71,12 +71,12 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.common.components.dragContainer
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.dragContainer
import org.meshtastic.core.ui.component.dragDropItemsIndexed
import org.meshtastic.core.ui.component.rememberDragDropState
import org.meshtastic.core.ui.theme.AppTheme
@Composable
internal fun QuickChatScreen(modifier: Modifier = Modifier, viewModel: UIViewModel = hiltViewModel()) {

View file

@ -49,12 +49,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.MDText
import com.geeksville.mesh.ui.common.components.Rssi
import com.geeksville.mesh.ui.common.components.Snr
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.MessageItemColors
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import org.meshtastic.core.database.entity.Reaction
@ -62,6 +57,11 @@ import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MDText
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MessageItemColors
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable

View file

@ -52,9 +52,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ui.common.components.BottomSheetDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.theme.AppTheme
@Composable
private fun ReactionItem(emoji: String, emojiCount: Int = 1, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}) {

View file

@ -63,14 +63,6 @@ import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.common.components.BatteryInfo
import com.geeksville.mesh.ui.common.components.OptionLabel
import com.geeksville.mesh.ui.common.components.SlidingSelector
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.GraphColors.Cyan
import com.geeksville.mesh.ui.common.theme.GraphColors.Green
import com.geeksville.mesh.ui.common.theme.GraphColors.Magenta
import com.geeksville.mesh.ui.common.theme.GraphColors.Red
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.metrics.CommonCharts.MAX_PERCENT_VALUE
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
@ -78,6 +70,13 @@ import com.geeksville.mesh.util.GraphUtil
import com.geeksville.mesh.util.GraphUtil.createPath
import com.geeksville.mesh.util.GraphUtil.plotPoint
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.BatteryInfo
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.GraphColors.Cyan
import org.meshtastic.core.ui.theme.GraphColors.Green
import org.meshtastic.core.ui.theme.GraphColors.Magenta
private const val CHART_WEIGHT = 1f
private const val Y_AXIS_WEIGHT = 0.1f

View file

@ -53,14 +53,14 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.common.components.IaqDisplayMode
import com.geeksville.mesh.ui.common.components.IndoorAirQuality
import com.geeksville.mesh.ui.common.components.OptionLabel
import com.geeksville.mesh.ui.common.components.SlidingSelector
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.IaqDisplayMode
import org.meshtastic.core.ui.component.IndoorAirQuality
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
@Composable
fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {

View file

@ -55,10 +55,10 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DecimalFormat
@OptIn(ExperimentalFoundationApi::class)

View file

@ -57,11 +57,11 @@ import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.common.components.OptionLabel
import com.geeksville.mesh.ui.common.components.SlidingSelector
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import java.text.DateFormat
import java.util.Date

View file

@ -64,10 +64,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DateFormat
import kotlin.time.Duration.Companion.days

View file

@ -62,15 +62,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.common.components.OptionLabel
import com.geeksville.mesh.ui.common.components.SlidingSelector
import com.geeksville.mesh.ui.common.theme.GraphColors.InfantryBlue
import com.geeksville.mesh.ui.common.theme.GraphColors.Red
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.util.GraphUtil
import com.geeksville.mesh.util.GraphUtil.createPath
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
import org.meshtastic.core.ui.theme.GraphColors.Red
import kotlin.math.ceil
import kotlin.math.floor

View file

@ -59,14 +59,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.TimeFrame
import com.geeksville.mesh.ui.common.components.LoraSignalIndicator
import com.geeksville.mesh.ui.common.components.OptionLabel
import com.geeksville.mesh.ui.common.components.SlidingSelector
import com.geeksville.mesh.ui.common.components.SnrAndRssi
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.util.GraphUtil.plotPoint
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.LoraSignalIndicator
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.component.SnrAndRssi
@Suppress("MagicNumber")
private enum class Metric(val color: Color, val min: Float, val max: Float) {

View file

@ -58,12 +58,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DateFormat
@OptIn(ExperimentalFoundationApi::class)

View file

@ -134,13 +134,7 @@ import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.node.components.TracerouteButton
@ -168,6 +162,12 @@ import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
private data class VectorMetricInfo(
@StringRes val label: Int,

View file

@ -49,7 +49,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.rememberTimeTickWithLifecycle
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
import com.geeksville.mesh.ui.node.components.NodeItem
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@ -60,6 +59,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")

View file

@ -31,8 +31,8 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun LastHeardInfo(modifier: Modifier = Modifier, lastHeard: Int, currentTimeMillis: Long) {

View file

@ -41,10 +41,10 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.HyperlinkBlue
import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.HyperlinkBlue
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)

View file

@ -57,9 +57,9 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.preview.LargeFontPreview
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongParameterList")
@Composable

View file

@ -51,14 +51,14 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ui.common.components.BatteryInfo
import com.geeksville.mesh.ui.common.components.SignalInfo
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.BatteryInfo
import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable

View file

@ -54,14 +54,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.geeksville.mesh.ui.common.components.CopyIconButton
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.google.protobuf.ByteString
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@Composable
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) =

View file

@ -38,10 +38,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SimpleAlertDialog
@Suppress("LongMethod")
@Composable

View file

@ -39,10 +39,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
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
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)

View file

@ -30,17 +30,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SatelliteCountInfo(
modifier: Modifier = Modifier,
satCount: Int,
) {
fun SatelliteCountInfo(modifier: Modifier = Modifier, satCount: Int) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(18.dp),
@ -59,9 +56,5 @@ fun SatelliteCountInfo(
@PreviewLightDark
@Composable
fun SatelliteCountInfoPreview() {
AppTheme {
SatelliteCountInfo(
satCount = 5,
)
}
AppTheme { SatelliteCountInfo(satCount = 5) }
}

View file

@ -34,9 +34,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.settings.components.SettingsItem
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
private const val COOL_DOWN_TIME_MS = 30000L

View file

@ -60,9 +60,6 @@ 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.common.components.MultipleChoiceAlertDialog
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
@ -78,6 +75,9 @@ 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.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

View file

@ -38,7 +38,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.theme.AppTheme
import org.meshtastic.core.ui.theme.AppTheme
/** A clickable settings button item. */
@Composable

View file

@ -44,14 +44,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.components.WarningDialog
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable

View file

@ -28,11 +28,11 @@ 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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -30,11 +30,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -30,11 +30,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun BluetoothConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -36,11 +36,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -73,16 +73,16 @@ 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.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.common.components.dragContainer
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.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.dragContainer
import org.meshtastic.core.ui.component.dragDropItemsIndexed
import org.meshtastic.core.ui.component.rememberDragDropState
@Composable
private fun ChannelItem(

View file

@ -33,11 +33,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -51,11 +51,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
private val DeviceConfig.Role.description: Int
get() =

View file

@ -30,11 +30,11 @@ 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.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -46,11 +46,11 @@ 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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
@Suppress("LongMethod")
@Composable

View file

@ -41,9 +41,9 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.google.protobuf.Descriptors
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SwitchPreference
private const val SUPPORTED_FIELDS = 7

View file

@ -34,12 +34,12 @@ 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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.common.components.TextDividerPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TextDividerPreference
@Composable
fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -33,15 +33,15 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.RegionInfo
import org.meshtastic.core.model.numChannels
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -33,12 +33,12 @@ 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.EditPasswordPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun MQTTConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -40,12 +40,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
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 kotlin.math.roundToInt
private const val POSITION_PRECISION_MIN = 12

View file

@ -28,11 +28,11 @@ 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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun NeighborInfoConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -44,16 +44,16 @@ 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.common.components.EditIPv4Preference
import com.geeksville.mesh.ui.common.components.EditPasswordPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.components.SwitchPreference
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.EditIPv4Preference
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =

View file

@ -28,12 +28,12 @@ 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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
@Composable
fun PaxcounterConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {

View file

@ -43,17 +43,17 @@ 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.BitwisePreference
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
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.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
@OptIn(ExperimentalPermissionsApi::class)
@Composable

Some files were not shown because too many files have changed in this diff Show more