diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt index b6c27c3be4..357cc8c650 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/CooldownOutlinedIconButton.kt @@ -16,8 +16,6 @@ */ package org.meshtastic.feature.node.component -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -26,11 +24,15 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.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 @@ -43,85 +45,69 @@ internal const val REQUEST_NEIGHBORS_COOL_DOWN_TIME_MS = 180000L // 3 minutes fun CooldownIconButton( onClick: () -> Unit, cooldownTimestamp: Long?, + modifier: Modifier = Modifier, cooldownDuration: Long = COOL_DOWN_TIME_MS, content: @Composable () -> Unit, -) { - val progress = remember { Animatable(0f) } - - LaunchedEffect(cooldownTimestamp) { - if (cooldownTimestamp == null) { - progress.snapTo(0f) - return@LaunchedEffect - } - val timeSinceLast = nowMillis - cooldownTimestamp - if (timeSinceLast < cooldownDuration) { - val remainingTime = cooldownDuration - timeSinceLast - progress.snapTo(remainingTime / cooldownDuration.toFloat()) - progress.animateTo( - targetValue = 0f, - animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }), - ) - } else { - progress.snapTo(0f) - } - } - - val isCoolingDown = progress.value > 0f - - IconButton( - onClick = { if (!isCoolingDown) onClick() }, - enabled = !isCoolingDown, - colors = IconButtonDefaults.iconButtonColors(), - ) { - if (isCoolingDown) { - CircularProgressIndicator( - progress = { progress.value }, - modifier = Modifier.size(24.dp), - strokeCap = StrokeCap.Round, - ) - } else { - content() - } - } -} +) = CooldownBaseButton( + onClick = onClick, + cooldownTimestamp = cooldownTimestamp, + cooldownDuration = cooldownDuration, + modifier = modifier, + outlined = false, + content = content, +) @Composable fun CooldownOutlinedIconButton( onClick: () -> Unit, cooldownTimestamp: Long?, + modifier: Modifier = Modifier, cooldownDuration: Long = COOL_DOWN_TIME_MS, content: @Composable () -> Unit, ) { - val progress = remember { Animatable(0f) } + CooldownBaseButton( + onClick = onClick, + cooldownTimestamp = cooldownTimestamp, + cooldownDuration = cooldownDuration, + modifier = modifier, + outlined = true, + content = content, + ) +} - LaunchedEffect(cooldownTimestamp) { - if (cooldownTimestamp == null) { - progress.snapTo(0f) - return@LaunchedEffect - } - val timeSinceLast = nowMillis - cooldownTimestamp - if (timeSinceLast < cooldownDuration) { - val remainingTime = cooldownDuration - timeSinceLast - progress.snapTo(remainingTime / cooldownDuration.toFloat()) - progress.animateTo( - targetValue = 0f, - animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }), - ) - } else { - progress.snapTo(0f) +private const val TICK = 100L + +@Composable +private fun CooldownBaseButton( + onClick: () -> Unit, + cooldownTimestamp: Long?, + cooldownDuration: Long, + modifier: Modifier = Modifier, + outlined: Boolean = false, + content: @Composable () -> Unit, +) { + var progress by remember { mutableStateOf(0f) } + var isCoolingDown by remember { mutableStateOf(false) } + + LaunchedEffect(cooldownTimestamp, cooldownDuration) { + val endTime = (cooldownTimestamp ?: 0L) + cooldownDuration + isCoolingDown = nowMillis < endTime + + while (isCoolingDown) { + val remainingTime = endTime - nowMillis + if (remainingTime <= 0) break + progress = (remainingTime.toFloat() / cooldownDuration).coerceIn(0f, 1f) + delay(TICK) + isCoolingDown = nowMillis < endTime } + progress = 0f + isCoolingDown = false } - val isCoolingDown = progress.value > 0f - - OutlinedIconButton( - onClick = { if (!isCoolingDown) onClick() }, - enabled = !isCoolingDown, - colors = IconButtonDefaults.outlinedIconButtonColors(), - ) { + val buttonContent: @Composable () -> Unit = { if (isCoolingDown) { CircularProgressIndicator( - progress = { progress.value }, + progress = { progress }, modifier = Modifier.size(24.dp), strokeCap = StrokeCap.Round, ) @@ -129,6 +115,24 @@ fun CooldownOutlinedIconButton( content() } } + + if (outlined) { + OutlinedIconButton( + onClick = onClick, + enabled = !isCoolingDown, + colors = IconButtonDefaults.outlinedIconButtonColors(), + modifier = modifier, + content = buttonContent, + ) + } else { + IconButton( + onClick = onClick, + enabled = !isCoolingDown, + colors = IconButtonDefaults.iconButtonColors(), + modifier = modifier, + content = buttonContent, + ) + } } @Preview(showBackground = true) diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt index 63f3ebc45a..1ca64fae91 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeRequestActions.kt @@ -58,8 +58,8 @@ class NodeRequestActions @Inject constructor(private val radioController: RadioC private val _effects = MutableSharedFlow() val effects: SharedFlow = _effects.asSharedFlow() - private val _lastTracerouteTimes = MutableStateFlow>(emptyMap()) - val lastTracerouteTimes: StateFlow> = _lastTracerouteTimes.asStateFlow() + private val _lastTracerouteTime = MutableStateFlow(null) + val lastTracerouteTime: StateFlow = _lastTracerouteTime.asStateFlow() private val _lastRequestNeighborTimes = MutableStateFlow>(emptyMap()) val lastRequestNeighborTimes: StateFlow> = _lastRequestNeighborTimes.asStateFlow() @@ -135,7 +135,7 @@ class NodeRequestActions @Inject constructor(private val radioController: RadioC Logger.i { "Requesting traceroute for '$destNum'" } val packetId = radioController.getPacketId() radioController.requestTraceroute(packetId, destNum) - _lastTracerouteTimes.update { it + (destNum to nowMillis) } + _lastTracerouteTime.value = nowMillis _effects.emit( NodeRequestEffect.ShowFeedback( UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName), diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt index f5955c9f36..53b753da5c 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt @@ -123,7 +123,7 @@ constructor( .onStart { emit(null) }, firmwareReleaseRepository.stableRelease, firmwareReleaseRepository.alphaRelease, - nodeRequestActions.lastTracerouteTimes.map { it[nodeId] }, + nodeRequestActions.lastTracerouteTime, nodeRequestActions.lastRequestNeighborTimes.map { it[nodeId] }, ) { edition, stable, alpha, trTime, niTime -> MetadataGroup(edition = edition, stable = stable, alpha = alpha, trTime = trTime, niTime = niTime) diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 5b8dea3b68..7e1002c405 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -185,9 +185,7 @@ constructor( val effects: SharedFlow = nodeRequestActions.effects - val lastTraceRouteTime: StateFlow = - combine(nodeRequestActions.lastTracerouteTimes, activeNodeId) { map, id -> id?.let { map[it] } } - .stateInWhileSubscribed(null) + val lastTraceRouteTime: StateFlow = nodeRequestActions.lastTracerouteTime val lastRequestNeighborsTime: StateFlow = combine(nodeRequestActions.lastRequestNeighborTimes, activeNodeId) { map, id -> id?.let { map[it] } }