mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate core UI and features to KMP, adopt Navigation 3 (#4750)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b1070321fe
commit
d076361c55
245 changed files with 3106 additions and 1748 deletions
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Preview(name = "Wind Dir -359°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionn359() {
|
||||
PreviewWindDirectionItem(-359f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 0°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection0() {
|
||||
PreviewWindDirectionItem(0f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 45°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection45() {
|
||||
PreviewWindDirectionItem(45f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 90°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection90() {
|
||||
PreviewWindDirectionItem(90f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 180°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection180() {
|
||||
PreviewWindDirectionItem(180f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 225°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection225() {
|
||||
PreviewWindDirectionItem(225f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 270°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection270() {
|
||||
PreviewWindDirectionItem(270f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 315°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection315() {
|
||||
PreviewWindDirectionItem(315f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir -45")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionN45() {
|
||||
PreviewWindDirectionItem(-45f)
|
||||
}
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionItem(windDirection: Float, windSpeed: String = "5 m/s") {
|
||||
val normalizedBearing = (windDirection + 180) % 360
|
||||
InfoCard(icon = Icons.Outlined.Navigation, text = "Wind", value = windSpeed, rotateIcon = normalizedBearing)
|
||||
}
|
||||
|
|
@ -154,8 +154,8 @@ fun NodeListScreen(
|
|||
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onImport = { uri ->
|
||||
viewModel.handleScannedUri(uri.toString()) {
|
||||
onImport = { uriString ->
|
||||
viewModel.handleScannedUri(uriString) {
|
||||
scope.launch { context.showToast(Res.string.channel_invalid) }
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ 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.tooling.preview.PreviewLightDark
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun ChannelInfo(
|
||||
|
|
@ -39,9 +37,3 @@ fun ChannelInfo(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ChannelInfoPreview() {
|
||||
AppTheme { ChannelInfo(channel = 2) }
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ import androidx.compose.ui.text.TextStyle
|
|||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -70,7 +69,6 @@ import org.meshtastic.core.resources.compass_uncertainty_unknown
|
|||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.exchange_position
|
||||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.compass.CompassUiState
|
||||
import org.meshtastic.feature.node.compass.CompassWarning
|
||||
import kotlin.math.cos
|
||||
|
|
@ -422,28 +420,3 @@ private fun Float.normalizeDegrees(): Float {
|
|||
val normalized = this % 360f
|
||||
return if (normalized < 0f) normalized + 360f else normalized
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
@Suppress("MagicNumber")
|
||||
private fun CompassSheetPreview() {
|
||||
AppTheme {
|
||||
CompassSheetContent(
|
||||
uiState =
|
||||
CompassUiState(
|
||||
targetName = "Sample Node",
|
||||
heading = 45f,
|
||||
bearing = 90f,
|
||||
distanceText = "1.2 km",
|
||||
bearingText = "90°",
|
||||
lastUpdateText = "0h 3m 10s ago",
|
||||
errorRadiusText = "150 m",
|
||||
angularErrorDeg = 12f,
|
||||
isAligned = false,
|
||||
),
|
||||
onRequestLocationPermission = {},
|
||||
onOpenLocationSettings = {},
|
||||
onRequestPosition = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ package org.meshtastic.feature.node.component
|
|||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
|
|
@ -30,13 +29,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
internal const val COOL_DOWN_TIME_MS = 30000L
|
||||
internal const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes
|
||||
|
|
@ -134,13 +129,3 @@ private fun CooldownBaseButton(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun CooldownOutlinedIconButtonPreview() {
|
||||
AppTheme {
|
||||
CooldownOutlinedIconButton(onClick = {}, cooldownTimestamp = nowMillis - 15000L) {
|
||||
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,9 +37,9 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
|
|
@ -130,7 +130,7 @@ private fun DeviceHardwareImage(deviceHardware: DeviceHardware, modifier: Modifi
|
|||
val hwImg = deviceHardware.images?.getOrNull(1) ?: deviceHardware.images?.getOrNull(0) ?: "unknown.svg"
|
||||
val imageUrl = "https://flasher.meshtastic.org/img/devices/$hwImg"
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).build(),
|
||||
model = ImageRequest.Builder(LocalPlatformContext.current).data(imageUrl).build(),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder =
|
||||
|
|
@ -22,11 +22,9 @@ 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.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.distance
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun DistanceInfo(
|
||||
|
|
@ -43,9 +41,3 @@ fun DistanceInfo(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DistanceInfoPreview() {
|
||||
AppTheme { DistanceInfo(distance = "423 mi.") }
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ 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.tooling.preview.Preview
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
|
|
@ -48,9 +47,3 @@ fun ElevationInfo(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ElevationInfoPreview() {
|
||||
MaterialTheme { ElevationInfo(altitude = 100, system = Config.DisplayConfig.DisplayUnits.METRIC, suffix = "ASL") }
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
|
|
@ -80,67 +81,115 @@ internal fun EnvironmentMetrics(
|
|||
if (!temp.isNaN()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.temperature,
|
||||
temp.toTempString(isFahrenheit),
|
||||
Icons.Rounded.Thermostat,
|
||||
label = Res.string.temperature,
|
||||
value = temp.toTempString(isFahrenheit),
|
||||
icon = Icons.Rounded.Thermostat,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
relative_humidity?.let { rh ->
|
||||
add(VectorMetricInfo(Res.string.humidity, "%.0f%%".format(rh), Icons.Rounded.WaterDrop))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.humidity,
|
||||
"${NumberFormatter.format(rh, 0)}%",
|
||||
Icons.Rounded.WaterDrop,
|
||||
),
|
||||
)
|
||||
}
|
||||
barometric_pressure?.let { bp ->
|
||||
add(VectorMetricInfo(Res.string.pressure, "%.0f hPa".format(bp), Icons.Rounded.Speed))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.pressure,
|
||||
"${NumberFormatter.format(bp, 0)} hPa",
|
||||
Icons.Rounded.Speed,
|
||||
),
|
||||
)
|
||||
}
|
||||
gas_resistance?.let { gr ->
|
||||
add(VectorMetricInfo(Res.string.gas_resistance, "%.0f MΩ".format(gr), Icons.Rounded.BlurOn))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.gas_resistance,
|
||||
value = "${NumberFormatter.format(gr, 0)} MΩ",
|
||||
icon = Icons.Rounded.BlurOn,
|
||||
),
|
||||
)
|
||||
}
|
||||
voltage?.let { v ->
|
||||
add(VectorMetricInfo(Res.string.voltage, "%.2fV".format(v), Icons.Rounded.Bolt))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.voltage,
|
||||
value = "${NumberFormatter.format(v, 2)}V",
|
||||
icon = Icons.Rounded.Bolt,
|
||||
),
|
||||
)
|
||||
}
|
||||
current?.let { c ->
|
||||
add(VectorMetricInfo(Res.string.current, "%.1fmA".format(c), Icons.Rounded.Power))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.current,
|
||||
value = "${NumberFormatter.format(c, 1)}mA",
|
||||
icon = Icons.Rounded.Power,
|
||||
),
|
||||
)
|
||||
}
|
||||
iaq?.let { i -> add(VectorMetricInfo(Res.string.iaq, i.toString(), Icons.Rounded.Air)) }
|
||||
distance?.let { d ->
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.distance,
|
||||
d.toSmallDistanceString(displayUnits),
|
||||
Icons.Rounded.Height,
|
||||
label = Res.string.distance,
|
||||
value = d.toSmallDistanceString(displayUnits),
|
||||
icon = Icons.Rounded.Height,
|
||||
),
|
||||
)
|
||||
}
|
||||
lux?.let { l ->
|
||||
add(VectorMetricInfo(Res.string.lux, "%.0f lx".format(l), Icons.Rounded.LightMode))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.lux,
|
||||
value = "${NumberFormatter.format(l, 0)} lx",
|
||||
icon = Icons.Rounded.LightMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
uv_lux?.let { uvl ->
|
||||
add(VectorMetricInfo(Res.string.uv_lux, "%.0f lx".format(uvl), Icons.Rounded.LightMode))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.uv_lux,
|
||||
value = "${NumberFormatter.format(uvl, 0)} lx",
|
||||
icon = Icons.Rounded.LightMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
wind_speed?.let { ws ->
|
||||
@Suppress("MagicNumber")
|
||||
val normalizedBearing = ((wind_direction ?: 0) + 180) % 360
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.wind,
|
||||
ws.toFloat().toSpeedString(displayUnits),
|
||||
Icons.Outlined.Navigation,
|
||||
normalizedBearing.toFloat(),
|
||||
label = Res.string.wind,
|
||||
value = ws.toFloat().toSpeedString(displayUnits),
|
||||
icon = Icons.Outlined.Navigation,
|
||||
rotateIcon = normalizedBearing.toFloat(),
|
||||
),
|
||||
)
|
||||
}
|
||||
weight?.let { w ->
|
||||
add(VectorMetricInfo(Res.string.weight, "%.2f kg".format(w), Icons.Rounded.Scale))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
label = Res.string.weight,
|
||||
value = "${NumberFormatter.format(w, 2)} kg",
|
||||
icon = Icons.Rounded.Scale,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (temperature != null && relative_humidity != null) {
|
||||
val dewPoint = UnitConversions.calculateDewPoint(temperature!!, relative_humidity!!)
|
||||
if (!dewPoint.isNaN()) {
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
Res.string.dew_point,
|
||||
dewPoint.toTempString(isFahrenheit),
|
||||
Res.drawable.ic_dew_point,
|
||||
label = Res.string.dew_point,
|
||||
value = dewPoint.toTempString(isFahrenheit),
|
||||
icon = Res.drawable.ic_dew_point,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -149,27 +198,21 @@ internal fun EnvironmentMetrics(
|
|||
if (!st.isNaN()) {
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
Res.string.soil_temperature,
|
||||
st.toTempString(isFahrenheit),
|
||||
Res.drawable.ic_soil_temperature,
|
||||
label = Res.string.soil_temperature,
|
||||
value = st.toTempString(isFahrenheit),
|
||||
icon = Res.drawable.ic_soil_temperature,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
soil_moisture?.let { sm ->
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
Res.string.soil_moisture,
|
||||
"%d%%".format(sm),
|
||||
Res.drawable.ic_soil_moisture,
|
||||
),
|
||||
)
|
||||
add(DrawableMetricInfo(Res.string.soil_moisture, "$sm%", Res.drawable.ic_soil_moisture))
|
||||
}
|
||||
radiation?.let { r ->
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
label = Res.string.radiation,
|
||||
value = "%.1f µR/h".format(r),
|
||||
value = "${NumberFormatter.format(r, 1)} µR/h",
|
||||
icon = Res.drawable.ic_radioactive,
|
||||
),
|
||||
)
|
||||
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -35,26 +33,19 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.download
|
||||
import org.meshtastic.core.resources.error_no_app_to_handle_link
|
||||
import org.meshtastic.core.resources.view_release
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.core.ui.util.rememberOpenUrl
|
||||
|
||||
@Composable
|
||||
fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modifier = Modifier) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val openUrl = rememberOpenUrl()
|
||||
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(rememberScrollState()).padding(16.dp).fillMaxWidth(),
|
||||
|
|
@ -64,34 +55,12 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
|
|||
Text(text = "Version: ${firmwareRelease.id}", style = MaterialTheme.typography.bodyMedium)
|
||||
Markdown(modifier = Modifier.padding(8.dp), content = firmwareRelease.releaseNotes)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, firmwareRelease.pageUrl.toUri())
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
scope.launch { context.showToast(Res.string.error_no_app_to_handle_link) }
|
||||
Logger.e(e) { "Failed to handle release page URL" }
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Button(onClick = { openUrl(firmwareRelease.pageUrl) }, modifier = Modifier.weight(1f)) {
|
||||
Icon(imageVector = Icons.Rounded.Link, contentDescription = stringResource(Res.string.view_release))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(Res.string.view_release))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, firmwareRelease.zipUrl.toUri())
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
scope.launch { context.showToast(Res.string.error_no_app_to_handle_link) }
|
||||
Logger.e(e) { "Failed to handle release zip URL" }
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Button(onClick = { openUrl(firmwareRelease.zipUrl) }, modifier = Modifier.weight(1f)) {
|
||||
Icon(imageVector = Icons.Rounded.Download, contentDescription = stringResource(Res.string.download))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(Res.string.download))
|
||||
|
|
@ -22,11 +22,9 @@ 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.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.hops_away
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
|
||||
|
|
@ -39,9 +37,3 @@ fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = Mat
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun HopsInfoPreview() {
|
||||
AppTheme { HopsInfo(hops = 3) }
|
||||
}
|
||||
|
|
@ -28,10 +28,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
||||
private const val SIZE_ICON = 20
|
||||
|
||||
|
|
@ -62,11 +59,3 @@ fun IconInfo(
|
|||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun IconInfoPreview() {
|
||||
MaterialTheme {
|
||||
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", content = { Text(text = "100") })
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -38,7 +37,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.semantics.Role
|
||||
|
|
@ -51,6 +49,7 @@ import org.jetbrains.compose.resources.painterResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -74,9 +73,7 @@ fun InfoCard(
|
|||
.defaultMinSize(minHeight = 48.dp)
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
coroutineScope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(text, value))) }
|
||||
},
|
||||
onLongClick = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, text)) } },
|
||||
onLongClickLabel = copyLabel,
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
|
|
@ -20,14 +20,11 @@ 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.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ic_antenna
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
||||
@Composable
|
||||
|
|
@ -46,9 +43,3 @@ fun LastHeardInfo(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun LastHeardInfoPreview() {
|
||||
AppTheme { LastHeardInfo(lastHeard = nowSeconds.toInt() - 8600) }
|
||||
}
|
||||
|
|
@ -16,9 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
|
|
@ -26,18 +23,13 @@ import androidx.compose.material.icons.rounded.LocationOn
|
|||
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.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.GPSFormat
|
||||
|
|
@ -50,10 +42,10 @@ import org.meshtastic.core.resources.elevation_suffix
|
|||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
import org.meshtastic.core.ui.component.icon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.core.ui.util.rememberOpenMap
|
||||
import org.meshtastic.proto.Config
|
||||
import java.net.URLEncoder
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -61,9 +53,9 @@ fun LinkedCoordinatesItem(
|
|||
node: Node,
|
||||
displayUnits: Config.DisplayConfig.DisplayUnits = Config.DisplayConfig.DisplayUnits.METRIC,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val openMap = rememberOpenMap()
|
||||
|
||||
val ago = formatAgo(node.position.time)
|
||||
val coordinates = GPSFormat.toDec(node.latitude, node.longitude)
|
||||
|
|
@ -82,9 +74,7 @@ fun LinkedCoordinatesItem(
|
|||
customActions =
|
||||
listOf(
|
||||
CustomAccessibilityAction(copyLabel) {
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", coordinates)))
|
||||
}
|
||||
coroutineScope.launch { clipboard.setClipEntry(createClipEntry(coordinates, copyLabel)) }
|
||||
true
|
||||
},
|
||||
)
|
||||
|
|
@ -93,27 +83,7 @@ fun LinkedCoordinatesItem(
|
|||
leadingIcon = Icons.Rounded.LocationOn,
|
||||
supportingText = "$ago • $coordinates$elevationText",
|
||||
trailingContent = Icons.AutoMirrored.Rounded.KeyboardArrowRight.icon(),
|
||||
onClick = {
|
||||
val label = URLEncoder.encode(node.user.long_name ?: "", "utf-8")
|
||||
val uri = "geo:0,0?q=${node.latitude},${node.longitude}&z=17&label=$label".toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
|
||||
try {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Logger.d { "Failed to open geo intent: $ex" }
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
coroutineScope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", coordinates))) }
|
||||
},
|
||||
onClick = { openMap(node.latitude, node.longitude, node.user.long_name ?: "") },
|
||||
onLongClick = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(coordinates, copyLabel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun LinkedCoordinatesPreview() {
|
||||
AppTheme { LinkedCoordinatesItem(Node(0)) }
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -40,7 +39,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.semantics.Role
|
||||
|
|
@ -55,6 +53,7 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
|
||||
@Composable
|
||||
internal fun SectionCard(
|
||||
|
|
@ -102,9 +101,7 @@ internal fun InfoItem(
|
|||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 48.dp) // Minimum touch target height
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
coroutineScope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(label, value))) }
|
||||
},
|
||||
onLongClick = { coroutineScope.launch { clipboard.setClipEntry(createClipEntry(value, label)) } },
|
||||
onLongClickLabel = copyLabel, // Clear intent for accessibility
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
import android.util.Base64
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -42,7 +40,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.semantics.Role
|
||||
|
|
@ -51,10 +48,10 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.Base64Factory
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
|
|
@ -78,7 +75,6 @@ import org.meshtastic.core.resources.supported
|
|||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.user_id
|
||||
import org.meshtastic.core.resources.via_mqtt
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.ArrowCircleUp
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
import org.meshtastic.core.ui.icon.Cloud
|
||||
|
|
@ -90,7 +86,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
|
|||
import org.meshtastic.core.ui.icon.Person
|
||||
import org.meshtastic.core.ui.icon.Verified
|
||||
import org.meshtastic.core.ui.icon.role
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.createClipEntry
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
||||
@Composable
|
||||
|
|
@ -321,7 +317,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
if (isMismatch) {
|
||||
stringResource(Res.string.error)
|
||||
} else {
|
||||
Base64.encodeToString(publicKeyBytes, Base64.DEFAULT).trim()
|
||||
Base64Factory.encode(publicKeyBytes).trim()
|
||||
}
|
||||
val label = stringResource(Res.string.public_key)
|
||||
val copyLabel = stringResource(Res.string.copy)
|
||||
|
|
@ -333,9 +329,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
.combinedClickable(
|
||||
onLongClick = {
|
||||
if (!isMismatch) {
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(label, publicKeyBase64)))
|
||||
}
|
||||
coroutineScope.launch { clipboard.setClipEntry(createClipEntry(publicKeyBase64, label)) }
|
||||
}
|
||||
},
|
||||
onLongClickLabel = copyLabel,
|
||||
|
|
@ -373,12 +367,3 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun NodeDetailsSectionPreview() {
|
||||
AppTheme {
|
||||
val node = NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.")
|
||||
NodeDetailsSection(node = node)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,8 +56,6 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
|
|
@ -73,7 +71,6 @@ import org.meshtastic.core.resources.node_filter_show_ignored
|
|||
import org.meshtastic.core.resources.node_filter_title
|
||||
import org.meshtastic.core.resources.node_sort_button
|
||||
import org.meshtastic.core.resources.node_sort_title
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
|
|
@ -139,6 +136,20 @@ fun NodeFilterTextField(
|
|||
}
|
||||
}
|
||||
|
||||
data class NodeFilterToggles(
|
||||
val includeUnknown: Boolean,
|
||||
val onToggleIncludeUnknown: () -> Unit,
|
||||
val excludeInfrastructure: Boolean,
|
||||
val onToggleExcludeInfrastructure: () -> Unit,
|
||||
val onlyOnline: Boolean,
|
||||
val onToggleOnlyOnline: () -> Unit,
|
||||
val onlyDirect: Boolean,
|
||||
val onToggleOnlyDirect: () -> Unit,
|
||||
val showIgnored: Boolean,
|
||||
val onToggleShowIgnored: () -> Unit,
|
||||
val ignoredNodeCount: Int,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
|
@ -295,42 +306,3 @@ private fun DropdownMenuCheck(
|
|||
text = { Text(text = text) },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Preview(name = "Large Font", fontScale = 2f)
|
||||
@Composable
|
||||
private fun NodeFilterTextFieldPreview() {
|
||||
AppTheme {
|
||||
NodeFilterTextField(
|
||||
filterText = "Filter text",
|
||||
onTextChange = {},
|
||||
currentSortOption = NodeSortOption.LAST_HEARD,
|
||||
onSortSelect = {},
|
||||
includeUnknown = false,
|
||||
onToggleIncludeUnknown = {},
|
||||
excludeInfrastructure = false,
|
||||
onToggleExcludeInfrastructure = {},
|
||||
onlyOnline = false,
|
||||
onToggleOnlyOnline = {},
|
||||
onlyDirect = false,
|
||||
onToggleOnlyDirect = {},
|
||||
showIgnored = false,
|
||||
onToggleShowIgnored = {},
|
||||
ignoredNodeCount = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class NodeFilterToggles(
|
||||
val includeUnknown: Boolean,
|
||||
val onToggleIncludeUnknown: () -> Unit,
|
||||
val excludeInfrastructure: Boolean,
|
||||
val onToggleExcludeInfrastructure: () -> Unit,
|
||||
val onlyOnline: Boolean,
|
||||
val onToggleOnlyOnline: () -> Unit,
|
||||
val onlyDirect: Boolean,
|
||||
val onToggleOnlyDirect: () -> Unit,
|
||||
val showIgnored: Boolean,
|
||||
val onToggleShowIgnored: () -> Unit,
|
||||
val ignoredNodeCount: Int,
|
||||
)
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -47,8 +46,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
|
|
@ -89,11 +86,9 @@ import org.meshtastic.core.ui.component.SoilTemperatureInfo
|
|||
import org.meshtastic.core.ui.component.TemperatureInfo
|
||||
import org.meshtastic.core.ui.component.TransportIcon
|
||||
import org.meshtastic.core.ui.component.determineSignalQuality
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.AirUtilization
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
private const val ACTIVE_ALPHA = 0.5f
|
||||
|
|
@ -462,49 +457,3 @@ private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
|
|||
NodeIdInfo(id = thatNode.user.id.ifEmpty { "???" }, contentColor = contentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoSimplePreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode = NodePreviewParameterProvider().values.last().copy(lastHeard = 0)
|
||||
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoStatusPreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode =
|
||||
NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.")
|
||||
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoSignalPreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode = NodePreviewParameterProvider().values.last().copy(hopsAway = 0, snr = 5.5f, rssi = -100)
|
||||
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatNode: Node) {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
NodeItem(
|
||||
thisNode = thisNode,
|
||||
thatNode = thatNode,
|
||||
distanceUnits = 1,
|
||||
tempInFahrenheit = true,
|
||||
connectionState = ConnectionState.Connected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
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 org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -194,15 +193,3 @@ private fun StatusBadge(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StatusIconsPreview() {
|
||||
NodeStatusIcons(
|
||||
isThisNode = true,
|
||||
isUnmessageable = true,
|
||||
isFavorite = true,
|
||||
isMuted = true,
|
||||
connectionState = ConnectionState.Connected,
|
||||
)
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_1
|
||||
|
|
@ -41,22 +42,59 @@ import org.meshtastic.feature.node.model.VectorMetricInfo
|
|||
* intended.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
internal fun PowerMetrics(node: Node) {
|
||||
val metrics =
|
||||
remember(node.powerMetrics) {
|
||||
buildList {
|
||||
with(node.powerMetrics) {
|
||||
if ((ch1_voltage ?: 0f) != 0f) {
|
||||
add(VectorMetricInfo(Res.string.channel_1, "%.2fV".format(ch1_voltage), Icons.Rounded.Bolt))
|
||||
add(VectorMetricInfo(Res.string.channel_1, "%.1fmA".format(ch1_current), Icons.Rounded.Power))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_1,
|
||||
"${NumberFormatter.format(ch1_voltage ?: 0f, 2)}V",
|
||||
Icons.Rounded.Bolt,
|
||||
),
|
||||
)
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_1,
|
||||
"${NumberFormatter.format(ch1_current ?: 0f, 1)}mA",
|
||||
Icons.Rounded.Power,
|
||||
),
|
||||
)
|
||||
}
|
||||
if ((ch2_voltage ?: 0f) != 0f) {
|
||||
add(VectorMetricInfo(Res.string.channel_2, "%.2fV".format(ch2_voltage), Icons.Rounded.Bolt))
|
||||
add(VectorMetricInfo(Res.string.channel_2, "%.1fmA".format(ch2_current), Icons.Rounded.Power))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_2,
|
||||
"${NumberFormatter.format(ch2_voltage ?: 0f, 2)}V",
|
||||
Icons.Rounded.Bolt,
|
||||
),
|
||||
)
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_2,
|
||||
"${NumberFormatter.format(ch2_current ?: 0f, 1)}mA",
|
||||
Icons.Rounded.Power,
|
||||
),
|
||||
)
|
||||
}
|
||||
if ((ch3_voltage ?: 0f) != 0f) {
|
||||
add(VectorMetricInfo(Res.string.channel_3, "%.2fV".format(ch3_voltage), Icons.Rounded.Bolt))
|
||||
add(VectorMetricInfo(Res.string.channel_3, "%.1fmA".format(ch3_current), Icons.Rounded.Power))
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_3,
|
||||
"${NumberFormatter.format(ch3_voltage ?: 0f, 2)}V",
|
||||
Icons.Rounded.Bolt,
|
||||
),
|
||||
)
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
Res.string.channel_3,
|
||||
"${NumberFormatter.format(ch3_current ?: 0f, 1)}mA",
|
||||
Icons.Rounded.Power,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,11 +22,9 @@ 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.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun SatelliteCountInfo(
|
||||
|
|
@ -43,9 +41,3 @@ fun SatelliteCountInfo(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun SatelliteCountInfoPreview() {
|
||||
AppTheme { SatelliteCountInfo(satCount = 5) }
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ package org.meshtastic.feature.node.detail
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
|
@ -34,7 +33,6 @@ import kotlinx.coroutines.launch
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
|
|
@ -68,9 +66,7 @@ open class NodeDetailViewModel(
|
|||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val nodeIdFromRoute: Int? =
|
||||
runCatching { savedStateHandle.toRoute<NodesRoutes.NodeDetail>().destNum }
|
||||
.getOrElse { runCatching { savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum }.getOrNull() }
|
||||
private val nodeIdFromRoute: Int? = savedStateHandle.get<Int>("destNum")
|
||||
|
||||
private val manualNodeId = MutableStateFlow<Int?>(null)
|
||||
private val activeNodeId =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue