Componentize traceroute button (#2965)

This commit is contained in:
Phil Oliver 2025-09-04 20:34:28 -04:00 committed by GitHub
parent 79eb62edac
commit 2583b3fcf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 66 deletions

View file

@ -20,8 +20,6 @@ package com.geeksville.mesh.ui.node
import android.content.Intent
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -93,11 +91,9 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -150,6 +146,7 @@ import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.node.components.TracerouteButton
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
@ -631,8 +628,7 @@ private fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction:
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
)
TracerouteActionButton(
title = stringResource(id = R.string.traceroute),
TracerouteButton(
lastTracerouteTime = lastTracerouteTime,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.TraceRoute(node))) },
)
@ -1035,59 +1031,6 @@ private fun PowerMetrics(node: Node) {
}
}
private const val COOL_DOWN_TIME_MS = 30000L
@Composable
fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () -> Unit) {
val progress = remember { Animatable(0f) }
var isCoolingDown by remember { mutableStateOf(false) }
LaunchedEffect(lastTracerouteTime) {
val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0)
isCoolingDown = timeSinceLast < COOL_DOWN_TIME_MS
if (isCoolingDown) {
val remainingTime = COOL_DOWN_TIME_MS - timeSinceLast
progress.snapTo(remainingTime / COOL_DOWN_TIME_MS.toFloat())
progress.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
)
isCoolingDown = false
}
}
Button(
onClick = {
if (!isCoolingDown) {
onClick()
}
},
enabled = !isCoolingDown,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (isCoolingDown) {
CircularProgressIndicator(
progress = { progress.value },
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
trackColor = ProgressIndicatorDefaults.circularDeterminateTrackColor,
strokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap,
)
} else {
Icon(
imageVector = Icons.Default.Route,
contentDescription = stringResource(R.string.traceroute),
modifier = Modifier.size(24.dp),
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
}
}
}
@Composable
fun NodeActionButton(
modifier: Modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp),

View file

@ -0,0 +1,100 @@
/*
* 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.node.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.settings.components.SettingsItem
private const val COOL_DOWN_TIME_MS = 30000L
@Composable
fun TracerouteButton(
text: String = stringResource(id = R.string.traceroute),
lastTracerouteTime: Long?,
onClick: () -> Unit,
) {
val progress = remember { Animatable(0f) }
LaunchedEffect(lastTracerouteTime) {
val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0)
if (timeSinceLast < COOL_DOWN_TIME_MS) {
val remainingTime = COOL_DOWN_TIME_MS - timeSinceLast
progress.snapTo(remainingTime / COOL_DOWN_TIME_MS.toFloat())
progress.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
)
}
}
TracerouteButton(text = text, progress = progress.value, onClick = onClick)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit) {
val isCoolingDown = progress > 0f
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
SettingsItem(
text = text,
enabled = !isCoolingDown,
leadingIcon = Icons.Default.Route,
trailingContent = {
if (isCoolingDown) {
CircularWavyProgressIndicator(
progress = { progress },
modifier = Modifier.size(24.dp),
stroke = stroke,
trackStroke = stroke,
wavelength = 8.dp,
)
}
},
onClick = {
if (!isCoolingDown) {
onClick()
}
},
)
}
@Preview(showBackground = true)
@Composable
private fun TracerouteButtonPreview() {
AppTheme { TracerouteButton(text = "Traceroute", progress = .6f, onClick = {}) }
}

View file

@ -26,7 +26,6 @@ import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
@ -51,18 +50,33 @@ fun SettingsItem(
trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
trailingIconTint: Color = LocalContentColor.current,
onClick: () -> Unit,
) {
SettingsItem(
text = text,
enabled = enabled,
leadingIcon = leadingIcon,
leadingIconTint = leadingIconTint,
trailingContent = { trailingIcon.Icon(trailingIconTint) },
onClick = onClick,
)
}
/** A clickable settings button item. */
@Composable
fun SettingsItem(
text: String,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingContent: @Composable (() -> Unit),
onClick: () -> Unit,
) {
ClickableWrapper(enabled = enabled, onClick = onClick) {
Content(
leading = { leadingIcon.Icon(leadingIconTint) },
text = text,
trailing = { trailingIcon.Icon(trailingIconTint) },
)
Content(leading = { leadingIcon.Icon(leadingIconTint) }, text = text, trailing = trailingContent)
}
}
/** A toggleable settings switch item. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingsItemSwitch(
checked: Boolean,