kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Merge branch 'master' into favorite-nodes
commit
da5cfaaba5
|
@ -150,12 +150,12 @@ dependencies {
|
|||
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.5'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.6'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
||||
implementation 'androidx.datastore:datastore:1.1.1'
|
||||
implementation 'androidx.datastore:datastore:1.1.2'
|
||||
|
||||
// Lifecycle
|
||||
def lifecycle_version = '2.8.7'
|
||||
|
@ -184,12 +184,12 @@ dependencies {
|
|||
kspAndroidTest "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
|
||||
// Navigation
|
||||
def nav_version = "2.8.5"
|
||||
def nav_version = "2.8.7"
|
||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
|
||||
|
||||
// Compose
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
|
||||
def composeBom = platform('androidx.compose:compose-bom:2025.02.00')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
|
||||
|
@ -221,13 +221,13 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
|
||||
// kotlin serialization
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0"
|
||||
|
||||
// rate this app
|
||||
googleImplementation 'com.suddenh4x.ratingdialog:awesome-app-rating:2.7.0'
|
||||
googleImplementation 'com.suddenh4x.ratingdialog:awesome-app-rating:2.8.0'
|
||||
|
||||
// Coroutines
|
||||
def coroutines_version = '1.9.0'
|
||||
def coroutines_version = '1.10.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines_version"
|
||||
|
||||
|
@ -238,7 +238,7 @@ dependencies {
|
|||
implementation 'com.github.mik3y:usb-serial-for-android:3.8.1'
|
||||
|
||||
// For Firebase Crashlytics & Analytics
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:33.7.0')
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:33.9.0')
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics'
|
||||
googleImplementation 'com.google.firebase:firebase-analytics'
|
||||
|
||||
|
|
|
@ -270,6 +270,22 @@
|
|||
],
|
||||
"requiresDfu": true
|
||||
},
|
||||
{
|
||||
"hwModel": 22,
|
||||
"hwModelSlug": "WISMESH_HUB",
|
||||
"platformioTarget": "rak2560",
|
||||
"architecture": "nrf52840",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "RAK WisMesh Repeater",
|
||||
"tags": [
|
||||
"RAK"
|
||||
],
|
||||
"images": [
|
||||
"rak2560.svg"
|
||||
],
|
||||
"requiresDfu": true
|
||||
},
|
||||
{
|
||||
"hwModel": 25,
|
||||
"hwModelSlug": "STATION_G1",
|
||||
|
|
|
@ -78,6 +78,7 @@ private fun getDrawableFrom(hwModel: Int): Int = when (hwModel) {
|
|||
HardwareModel.RPI_PICO_VALUE -> R.drawable.hw_pico
|
||||
HardwareModel.NRF52_PROMICRO_DIY_VALUE -> R.drawable.hw_promicro
|
||||
HardwareModel.RAK11310_VALUE -> R.drawable.hw_rak11310
|
||||
HardwareModel.RAK2560_VALUE -> R.drawable.hw_rak2560
|
||||
HardwareModel.RAK4631_VALUE -> R.drawable.hw_rak4631_case
|
||||
HardwareModel.RPI_PICO2_VALUE -> R.drawable.hw_rpipicow
|
||||
HardwareModel.SENSECAP_INDICATOR_VALUE -> R.drawable.hw_seeed_sensecap_indicator
|
||||
|
|
|
@ -74,6 +74,7 @@ data class MetricsState(
|
|||
val deviceMetrics: List<Telemetry> = emptyList(),
|
||||
val environmentMetrics: List<Telemetry> = emptyList(),
|
||||
val signalMetrics: List<MeshPacket> = emptyList(),
|
||||
val powerMetrics: List<Telemetry> = emptyList(),
|
||||
val tracerouteRequests: List<MeshLog> = emptyList(),
|
||||
val tracerouteResults: List<MeshPacket> = emptyList(),
|
||||
val positionLogs: List<Position> = emptyList(),
|
||||
|
@ -82,6 +83,7 @@ data class MetricsState(
|
|||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
|
||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||
fun hasPowerMetrics() = powerMetrics.isNotEmpty()
|
||||
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
||||
|
||||
|
@ -100,6 +102,11 @@ data class MetricsState(
|
|||
return signalMetrics.filter { it.rxTime >= oldestTime }
|
||||
}
|
||||
|
||||
fun powerMetricsFiltered(timeFrame: TimeFrame): List<Telemetry> {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
return powerMetrics.filter { it.time >= oldestTime }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Empty = MetricsState()
|
||||
}
|
||||
|
@ -247,7 +254,8 @@ class MetricsViewModel @Inject constructor(
|
|||
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
||||
environmentMetrics = telemetry.filter {
|
||||
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
|
||||
}
|
||||
},
|
||||
powerMetrics = telemetry.filter { it.hasPowerMetrics() }
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
|
|
@ -86,13 +86,13 @@ import javax.inject.Inject
|
|||
import kotlin.math.roundToInt
|
||||
|
||||
// Given a human name, strip out the first letter of the first three words and return that as the initials for
|
||||
// that user. If the original name is only one word, strip vowels from the original name and if the result is
|
||||
// 3 or more characters, use the first three characters. If not, just take the first 3 characters of the
|
||||
// original name.
|
||||
// that user, ignoring emojis. If the original name is only one word, strip vowels from the original
|
||||
// name and if the result is 3 or more characters, use the first three characters. If not, just take
|
||||
// the first 3 characters of the original name.
|
||||
fun getInitials(nameIn: String): String {
|
||||
val nchars = 4
|
||||
val minchars = 2
|
||||
val name = nameIn.trim()
|
||||
val name = nameIn.trim().withoutEmojis()
|
||||
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }
|
||||
|
||||
val initials = when (words.size) {
|
||||
|
@ -109,6 +109,8 @@ fun getInitials(nameIn: String): String {
|
|||
return initials.take(nchars)
|
||||
}
|
||||
|
||||
private fun String.withoutEmojis(): String = filterNot { char -> char.isSurrogate() }
|
||||
|
||||
/**
|
||||
* Builds a [Channel] list from the difference between two [ChannelSettings] lists.
|
||||
* Only changes are included in the resulting list.
|
||||
|
|
|
@ -28,6 +28,7 @@ import com.geeksville.mesh.ui.components.DeviceMetricsScreen
|
|||
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
|
||||
import com.geeksville.mesh.ui.components.NodeMapScreen
|
||||
import com.geeksville.mesh.ui.components.PositionLogScreen
|
||||
import com.geeksville.mesh.ui.components.PowerMetricsScreen
|
||||
import com.geeksville.mesh.ui.components.SignalMetricsScreen
|
||||
import com.geeksville.mesh.ui.components.TracerouteLogScreen
|
||||
|
||||
|
@ -67,6 +68,12 @@ fun NavGraphBuilder.addNodDetailSection(navController: NavController) {
|
|||
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
|
||||
)
|
||||
}
|
||||
composable<Route.PowerMetrics> {
|
||||
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
|
||||
PowerMetricsScreen(
|
||||
viewModel = hiltViewModel<MetricsViewModel>(parentEntry),
|
||||
)
|
||||
}
|
||||
composable<Route.TracerouteLog> {
|
||||
val parentEntry = remember { navController.getBackStackEntry<Route.NodeDetail>() }
|
||||
TracerouteLogScreen(
|
||||
|
|
|
@ -66,5 +66,6 @@ sealed interface Route {
|
|||
@Serializable data object PositionLog : Route
|
||||
@Serializable data object EnvironmentMetrics : Route
|
||||
@Serializable data object SignalMetrics : Route
|
||||
@Serializable data object PowerMetrics : Route
|
||||
@Serializable data object TracerouteLog : Route
|
||||
}
|
||||
|
|
|
@ -363,6 +363,14 @@ fun LogNavigationList(state: MetricsState, onNavigate: (Route) -> Unit) {
|
|||
onNavigate(Route.SignalMetrics)
|
||||
}
|
||||
|
||||
NavCard(
|
||||
title = stringResource(R.string.power_metrics_log),
|
||||
icon = Icons.Default.Power,
|
||||
enabled = state.hasPowerMetrics()
|
||||
) {
|
||||
onNavigate(Route.PowerMetrics)
|
||||
}
|
||||
|
||||
NavCard(
|
||||
title = stringResource(R.string.traceroute_log),
|
||||
icon = Icons.Default.Route,
|
||||
|
@ -457,14 +465,14 @@ private fun EnvironmentMetrics(
|
|||
InfoCard(
|
||||
icon = Icons.Default.Speed,
|
||||
text = "Pressure",
|
||||
value = "%.0f".format(barometricPressure)
|
||||
value = "%.0f hPa".format(barometricPressure)
|
||||
)
|
||||
}
|
||||
if (gasResistance != 0f) {
|
||||
InfoCard(
|
||||
icon = Icons.Default.BlurOn,
|
||||
text = "Gas Resistance",
|
||||
value = "%.0f".format(gasResistance)
|
||||
value = "%.0f MΩ".format(gasResistance)
|
||||
)
|
||||
}
|
||||
if (voltage != 0f) {
|
||||
|
@ -499,7 +507,7 @@ private fun EnvironmentMetrics(
|
|||
InfoCard(
|
||||
icon = Icons.Default.LightMode,
|
||||
text = "Lux",
|
||||
value = "%.0f".format(lux)
|
||||
value = "%.0f lx".format(lux)
|
||||
)
|
||||
}
|
||||
if (hasWindSpeed()) {
|
||||
|
@ -523,7 +531,7 @@ private fun EnvironmentMetrics(
|
|||
InfoCard(
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_filled_radioactive_24),
|
||||
text = "Radiation",
|
||||
value = "%.1f µR".format(radiation)
|
||||
value = "%.1f µR/h".format(radiation)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
|||
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MAX_PERCENT_VALUE
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import java.text.DateFormat
|
||||
|
||||
|
@ -71,12 +72,13 @@ object CommonCharts {
|
|||
const val MS_PER_SEC = 1000L
|
||||
const val LINE_LIMIT = 4
|
||||
const val TEXT_PAINT_ALPHA = 192
|
||||
const val MAX_PERCENT_VALUE = 100f
|
||||
}
|
||||
|
||||
private val TIME_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
private val DATE_FORMAT: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
private const val LINE_ON = 10f
|
||||
private const val LINE_OFF = 20f
|
||||
private val TIME_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
private val DATE_FORMAT: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
private const val DATE_Y = 32f
|
||||
|
||||
data class LegendData(val nameRes: Int, val color: Color, val isLine: Boolean = false)
|
||||
|
@ -104,7 +106,7 @@ fun ChartHeader(amount: Int) {
|
|||
* @param lineColors A list of 5 `Color`s for the chart lines, 0 being the lowest line on the chart.
|
||||
* @param leaveSpace When true the lines will leave space for Y labels on the left side of the graph.
|
||||
*/
|
||||
@Deprecated("Will soon be replaced with YAxisLabels() and HorizontalLines()", level = DeprecationLevel.WARNING)
|
||||
@Deprecated("Will soon be replaced with YAxisLabels() and HorizontalLinesOverlay()", level = DeprecationLevel.WARNING)
|
||||
@Composable
|
||||
fun ChartOverlay(
|
||||
modifier: Modifier,
|
||||
|
@ -166,7 +168,7 @@ fun ChartOverlay(
|
|||
}
|
||||
|
||||
/**
|
||||
* Draws chart lines with respect to the Y-axis range; defined by (`maxValue` - `minValue`).
|
||||
* Draws chart lines with respect to the Y-axis.
|
||||
*
|
||||
* @param lineColors A list of 5 `Color`s for the chart lines, 0 being the lowest line on the chart.
|
||||
*/
|
||||
|
@ -174,21 +176,18 @@ fun ChartOverlay(
|
|||
fun HorizontalLinesOverlay(
|
||||
modifier: Modifier,
|
||||
lineColors: List<Color>,
|
||||
minValue: Float,
|
||||
maxValue: Float,
|
||||
) {
|
||||
val range = maxValue - minValue
|
||||
val verticalSpacing = range / LINE_LIMIT
|
||||
/* 100 is a good number to divide into quarters */
|
||||
val verticalSpacing = MAX_PERCENT_VALUE / LINE_LIMIT
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val lineStart = 0f
|
||||
val height = size.height
|
||||
val width = size.width
|
||||
|
||||
/* Horizontal Lines */
|
||||
var lineY = minValue
|
||||
var lineY = 0f
|
||||
for (i in 0..LINE_LIMIT) {
|
||||
val ratio = (lineY - minValue) / range
|
||||
val ratio = lineY / MAX_PERCENT_VALUE
|
||||
val y = height - (ratio * height)
|
||||
drawLine(
|
||||
start = Offset(lineStart, y),
|
||||
|
@ -322,11 +321,7 @@ fun TimeLabels(
|
|||
oldest: Int,
|
||||
newest: Int
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = DATE_TIME_FORMAT.format(oldest * MS_PER_SEC),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
|
@ -350,7 +345,11 @@ fun TimeLabels(
|
|||
* @param promptInfoDialog Executes when the user presses the info icon.
|
||||
*/
|
||||
@Composable
|
||||
fun Legend(legendData: List<LegendData>, promptInfoDialog: () -> Unit) {
|
||||
fun Legend(
|
||||
legendData: List<LegendData>,
|
||||
displayInfoIcon: Boolean = true,
|
||||
promptInfoDialog: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -367,13 +366,14 @@ fun Legend(legendData: List<LegendData>, promptInfoDialog: () -> Unit) {
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
modifier = Modifier.clickable { promptInfoDialog() },
|
||||
contentDescription = stringResource(R.string.info)
|
||||
)
|
||||
if (displayInfoIcon) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
modifier = Modifier.clickable { promptInfoDialog() },
|
||||
contentDescription = stringResource(R.string.info)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
|
|
@ -64,22 +64,21 @@ import com.geeksville.mesh.model.TimeFrame
|
|||
import com.geeksville.mesh.ui.BatteryInfo
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MAX_PERCENT_VALUE
|
||||
import com.geeksville.mesh.ui.theme.Orange
|
||||
import com.geeksville.mesh.util.GraphUtil
|
||||
import com.geeksville.mesh.util.GraphUtil.plotPoint
|
||||
import com.geeksville.mesh.util.GraphUtil.createPath
|
||||
|
||||
private val DEVICE_METRICS_COLORS = listOf(Color.Green, Color.Magenta, Color.Cyan)
|
||||
private const val MAX_PERCENT_VALUE = 100f
|
||||
private enum class Device {
|
||||
BATTERY,
|
||||
CH_UTIL,
|
||||
AIR_UTIL
|
||||
private enum class Device(val color: Color) {
|
||||
BATTERY(Color.Green),
|
||||
CH_UTIL(Color.Magenta),
|
||||
AIR_UTIL(Color.Cyan)
|
||||
}
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.battery, color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal], isLine = true),
|
||||
LegendData(nameRes = R.string.channel_utilization, color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal]),
|
||||
LegendData(nameRes = R.string.air_utilization, color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal]),
|
||||
LegendData(nameRes = R.string.battery, color = Device.BATTERY.color, isLine = true),
|
||||
LegendData(nameRes = R.string.channel_utilization, color = Device.CH_UTIL.color),
|
||||
LegendData(nameRes = R.string.air_utilization, color = Device.AIR_UTIL.color),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
@ -112,11 +111,12 @@ fun DeviceMetricsScreen(
|
|||
promptInfoDialog = { displayInfoDialog = true }
|
||||
)
|
||||
|
||||
MetricsTimeSelector(
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedTimeFrame,
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||
) {
|
||||
TimeLabel(stringResource(it.strRes))
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
/* Device Metric Cards */
|
||||
|
@ -180,8 +180,6 @@ private fun DeviceMetricsChart(
|
|||
HorizontalLinesOverlay(
|
||||
modifier.width(dp),
|
||||
lineColors = listOf(graphColor, Orange, Color.Red, graphColor, graphColor),
|
||||
minValue = 0f,
|
||||
maxValue = 100f
|
||||
)
|
||||
|
||||
TimeAxisOverlay(
|
||||
|
@ -206,7 +204,7 @@ private fun DeviceMetricsChart(
|
|||
/* Channel Utilization */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
||||
color = Device.CH_UTIL.color,
|
||||
x = x,
|
||||
value = telemetry.deviceMetrics.channelUtilization,
|
||||
divisor = MAX_PERCENT_VALUE
|
||||
|
@ -215,7 +213,7 @@ private fun DeviceMetricsChart(
|
|||
/* Air Utilization Transmit */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
||||
color = Device.AIR_UTIL.color,
|
||||
x = x,
|
||||
value = telemetry.deviceMetrics.airUtilTx,
|
||||
divisor = MAX_PERCENT_VALUE
|
||||
|
@ -242,7 +240,7 @@ private fun DeviceMetricsChart(
|
|||
}
|
||||
drawPath(
|
||||
path = path,
|
||||
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
||||
color = Device.BATTERY.color,
|
||||
style = Stroke(
|
||||
width = GraphUtil.RADIUS,
|
||||
cap = StrokeCap.Round
|
||||
|
@ -260,7 +258,7 @@ private fun DeviceMetricsChart(
|
|||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Legend(legendData = LEGEND_DATA, promptInfoDialog)
|
||||
Legend(legendData = LEGEND_DATA, promptInfoDialog = promptInfoDialog)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
|
|
@ -61,30 +61,30 @@ import com.geeksville.mesh.R
|
|||
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.components.CommonCharts.X_AXIS_SPACING
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
|
||||
private val ENVIRONMENT_METRICS_COLORS = listOf(Color.Red, Color.Blue, Color.Green)
|
||||
private enum class Environment {
|
||||
TEMPERATURE,
|
||||
HUMIDITY,
|
||||
IAQ
|
||||
private enum class Environment(val color: Color) {
|
||||
TEMPERATURE(Color.Red),
|
||||
HUMIDITY(Color.Blue),
|
||||
IAQ(Color.Green)
|
||||
}
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(
|
||||
nameRes = R.string.temperature,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.TEMPERATURE.ordinal],
|
||||
color = Environment.TEMPERATURE.color,
|
||||
isLine = true
|
||||
),
|
||||
LegendData(
|
||||
nameRes = R.string.humidity,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.HUMIDITY.ordinal],
|
||||
color = Environment.HUMIDITY.color,
|
||||
isLine = true
|
||||
),
|
||||
LegendData(
|
||||
nameRes = R.string.iaq,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.IAQ.ordinal],
|
||||
color = Environment.IAQ.color,
|
||||
isLine = true
|
||||
),
|
||||
)
|
||||
|
@ -137,11 +137,12 @@ fun EnvironmentMetricsScreen(
|
|||
promptInfoDialog = { displayInfoDialog = true }
|
||||
)
|
||||
|
||||
MetricsTimeSelector(
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedTimeFrame,
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||
) {
|
||||
TimeLabel(stringResource(it.strRes))
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
/* Environment Metric Cards */
|
||||
|
@ -178,13 +179,13 @@ private fun EnvironmentMetricsChart(
|
|||
|
||||
val graphColor = MaterialTheme.colors.onSurface
|
||||
val transparentTemperatureColor = remember {
|
||||
ENVIRONMENT_METRICS_COLORS[Environment.TEMPERATURE.ordinal].copy(alpha = 0.5f)
|
||||
Environment.TEMPERATURE.color.copy(alpha = 0.5f)
|
||||
}
|
||||
val transparentHumidityColor = remember {
|
||||
ENVIRONMENT_METRICS_COLORS[Environment.HUMIDITY.ordinal].copy(alpha = 0.5f)
|
||||
Environment.HUMIDITY.color.copy(alpha = 0.5f)
|
||||
}
|
||||
val transparentIAQColor = remember {
|
||||
ENVIRONMENT_METRICS_COLORS[Environment.IAQ.ordinal].copy(alpha = 0.5f)
|
||||
Environment.IAQ.color.copy(alpha = 0.5f)
|
||||
}
|
||||
val spacing = X_AXIS_SPACING
|
||||
|
||||
|
@ -280,7 +281,7 @@ private fun EnvironmentMetricsChart(
|
|||
|
||||
drawPath(
|
||||
path = temperaturePath,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.TEMPERATURE.ordinal],
|
||||
color = Environment.TEMPERATURE.color,
|
||||
style = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
|
@ -333,7 +334,7 @@ private fun EnvironmentMetricsChart(
|
|||
|
||||
drawPath(
|
||||
path = humidityPath,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.HUMIDITY.ordinal],
|
||||
color = Environment.HUMIDITY.color,
|
||||
style = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
|
@ -388,7 +389,7 @@ private fun EnvironmentMetricsChart(
|
|||
|
||||
drawPath(
|
||||
path = iaqPath,
|
||||
color = ENVIRONMENT_METRICS_COLORS[Environment.IAQ.ordinal],
|
||||
color = Environment.IAQ.color,
|
||||
style = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
cap = StrokeCap.Round
|
||||
|
@ -399,7 +400,7 @@ private fun EnvironmentMetricsChart(
|
|||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Legend(LEGEND_DATA, promptInfoDialog)
|
||||
Legend(LEGEND_DATA, promptInfoDialog = promptInfoDialog)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,371 @@
|
|||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.TimeFrame
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.util.GraphUtil
|
||||
import com.geeksville.mesh.util.GraphUtil.createPath
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private enum class Power(val color: Color, val min: Float, val max: Float) {
|
||||
CURRENT(Color(75, 119, 190), -500f, 500f),
|
||||
VOLTAGE(Color.Red, 0f, 20f);
|
||||
/**
|
||||
* Difference between the metrics `max` and `min` values.
|
||||
*/
|
||||
fun difference() = max - min
|
||||
}
|
||||
private enum class PowerChannel(@StringRes val strRes: Int) {
|
||||
ONE(R.string.channel_1),
|
||||
TWO(R.string.channel_2),
|
||||
THREE(R.string.channel_3)
|
||||
}
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.current, color = Power.CURRENT.color, isLine = true),
|
||||
LegendData(nameRes = R.string.voltage, color = Power.VOLTAGE.color, isLine = true),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PowerMetricsScreen(
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
||||
var selectedChannel by remember { mutableStateOf(PowerChannel.ONE) }
|
||||
val data = state.powerMetricsFiltered(selectedTimeFrame)
|
||||
|
||||
Column {
|
||||
|
||||
PowerMetricsChart(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.33f),
|
||||
telemetries = data.reversed(),
|
||||
selectedTimeFrame,
|
||||
selectedChannel,
|
||||
)
|
||||
|
||||
SlidingSelector(
|
||||
PowerChannel.entries.toList(),
|
||||
selectedChannel,
|
||||
onOptionSelected = { selectedChannel = it }
|
||||
) {
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedTimeFrame,
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||
) {
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(data) { telemetry -> PowerMetricsCard(telemetry) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun PowerMetricsChart(
|
||||
modifier: Modifier = Modifier,
|
||||
telemetries: List<Telemetry>,
|
||||
selectedTime: TimeFrame,
|
||||
selectedChannel: PowerChannel,
|
||||
) {
|
||||
ChartHeader(amount = telemetries.size)
|
||||
if (telemetries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val (oldest, newest) = remember(key1 = telemetries) {
|
||||
Pair(
|
||||
telemetries.minBy { it.time },
|
||||
telemetries.maxBy { it.time }
|
||||
)
|
||||
}
|
||||
val timeDiff = newest.time - oldest.time
|
||||
|
||||
TimeLabels(
|
||||
oldest = oldest.time,
|
||||
newest = newest.time
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val graphColor = MaterialTheme.colors.onSurface
|
||||
val currentDiff = Power.CURRENT.difference()
|
||||
val voltageDiff = Power.VOLTAGE.difference()
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidth = configuration.screenWidthDp
|
||||
val dp by remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong()))
|
||||
}
|
||||
|
||||
Row {
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
Power.CURRENT.color,
|
||||
minValue = Power.CURRENT.min,
|
||||
maxValue = Power.CURRENT.max,
|
||||
)
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.horizontalScroll(state = scrollState, reverseScrolling = true)
|
||||
.weight(1f)
|
||||
) {
|
||||
HorizontalLinesOverlay(
|
||||
modifier.width(dp),
|
||||
lineColors = List(size = 5) { graphColor },
|
||||
)
|
||||
|
||||
TimeAxisOverlay(
|
||||
modifier.width(dp),
|
||||
oldest = oldest.time,
|
||||
newest = newest.time,
|
||||
selectedTime.lineInterval()
|
||||
)
|
||||
|
||||
/* Plot */
|
||||
Canvas(modifier = modifier.width(dp)) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
/* Voltage */
|
||||
var index = 0
|
||||
while (index < telemetries.size) {
|
||||
val path = Path()
|
||||
index = createPath(
|
||||
telemetries = telemetries,
|
||||
index = index,
|
||||
path = path,
|
||||
oldestTime = oldest.time,
|
||||
timeRange = timeDiff,
|
||||
width = width,
|
||||
timeThreshold = selectedTime.timeThreshold()
|
||||
) { i ->
|
||||
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
|
||||
val ratio = retrieveVoltage(selectedChannel, telemetry) / voltageDiff
|
||||
val y = height - (ratio * height)
|
||||
return@createPath y
|
||||
}
|
||||
drawPath(
|
||||
path = path,
|
||||
color = Power.VOLTAGE.color,
|
||||
style = Stroke(
|
||||
width = GraphUtil.RADIUS,
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
)
|
||||
}
|
||||
/* Current */
|
||||
index = 0
|
||||
while (index < telemetries.size) {
|
||||
val path = Path()
|
||||
index = createPath(
|
||||
telemetries = telemetries,
|
||||
index = index,
|
||||
path = path,
|
||||
oldestTime = oldest.time,
|
||||
timeRange = timeDiff,
|
||||
width = width,
|
||||
timeThreshold = selectedTime.timeThreshold()
|
||||
) { i ->
|
||||
val telemetry = telemetries.getOrNull(i) ?: telemetries.last()
|
||||
val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff
|
||||
val y = height - (ratio * height)
|
||||
return@createPath y
|
||||
}
|
||||
drawPath(
|
||||
path = path,
|
||||
color = Power.CURRENT.color,
|
||||
style = Stroke(
|
||||
width = GraphUtil.RADIUS,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
Power.VOLTAGE.color,
|
||||
minValue = Power.VOLTAGE.min,
|
||||
maxValue = Power.VOLTAGE.max,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Legend(legendData = LEGEND_DATA, displayInfoIcon = false)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PowerMetricsCard(telemetry: Telemetry) {
|
||||
val time = telemetry.time * MS_PER_SEC
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Surface {
|
||||
SelectionContainer {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
) {
|
||||
/* Time */
|
||||
Row {
|
||||
Text(
|
||||
text = DATE_TIME_FORMAT.format(time),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = MaterialTheme.typography.button.fontSize
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (telemetry.powerMetrics.hasCh1Current() || telemetry.powerMetrics.hasCh1Voltage()) {
|
||||
PowerChannelColumn(
|
||||
R.string.channel_1,
|
||||
telemetry.powerMetrics.ch1Voltage,
|
||||
telemetry.powerMetrics.ch1Current
|
||||
)
|
||||
}
|
||||
if (telemetry.powerMetrics.hasCh2Current() || telemetry.powerMetrics.hasCh2Voltage()) {
|
||||
PowerChannelColumn(
|
||||
R.string.channel_2,
|
||||
telemetry.powerMetrics.ch2Voltage,
|
||||
telemetry.powerMetrics.ch2Current
|
||||
)
|
||||
}
|
||||
if (telemetry.powerMetrics.hasCh3Current() || telemetry.powerMetrics.hasCh3Voltage()) {
|
||||
PowerChannelColumn(
|
||||
R.string.channel_3,
|
||||
telemetry.powerMetrics.ch3Voltage,
|
||||
telemetry.powerMetrics.ch3Current
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PowerChannelColumn(@StringRes titleRes: Int, voltage: Float, current: Float) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
fontSize = MaterialTheme.typography.button.fontSize
|
||||
)
|
||||
Text(
|
||||
text = "%.2fV".format(voltage),
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = MaterialTheme.typography.button.fontSize
|
||||
)
|
||||
Text(
|
||||
text = "%.1fmA".format(current),
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontSize = MaterialTheme.typography.button.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate voltage depending on `channelSelected`.
|
||||
*/
|
||||
private fun retrieveVoltage(channelSelected: PowerChannel, telemetry: Telemetry): Float {
|
||||
return when (channelSelected) {
|
||||
PowerChannel.ONE -> telemetry.powerMetrics.ch1Voltage
|
||||
PowerChannel.TWO -> telemetry.powerMetrics.ch2Voltage
|
||||
PowerChannel.THREE -> telemetry.powerMetrics.ch3Voltage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate current depending on `channelSelected`.
|
||||
*/
|
||||
private fun retrieveCurrent(channelSelected: PowerChannel, telemetry: Telemetry): Float {
|
||||
return when (channelSelected) {
|
||||
PowerChannel.ONE -> telemetry.powerMetrics.ch1Current
|
||||
PowerChannel.TWO -> telemetry.powerMetrics.ch2Current
|
||||
PowerChannel.THREE -> telemetry.powerMetrics.ch3Current
|
||||
}
|
||||
}
|
|
@ -63,20 +63,18 @@ import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
|||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.util.GraphUtil.plotPoint
|
||||
|
||||
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private enum class Metric(val min: Float, val max: Float) {
|
||||
SNR(-20f, 12f), /* Selected 12 as the max to get 4 equal vertical sections. */
|
||||
RSSI(-140f, -20f);
|
||||
private enum class Metric(val color: Color, val min: Float, val max: Float) {
|
||||
SNR(Color.Green, -20f, 12f), /* Selected 12 as the max to get 4 equal vertical sections. */
|
||||
RSSI(Color.Blue, -140f, -20f);
|
||||
/**
|
||||
* Difference between the metrics `max` and `min` values.
|
||||
*/
|
||||
fun difference() = max - min
|
||||
}
|
||||
private val LEGEND_DATA = listOf(
|
||||
LegendData(nameRes = R.string.rssi, color = METRICS_COLORS[Metric.RSSI.ordinal]),
|
||||
LegendData(nameRes = R.string.snr, color = METRICS_COLORS[Metric.SNR.ordinal])
|
||||
LegendData(nameRes = R.string.rssi, color = Metric.RSSI.color),
|
||||
LegendData(nameRes = R.string.snr, color = Metric.SNR.color)
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
@ -109,11 +107,12 @@ fun SignalMetricsScreen(
|
|||
promptInfoDialog = { displayInfoDialog = true }
|
||||
)
|
||||
|
||||
MetricsTimeSelector(
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedTimeFrame,
|
||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||
) {
|
||||
TimeLabel(stringResource(it.strRes))
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
|
@ -166,7 +165,7 @@ private fun SignalMetricsChart(
|
|||
Row {
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
METRICS_COLORS[Metric.RSSI.ordinal],
|
||||
Metric.RSSI.color,
|
||||
minValue = Metric.RSSI.min,
|
||||
maxValue = Metric.RSSI.max,
|
||||
)
|
||||
|
@ -179,8 +178,6 @@ private fun SignalMetricsChart(
|
|||
HorizontalLinesOverlay(
|
||||
modifier.width(dp),
|
||||
lineColors = List(size = 5) { graphColor },
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max
|
||||
)
|
||||
|
||||
TimeAxisOverlay(
|
||||
|
@ -202,7 +199,7 @@ private fun SignalMetricsChart(
|
|||
/* SNR */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = METRICS_COLORS[Metric.SNR.ordinal],
|
||||
color = Metric.SNR.color,
|
||||
x = x,
|
||||
value = packet.rxSnr - Metric.SNR.min,
|
||||
divisor = snrDiff
|
||||
|
@ -211,7 +208,7 @@ private fun SignalMetricsChart(
|
|||
/* RSSI */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = METRICS_COLORS[Metric.RSSI.ordinal],
|
||||
color = Metric.RSSI.color,
|
||||
x = x,
|
||||
value = packet.rxRssi - Metric.RSSI.min,
|
||||
divisor = rssiDiff
|
||||
|
@ -221,7 +218,7 @@ private fun SignalMetricsChart(
|
|||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
METRICS_COLORS[Metric.SNR.ordinal],
|
||||
Metric.SNR.color,
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max,
|
||||
)
|
||||
|
@ -229,7 +226,7 @@ private fun SignalMetricsChart(
|
|||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Legend(legendData = LEGEND_DATA, promptInfoDialog)
|
||||
Legend(legendData = LEGEND_DATA, promptInfoDialog = promptInfoDialog)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
|
|
@ -94,20 +94,22 @@ private const val PRESSED_UNSELECTED_ALPHA = .6f
|
|||
private val BACKGROUND_SHAPE = RoundedCornerShape(8.dp)
|
||||
|
||||
/**
|
||||
* Provides the user with a set of time options they can choose from that controls
|
||||
* the time frame the data being plotted was received.
|
||||
* Provides the user with a set of options they can choose from.
|
||||
*
|
||||
* (Inspired by https://gist.github.com/zach-klippenstein/7ae8874db304f957d6bb91263e292117)
|
||||
*/
|
||||
@Composable
|
||||
fun MetricsTimeSelector(
|
||||
selectedTime: TimeFrame,
|
||||
onOptionSelected: (TimeFrame) -> Unit,
|
||||
fun <T : Any> SlidingSelector(
|
||||
options: List<T>,
|
||||
selectedOption: T,
|
||||
onOptionSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (TimeFrame) -> Unit
|
||||
content: @Composable (T) -> Unit
|
||||
) {
|
||||
val state = remember { TimeSelectorState() }
|
||||
state.selectedOption = state.options.indexOf(selectedTime)
|
||||
state.onOptionSelected = { onOptionSelected(state.options[it]) }
|
||||
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")
|
||||
|
@ -116,7 +118,7 @@ fun MetricsTimeSelector(
|
|||
content = {
|
||||
SelectedIndicator(state)
|
||||
Dividers(state)
|
||||
TimeOptions(state, content)
|
||||
Options(state, options, content)
|
||||
},
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -133,7 +135,7 @@ fun MetricsTimeSelector(
|
|||
/* Measure the indicator and dividers to be the right size. */
|
||||
val indicatorPlaceable = indicatorMeasurable.measure(
|
||||
Constraints.fixed(
|
||||
width = optionsPlaceable.width / state.options.size,
|
||||
width = optionsPlaceable.width / options.size,
|
||||
height = optionsPlaceable.height
|
||||
)
|
||||
)
|
||||
|
@ -146,7 +148,7 @@ fun MetricsTimeSelector(
|
|||
)
|
||||
|
||||
layout(optionsPlaceable.width, optionsPlaceable.height) {
|
||||
val optionWidth = optionsPlaceable.width / state.options.size
|
||||
val optionWidth = optionsPlaceable.width / options.size
|
||||
|
||||
/* Place the indicator first so that it's below the option labels. */
|
||||
indicatorPlaceable.placeRelative(
|
||||
|
@ -160,18 +162,18 @@ fun MetricsTimeSelector(
|
|||
}
|
||||
|
||||
/**
|
||||
* Visual representation of the time option the user may select.
|
||||
* Visual representation of the option the user may select.
|
||||
*/
|
||||
@Composable
|
||||
fun TimeLabel(text: String) {
|
||||
fun OptionLabel(text: String) {
|
||||
Text(text, maxLines = 1, overflow = Ellipsis)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the selected indicator on the [MetricsTimeSelector] track.
|
||||
* Draws the selected indicator on the [SlidingSelector] track.
|
||||
*/
|
||||
@Composable
|
||||
private fun SelectedIndicator(state: TimeSelectorState) {
|
||||
private fun SelectedIndicator(state: SelectorState) {
|
||||
Box(
|
||||
Modifier
|
||||
.then(
|
||||
|
@ -186,18 +188,18 @@ private fun SelectedIndicator(state: TimeSelectorState) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Draws dividers between [TimeLabel]s.
|
||||
* Draws dividers between [OptionLabel]s.
|
||||
*/
|
||||
@Composable
|
||||
private fun Dividers(state: TimeSelectorState) {
|
||||
private fun Dividers(state: SelectorState) {
|
||||
/* Animate each divider independently. */
|
||||
val alphas = (0 until state.options.size).map { i ->
|
||||
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.options.size
|
||||
val optionWidth = size.width / state.optionCount
|
||||
val dividerPadding = TRACK_PADDING + PRESSED_TRACK_PADDING
|
||||
|
||||
alphas.forEachIndexed { i, alpha ->
|
||||
|
@ -213,12 +215,13 @@ private fun Dividers(state: TimeSelectorState) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Draws the time options available to the user.
|
||||
* Draws the options available to the user.
|
||||
*/
|
||||
@Composable
|
||||
private fun TimeOptions(
|
||||
state: TimeSelectorState,
|
||||
content: @Composable (TimeFrame) -> Unit
|
||||
private fun <T> Options(
|
||||
state: SelectorState,
|
||||
options: List<T>,
|
||||
content: @Composable (T) -> Unit
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides TextStyle(fontWeight = FontWeight.Medium)
|
||||
|
@ -229,7 +232,7 @@ private fun TimeOptions(
|
|||
.fillMaxWidth()
|
||||
.selectableGroup()
|
||||
) {
|
||||
state.options.forEachIndexed { i, timeFrame ->
|
||||
options.forEachIndexed { i, timeFrame ->
|
||||
val isSelected = i == state.selectedOption
|
||||
val isPressed = i == state.pressedOption
|
||||
|
||||
|
@ -267,10 +270,10 @@ private fun TimeOptions(
|
|||
}
|
||||
|
||||
/**
|
||||
* Contains and handles the state necessary to present the [MetricsTimeSelector] to the user.
|
||||
* Contains and handles the state necessary to present the [SlidingSelector] to the user.
|
||||
*/
|
||||
private class TimeSelectorState {
|
||||
val options = TimeFrame.entries.toTypedArray()
|
||||
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)
|
||||
|
@ -317,7 +320,7 @@ private class TimeSelectorState {
|
|||
this.transformOrigin = TransformOrigin(
|
||||
pivotFractionX = when (option) {
|
||||
0 -> 0f
|
||||
options.size - 1 -> 1f
|
||||
optionCount - 1 -> 1f
|
||||
else -> .5f
|
||||
},
|
||||
pivotFractionY = .5f
|
||||
|
@ -326,7 +329,7 @@ private class TimeSelectorState {
|
|||
/* But should still move inwards to keep the pressed padding consistent with top and bottom. */
|
||||
this.translationX = when (option) {
|
||||
0 -> xOffset.toPx()
|
||||
options.size - 1 -> -xOffset.toPx()
|
||||
optionCount - 1 -> -xOffset.toPx()
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
|
@ -336,14 +339,14 @@ private class TimeSelectorState {
|
|||
* A [Modifier] that will listen for touch gestures and update the selected and pressed properties
|
||||
* of this state appropriately.
|
||||
*/
|
||||
val inputModifier = Modifier.pointerInput(options.size) {
|
||||
val optionWidth = size.width / options.size
|
||||
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()) * options.size)
|
||||
((change.position.x / size.width.toFloat()) * optionCount)
|
||||
.toInt()
|
||||
.coerceIn(0, options.size - 1)
|
||||
.coerceIn(0, optionCount - 1)
|
||||
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown()
|
||||
|
@ -401,17 +404,18 @@ private suspend fun AwaitPointerEventScope.waitForUpOrCancellation(inBounds: Rec
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MetricsTimeSelectorPreview() {
|
||||
fun SlidingSelectorPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
|
||||
var selectedOption by remember { mutableStateOf(TimeFrame.TWENTY_FOUR_HOURS) }
|
||||
MetricsTimeSelector(
|
||||
SlidingSelector(
|
||||
TimeFrame.entries.toList(),
|
||||
selectedOption,
|
||||
onOptionSelected = { selectedOption = it }
|
||||
) {
|
||||
TimeLabel(stringResource(it.strRes))
|
||||
OptionLabel(stringResource(it.strRes))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ object GraphUtil {
|
|||
* @param width of the [DrawContext]
|
||||
* @param timeThreshold to determine significant breaks in time between [Telemetry]s
|
||||
* @param calculateY (`index`) -> `y` coordinate
|
||||
* @return the current index after iterating
|
||||
*/
|
||||
fun createPath(
|
||||
telemetries: List<Telemetry>,
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="271.78dp"
|
||||
android:height="510.19dp"
|
||||
android:viewportWidth="271.78"
|
||||
android:viewportHeight="510.19">
|
||||
<path
|
||||
android:pathData="M77.04,477.29L107.91,477.29A3.38,3.38 0,0 1,111.29 480.67L111.29,499.14A3.38,3.38 0,0 1,107.91 502.52L77.04,502.52A3.38,3.38 0,0 1,73.66 499.14L73.66,480.67A3.38,3.38 0,0 1,77.04 477.29z"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M74.34,413.28L111.72,413.28A3.69,3.69 0,0 1,115.41 416.97L115.41,434.82A3.69,3.69 0,0 1,111.72 438.51L74.34,438.51A3.69,3.69 0,0 1,70.65 434.82L70.65,416.97A3.69,3.69 0,0 1,74.34 413.28z"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M107.91,502.57h0a7.12,7.12 0,0 1,-7.12 7.12L84.17,509.69A7.12,7.12 0,0 1,77.03 502.57h0"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M74.34,407.87h37.38v5.41h-37.38z"
|
||||
android:fillColor="#33353d"/>
|
||||
<path
|
||||
android:pathData="M110.33,438.51s-4.84,14.19 -4.84,38.78L79.18,477.29c0,-24.59 -4.84,-38.78 -4.84,-38.78Z"
|
||||
android:fillColor="#33353d"/>
|
||||
<path
|
||||
android:pathData="M74.56,415.61L77.15,415.61A0.9,0.9 0,0 1,78.05 416.51L78.05,434.85A0.9,0.9 0,0 1,77.15 435.75L74.56,435.75A0.9,0.9 0,0 1,73.66 434.85L73.66,416.51A0.9,0.9 0,0 1,74.56 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M83.24,415.61L85.83,415.61A0.9,0.9 0,0 1,86.73 416.51L86.73,434.85A0.9,0.9 0,0 1,85.83 435.75L83.24,435.75A0.9,0.9 0,0 1,82.34 434.85L82.34,416.51A0.9,0.9 0,0 1,83.24 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M91.91,415.61L94.5,415.61A0.9,0.9 0,0 1,95.4 416.51L95.4,434.85A0.9,0.9 0,0 1,94.5 435.75L91.91,435.75A0.9,0.9 0,0 1,91.01 434.85L91.01,416.51A0.9,0.9 0,0 1,91.91 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M100.58,415.61L103.17,415.61A0.9,0.9 0,0 1,104.07 416.51L104.07,434.85A0.9,0.9 0,0 1,103.17 435.75L100.58,435.75A0.9,0.9 0,0 1,99.68 434.85L99.68,416.51A0.9,0.9 0,0 1,100.58 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M109.25,415.61L111.84,415.61A0.9,0.9 0,0 1,112.74 416.51L112.74,434.85A0.9,0.9 0,0 1,111.84 435.75L109.25,435.75A0.9,0.9 0,0 1,108.35 434.85L108.35,416.51A0.9,0.9 0,0 1,109.25 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M76.09,479.84L78.29,479.84A0.85,0.85 0,0 1,79.14 480.69L79.14,499.13A0.85,0.85 0,0 1,78.29 499.98L76.09,499.98A0.85,0.85 0,0 1,75.24 499.13L75.24,480.69A0.85,0.85 0,0 1,76.09 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M83.79,479.84L85.99,479.84A0.85,0.85 0,0 1,86.84 480.69L86.84,499.13A0.85,0.85 0,0 1,85.99 499.98L83.79,499.98A0.85,0.85 0,0 1,82.94 499.13L82.94,480.69A0.85,0.85 0,0 1,83.79 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M91.48,479.84L93.68,479.84A0.85,0.85 0,0 1,94.53 480.69L94.53,499.13A0.85,0.85 0,0 1,93.68 499.98L91.48,499.98A0.85,0.85 0,0 1,90.63 499.13L90.63,480.69A0.85,0.85 0,0 1,91.48 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M99.18,479.84L101.38,479.84A0.85,0.85 0,0 1,102.23 480.69L102.23,499.13A0.85,0.85 0,0 1,101.38 499.98L99.18,499.98A0.85,0.85 0,0 1,98.33 499.13L98.33,480.69A0.85,0.85 0,0 1,99.18 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M106.88,479.84L109.08,479.84A0.85,0.85 0,0 1,109.93 480.69L109.93,499.13A0.85,0.85 0,0 1,109.08 499.98L106.88,499.98A0.85,0.85 0,0 1,106.03 499.13L106.03,480.69A0.85,0.85 0,0 1,106.88 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M153.54,477.29L184.41,477.29A3.38,3.38 0,0 1,187.79 480.67L187.79,499.14A3.38,3.38 0,0 1,184.41 502.52L153.54,502.52A3.38,3.38 0,0 1,150.16 499.14L150.16,480.67A3.38,3.38 0,0 1,153.54 477.29z"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M150.84,413.28L188.22,413.28A3.69,3.69 0,0 1,191.91 416.97L191.91,434.82A3.69,3.69 0,0 1,188.22 438.51L150.84,438.51A3.69,3.69 0,0 1,147.15 434.82L147.15,416.97A3.69,3.69 0,0 1,150.84 413.28z"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M184.41,502.57h0a7.12,7.12 0,0 1,-7.12 7.12L160.66,509.69a7.11,7.11 0,0 1,-7.11 -7.12h0"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:pathData="M150.84,407.87h37.38v5.41h-37.38z"
|
||||
android:fillColor="#33353d"/>
|
||||
<path
|
||||
android:pathData="M186.82,438.51S182.03,452.7 182.03,477.29L155.67,477.29c0,-24.59 -4.83,-38.78 -4.83,-38.78Z"
|
||||
android:fillColor="#33353d"/>
|
||||
<path
|
||||
android:pathData="M151.06,415.61L153.65,415.61A0.9,0.9 0,0 1,154.55 416.51L154.55,434.85A0.9,0.9 0,0 1,153.65 435.75L151.06,435.75A0.9,0.9 0,0 1,150.16 434.85L150.16,416.51A0.9,0.9 0,0 1,151.06 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M159.73,415.61L162.32,415.61A0.9,0.9 0,0 1,163.22 416.51L163.22,434.85A0.9,0.9 0,0 1,162.32 435.75L159.73,435.75A0.9,0.9 0,0 1,158.83 434.85L158.83,416.51A0.9,0.9 0,0 1,159.73 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M168.41,415.61L171,415.61A0.9,0.9 0,0 1,171.9 416.51L171.9,434.85A0.9,0.9 0,0 1,171 435.75L168.41,435.75A0.9,0.9 0,0 1,167.51 434.85L167.51,416.51A0.9,0.9 0,0 1,168.41 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M177.08,415.61L179.67,415.61A0.9,0.9 0,0 1,180.57 416.51L180.57,434.85A0.9,0.9 0,0 1,179.67 435.75L177.08,435.75A0.9,0.9 0,0 1,176.18 434.85L176.18,416.51A0.9,0.9 0,0 1,177.08 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M185.75,415.61L188.34,415.61A0.9,0.9 0,0 1,189.24 416.51L189.24,434.85A0.9,0.9 0,0 1,188.34 435.75L185.75,435.75A0.9,0.9 0,0 1,184.85 434.85L184.85,416.51A0.9,0.9 0,0 1,185.75 415.61z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M152.59,479.84L154.79,479.84A0.85,0.85 0,0 1,155.64 480.69L155.64,499.13A0.85,0.85 0,0 1,154.79 499.98L152.59,499.98A0.85,0.85 0,0 1,151.74 499.13L151.74,480.69A0.85,0.85 0,0 1,152.59 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M160.28,479.84L162.48,479.84A0.85,0.85 0,0 1,163.33 480.69L163.33,499.13A0.85,0.85 0,0 1,162.48 499.98L160.28,499.98A0.85,0.85 0,0 1,159.43 499.13L159.43,480.69A0.85,0.85 0,0 1,160.28 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M167.98,479.84L170.18,479.84A0.85,0.85 0,0 1,171.03 480.69L171.03,499.13A0.85,0.85 0,0 1,170.18 499.98L167.98,499.98A0.85,0.85 0,0 1,167.13 499.13L167.13,480.69A0.85,0.85 0,0 1,167.98 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M175.68,479.84L177.88,479.84A0.85,0.85 0,0 1,178.73 480.69L178.73,499.13A0.85,0.85 0,0 1,177.88 499.98L175.68,499.98A0.85,0.85 0,0 1,174.83 499.13L174.83,480.69A0.85,0.85 0,0 1,175.68 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M183.38,479.84L185.58,479.84A0.85,0.85 0,0 1,186.43 480.69L186.43,499.13A0.85,0.85 0,0 1,185.58 499.98L183.38,499.98A0.85,0.85 0,0 1,182.53 499.13L182.53,480.69A0.85,0.85 0,0 1,183.38 479.84z"
|
||||
android:fillColor="#0e5796"/>
|
||||
<path
|
||||
android:pathData="M271.28,50.5L271.28,357.87a50,50 0,0 1,-50 50L50.5,407.87a50,50 0,0 1,-50 -50L0.5,50.5a50,50 0,0 1,50 -50L221.27,0.5A50,50 0,0 1,271.28 50.5Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M271.28,50.5L271.28,160.18L0.5,160.18L0.5,50.5a50,50 0,0 1,50 -50L221.27,0.5A50,50 0,0 1,271.28 50.5Z"
|
||||
android:fillColor="#1c60ac"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M271.28,50.5L271.28,357.87a50,50 0,0 1,-50 50L50.5,407.87a50,50 0,0 1,-50 -50L0.5,50.5a50,50 0,0 1,50 -50L221.27,0.5A50,50 0,0 1,271.28 50.5Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M74.5,80.43a8.83,8.83 0,0 0,-5.13 0.45s-1.27,-2.38 -0.86,-7.18c0.16,-1.81 0.47,-4.26 2.63,-4.63l0.1,0a2.54,2.54 0,0 1,3.26 3.1C73.88,77.8 74.72,80.4 74.5,80.43Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M64.84,68.5a2.33,2.33 0,0 1,2.16 2.94,13 13,0 0,0 -0.33,4.26c0.27,4.34 1.76,5.88 1.76,5.88a4.28,4.28 0,0 0,-1.49 2.75,16.14 16.14,0 0,1 -4.74,-13 3.29,3.29 0,0 1,0.94 -2.14,2.34 2.34,0 0,1 1.41,-0.69A1.42,1.42 0,0 1,64.84 68.5Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M66.33,92.14A18.32,18.32 0,0 1,56.03 80.88a20.24,20.24 0,0 1,-1 -7.95,4.84 4.84,0 0,1 1,-3.08 2.87,2.87 0,0 1,4.25 1,5.05 5.05,0 0,1 0.08,2.19 14.84,14.84 0,0 0,0.62 5.95c1.39,4.89 6.95,9.07 6.95,9.07A5.94,5.94 0,0 0,66.33 92.14Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M76.03,74.46a8.93,8.93 0,0 0,-0.45 -5.13s2.38,-1.26 7.18,-0.86c1.81,0.16 4.26,0.48 4.63,2.64a0.29,0.29 0,0 0,0 0.09,2.55 2.55,0 0,1 -3.1,3.27C78.65,73.85 76.03,74.69 76.03,74.46Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M87.96,64.81a2.33,2.33 0,0 1,-2.94 2.15,13.43 13.43,0 0,0 -4.26,-0.32c-4.34,0.27 -5.88,1.75 -5.88,1.75a4.24,4.24 0,0 0,-2.75 -1.48,16.14 16.14,0 0,1 13,-4.74 3.28,3.28 0,0 1,2.14 0.93,2.36 2.36,0 0,1 0.69,1.41A1.51,1.51 0,0 1,87.96 64.81Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M64.32,66.3a18.29,18.29 0,0 1,11.26 -10.34,20.05 20.05,0 0,1 7.95,-1 4.77,4.77 0,0 1,3.08 1,2.87 2.87,0 0,1 -1,4.25 4.77,4.77 0,0 1,-2.19 0.08,15.07 15.07,0 0,0 -5.95,0.63c-4.89,1.39 -9.07,7 -9.07,7A5.94,5.94 0,0 0,64.32 66.3Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M81.89,76.41a8.93,8.93 0,0 0,5.13 -0.45s1.26,2.38 0.86,7.17c-0.16,1.82 -0.48,4.27 -2.64,4.63l-0.09,0a2.53,2.53 0,0 1,-3.26 -3.09C82.51,79.03 81.67,76.44 81.89,76.41Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M91.54,88.34a2.33,2.33 0,0 1,-2.15 -2.94,13.37 13.37,0 0,0 0.33,-4.26c-0.27,-4.34 -1.76,-5.88 -1.76,-5.88a4.28,4.28 0,0 0,1.49 -2.75,16.14 16.14,0 0,1 4.74,13 3.29,3.29 0,0 1,-0.94 2.14,2.36 2.36,0 0,1 -1.41,0.69Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M90.03,64.7a18.29,18.29 0,0 1,10.34 11.26,19.87 19.87,0 0,1 1,8 4.77,4.77 0,0 1,-1 3.08,2.87 2.87,0 0,1 -4.25,-1 4.91,4.91 0,0 1,-0.08 -2.19,15.21 15.21,0 0,0 -0.62,-5.95c-1.39,-4.89 -7,-9.07 -7,-9.07A5.89,5.89 0,0 0,90.03 64.7Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M80.36,82.38a8.79,8.79 0,0 0,0.45 5.12s-2.38,1.27 -7.18,0.87c-1.81,-0.16 -4.26,-0.48 -4.63,-2.64a0.29,0.29 0,0 0,0 -0.09,3 3,0 0,1 0.8,-2.79 3,3 0,0 1,2.3 -0.48C77.73,82.99 80.33,82.15 80.36,82.38Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M68.43,92.03a2.32,2.32 0,0 1,2.93 -2.15,13.5 13.5,0 0,0 4.27,0.32c4.34,-0.27 5.87,-1.75 5.87,-1.75a4.24,4.24 0,0 0,2.76 1.48,16.14 16.14,0 0,1 -13,4.74 3.23,3.23 0,0 1,-2.14 -0.93,2.43 2.43,0 0,1 -0.7,-1.41C68.43,92.23 68.43,92.13 68.43,92.03Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M92.03,90.57a18.29,18.29 0,0 1,-11.26 10.34,20.26 20.26,0 0,1 -7.95,1 4.76,4.76 0,0 1,-3.08 -1,2.87 2.87,0 0,1 1,-4.25 4.86,4.86 0,0 1,2.18 -0.07,15.11 15.11,0 0,0 6,-0.63c4.89,-1.39 9.06,-7 9.06,-7A6,6 0,0 0,92.03 90.57Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M118.13,81.86v13h-7.47L110.66,62.45h15.53c7.7,0 11.84,3.89 11.84,8.88a7.75,7.75 0,0 1,-5.13 7.66c1.75,0.59 4.39,2.26 4.39,8.05v1.6a23.55,23.55 0,0 0,0.51 6.17h-7.24c-0.62,-1.41 -0.74,-3.83 -0.74,-7.27v-0.47c0,-3.54 -1,-5.21 -6.67,-5.21ZM118.13,76.29h6.4c4.18,0 5.8,-1.46 5.8,-4.17s-1.88,-4.1 -5.62,-4.1L118.13,68.02Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M148.84,87.33l-2.43,7.48h-6.65l10.62,-32.36h8.48l11.07,32.36h-7.1l-2.57,-7.48ZM158.84,81.6c-2.22,-6.83 -3.64,-11.19 -4.39,-14.1h-0.05c-0.77,3.19 -2.33,8.26 -4.11,14.1Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M172.44,62.45h6.68v14.44c2.06,-2.49 8.49,-9.79 12.44,-14.44h8.09l-12.56,13.45 13,18.91L192.03,94.81l-9.59,-14.38 -3.29,3.27v11.11h-6.68Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M205.76,66.67L205.76,68.57h-0.79v-4.39h2.15c1.12,0 1.72,0.5 1.72,1.18a1.06,1.06 0,0 1,-0.89 1,1 1,0 0,1 0.79,1.11v0.2a2.79,2.79 0,0 0,0.07 0.86L208.03,68.53a2.67,2.67 0,0 1,-0.09 -0.95L207.94,67.57c0,-0.6 -0.21,-0.86 -1.15,-0.86ZM205.76,66.13h1.13c0.79,0 1.13,-0.24 1.13,-0.71s-0.36,-0.7 -1.06,-0.7h-1.2Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M206.65,71.06a4.7,4.7 0,1 1,4.71 -4.7A4.71,4.71 0,0 1,206.65 71.06ZM206.65,62.32a4,4 0,1 0,4 4A4,4 0,0 0,206.65 62.32Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M114.76,108.76v-7.92h1v7.92Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M121.96,106c0,1.6 -0.9,2.89 -2.54,2.89s-2.48,-1.22 -2.48,-2.87 0.92,-2.89 2.54,-2.89S121.96,104.28 121.96,106ZM117.96,106c0,1.18 0.56,2 1.47,2s1.44,-0.79 1.44,-2 -0.52,-2 -1.46,-2S118.03,104.77 118.03,106.01Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M123.92,102.2h-2.32v-0.94h5.72v0.94L125.03,102.2v6.56h-1.08Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M137.15,105.64c0,-1.38 0,-2.8 0.05,-3.6h0c-0.32,1.28 -1.41,4.36 -2.21,6.72h-1c-0.61,-1.94 -1.74,-5.41 -2.06,-6.73h0c0.06,0.87 0.09,2.51 0.09,3.8v2.93h-1v-7.5h1.61c0.77,2.26 1.71,5.16 1.95,6.14h0c0.17,-0.77 1.29,-3.94 2.09,-6.14h1.56v7.5h-1.05Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M143.8,107.57a9.39,9.39 0,0 0,0.08 1.24h-1a2.46,2.46 0,0 1,-0.09 -0.68,1.88 1.88,0 0,1 -3.41,-0.88c0,-1.23 0.94,-1.8 2.52,-1.8h0.88v-0.44c0,-0.47 -0.15,-1 -1.09,-1s-1,0.42 -1.07,0.85h-1c0.07,-0.8 0.55,-1.66 2.1,-1.66 1.32,0 2,0.56 2,1.82ZM142.8,106.16L142.03,106.16c-1,0 -1.55,0.3 -1.55,1a0.9,0.9 0,0 0,1 0.94c1.21,0 1.37,-0.85 1.37,-1.8Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M149.85,100.84v6.38c0,0.51 0,1 0,1.54h-1c0,-0.15 0,-0.54 0,-0.74a1.63,1.63 0,0 1,-1.64 0.87c-1.36,0 -2.21,-1.14 -2.21,-2.82s0.91,-2.94 2.39,-2.94c0.91,0 1.33,0.37 1.46,0.67v-3ZM146.04,106.04c0,1.27 0.57,2 1.38,2 1.2,0 1.44,-0.94 1.44,-2.06s-0.22,-1.93 -1.37,-1.93C146.58,104.02 146.03,104.76 146.03,106.04Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M151.76,106.21c0,1 0.51,1.81 1.38,1.81a1.15,1.15 0,0 0,1.21 -0.8h1a2.19,2.19 0,0 1,-2.27 1.67c-1.69,0 -2.39,-1.39 -2.39,-2.82 0,-1.65 0.81,-2.94 2.44,-2.94a2.33,2.33 0,0 1,2.32 2.63,3.24 3.24,0 0,1 0,0.45ZM154.44,105.49c0,-0.85 -0.42,-1.54 -1.29,-1.54s-1.28,0.64 -1.36,1.54Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M164.03,105.31h-3.59v2.51h3.94l-0.13,0.94h-4.86v-7.5h4.81v0.94h-3.76v2.16L164.03,104.36Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M169.62,107.57a9.9,9.9 0,0 0,0.07 1.24h-1a2.93,2.93 0,0 1,-0.09 -0.68,1.88 1.88,0 0,1 -3.41,-0.88c0,-1.23 0.94,-1.8 2.52,-1.8h0.88v-0.44c0,-0.47 -0.15,-1 -1.08,-1s-1,0.42 -1.08,0.85h-1c0.07,-0.8 0.55,-1.66 2.1,-1.66 1.33,0 2.06,0.56 2.06,1.82ZM168.62,106.16h-0.81c-1,0 -1.55,0.3 -1.55,1a0.91,0.91 0,0 0,1 0.94c1.21,0 1.37,-0.85 1.37,-1.8Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M171.66,107.17a1.16,1.16 0,0 0,1.25 0.9c0.74,0 1,-0.32 1,-0.78s-0.24,-0.7 -1.2,-1c-1.57,-0.39 -1.88,-0.89 -1.88,-1.64s0.56,-1.57 2,-1.57a1.8,1.8 0,0 1,2 1.6h-1a1,1 0,0 0,-1.1 -0.8c-0.69,0 -0.9,0.35 -0.9,0.68s0.21,0.58 1.16,0.81c1.65,0.4 2,1 2,1.79s-0.73,1.68 -2.11,1.68a2,2 0,0 1,-2.23 -1.72Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M176.68,103.26c0.8,2.49 1.28,4 1.41,4.55h0c0.16,-0.61 0.52,-1.87 1.35,-4.55h1l-1.89,5.74c-0.53,1.6 -0.94,2 -2.07,2a4.38,4.38 0,0 1,-0.59 0v-0.9a2.9,2.9 0,0 0,0.43 0c0.65,0 0.91,-0.29 1.2,-1.15l-2,-5.7Z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M155.03,312.26v22.45a6.83,6.83 0,0 1,-6.83 6.83h-11.1a8,8 0,0 1,-5.42 -2.38,9.05 9.05,0 0,1 -2.56,-6.44L129.12,314.19l11.73,11.74v-3.69l-11.73,-11.74 -3,-3v25.17c0,3.22 1.82,8.05 4,8.9h-5.75a6.83,6.83 0,0 1,-6.83 -6.83L117.54,312.26a6.83,6.83 0,0 1,6.83 -6.83L136.03,305.43a8,8 0,0 1,5.42 2.38,9.05 9.05,0 0,1 2.56,6.44v18.53l-11.73,-11.74v3.69L144.03,336.47l3,3L147.03,314.33c0,-3.22 -1.82,-8.05 -4,-8.9h5.22A6.83,6.83 0,0 1,155.03 312.26Z"
|
||||
android:fillColor="#9e9d9e"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M77.04,477.29L107.91,477.29A3.38,3.38 0,0 1,111.29 480.67L111.29,499.14A3.38,3.38 0,0 1,107.91 502.52L77.04,502.52A3.38,3.38 0,0 1,73.66 499.14L73.66,480.67A3.38,3.38 0,0 1,77.04 477.29z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M74.34,413.28L111.72,413.28A3.69,3.69 0,0 1,115.41 416.97L115.41,434.82A3.69,3.69 0,0 1,111.72 438.51L74.34,438.51A3.69,3.69 0,0 1,70.65 434.82L70.65,416.97A3.69,3.69 0,0 1,74.34 413.28z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M107.91,502.57h0a7.12,7.12 0,0 1,-7.12 7.12L84.17,509.69A7.12,7.12 0,0 1,77.03 502.57h0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M74.34,413.28L74.34,407.87"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M111.72,413.28L111.72,407.87"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M74.34,438.51s4.84,14.19 4.84,38.78"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M110.33,438.51s-4.84,14.19 -4.84,38.78"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M153.54,477.29L184.41,477.29A3.38,3.38 0,0 1,187.79 480.67L187.79,499.14A3.38,3.38 0,0 1,184.41 502.52L153.54,502.52A3.38,3.38 0,0 1,150.16 499.14L150.16,480.67A3.38,3.38 0,0 1,153.54 477.29z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M150.84,413.28L188.22,413.28A3.69,3.69 0,0 1,191.91 416.97L191.91,434.82A3.69,3.69 0,0 1,188.22 438.51L150.84,438.51A3.69,3.69 0,0 1,147.15 434.82L147.15,416.97A3.69,3.69 0,0 1,150.84 413.28z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M184.41,502.57h0a7.12,7.12 0,0 1,-7.12 7.12L160.66,509.69a7.11,7.11 0,0 1,-7.11 -7.12h0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M150.84,413.28L150.84,407.87"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M188.22,413.28L188.22,407.87"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M150.84,438.51s4.83,14.19 4.83,38.78"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M186.82,438.51S182.03,452.7 182.03,477.29"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
</vector>
|
|
@ -316,4 +316,10 @@
|
|||
<string name="favorite">Favorite</string>
|
||||
<string name="favorite_add">Add \'%s\' as a favorite node?</string>
|
||||
<string name="favorite_remove">Remove \'%s\' as a favorite node?</string>
|
||||
<string name="power_metrics_log">Power Metrics Log</string>
|
||||
<string name="channel_1">Channel 1</string>
|
||||
<string name="channel_2">Channel 2</string>
|
||||
<string name="channel_3">Channel 3</string>
|
||||
<string name="current">Current</string>
|
||||
<string name="voltage">Voltage</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,34 +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
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
|
@ -18,22 +18,25 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class UIUnitTest {
|
||||
@Test
|
||||
fun initialsGood() {
|
||||
Assert.assertEquals("KH", getInitials("Kevin Hester"))
|
||||
Assert.assertEquals("KHLC", getInitials(" Kevin Hester Lesser Cat "))
|
||||
Assert.assertEquals("", getInitials(" "))
|
||||
Assert.assertEquals("gksv", getInitials("geeksville"))
|
||||
Assert.assertEquals("geek", getInitials("geek"))
|
||||
Assert.assertEquals("gks1", getInitials("geeks1"))
|
||||
assertEquals("KH", getInitials("Kevin Hester"))
|
||||
assertEquals("KHLC", getInitials(" Kevin Hester Lesser Cat "))
|
||||
assertEquals("", getInitials(" "))
|
||||
assertEquals("gksv", getInitials("geeksville"))
|
||||
assertEquals("geek", getInitials("geek"))
|
||||
assertEquals("gks1", getInitials("geeks1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreEmojisWhenCreatingInitials() {
|
||||
assertEquals("TG", getInitials("The \uD83D\uDC10 Goat"))
|
||||
assertEquals("TT", getInitials("The \uD83E\uDD14Thinker"))
|
||||
assertEquals("TCH", getInitials("\uD83D\uDC4F\uD83C\uDFFFThe Clapping Hands"))
|
||||
assertEquals("山羊", getInitials("山羊\uD83D\uDC10"))
|
||||
}
|
||||
}
|
||||
|
|
12
build.gradle
12
build.gradle
|
@ -3,9 +3,9 @@
|
|||
buildscript {
|
||||
ext {
|
||||
useCrashlytics = false
|
||||
kotlin_version = '2.0.21'
|
||||
hilt_version = '2.54'
|
||||
protobuf_version = '4.29.2'
|
||||
kotlin_version = '2.1.10'
|
||||
hilt_version = '2.55'
|
||||
protobuf_version = '4.29.3'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -13,7 +13,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.3'
|
||||
classpath 'com.android.tools.build:gradle:8.8.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
|
||||
|
@ -23,7 +23,7 @@ buildscript {
|
|||
// Firebase Crashlytics
|
||||
if (useCrashlytics) {
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3'
|
||||
}
|
||||
|
||||
// protobuf plugin - docs here https://github.com/google/protobuf-gradle-plugin
|
||||
|
@ -35,7 +35,7 @@ buildscript {
|
|||
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.jvm" version "$kotlin_version" apply false
|
||||
id "com.google.devtools.ksp" version "2.0.21-1.0.28" apply false
|
||||
id "com.google.devtools.ksp" version "2.1.10-1.0.30" apply false
|
||||
id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version" apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
"replacements:all",
|
||||
"workarounds:all"
|
||||
],
|
||||
"commitMessageTopic": "{{depName}}"
|
||||
"commitMessageTopic": "{{depName}}",
|
||||
"labels": ["dependencies"]
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue