diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MaterialBatteryInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MaterialBatteryInfo.kt new file mode 100644 index 000000000..2ff3b1fc1 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MaterialBatteryInfo.kt @@ -0,0 +1,129 @@ +/* + * 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 . + */ + +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.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 = 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 = BatteryEmpty, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = null, + ) + + Text( + text = levelString, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + ) + } + } +} + +class BatteryLevelProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf(-1, 19, 39, 90, 101) +} + +@PreviewLightDark +@Composable +fun MaterialBatteryInfoPreview(@PreviewParameter(BatteryLevelProvider::class) batteryLevel: Int) { + AppTheme { MaterialBatteryInfo(level = batteryLevel) } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/icons/Battery.kt b/app/src/main/java/com/geeksville/mesh/ui/common/icons/Battery.kt new file mode 100644 index 000000000..382844980 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/icons/Battery.kt @@ -0,0 +1,184 @@ +/* + * 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 . + */ + +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?selected=Material+Symbols+Outlined:battery_android_0:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android) + */ +val 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?selected=Material+Symbols+Outlined:battery_android_question:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android) + */ +val 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