Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -43,92 +45,94 @@ 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,
)
} else {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ class NodeRequestActions @Inject constructor(private val radioController: RadioC
private val _effects = MutableSharedFlow<NodeRequestEffect>()
val effects: SharedFlow<NodeRequestEffect> = _effects.asSharedFlow()

private val _lastTracerouteTimes = MutableStateFlow<Map<Int, Long>>(emptyMap())
val lastTracerouteTimes: StateFlow<Map<Int, Long>> = _lastTracerouteTimes.asStateFlow()
private val _lastTracerouteTime = MutableStateFlow<Long?>(null)
val lastTracerouteTime: StateFlow<Long?> = _lastTracerouteTime.asStateFlow()

private val _lastRequestNeighborTimes = MutableStateFlow<Map<Int, Long>>(emptyMap())
val lastRequestNeighborTimes: StateFlow<Map<Int, Long>> = _lastRequestNeighborTimes.asStateFlow()
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,7 @@ constructor(

val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects

val lastTraceRouteTime: StateFlow<Long?> =
combine(nodeRequestActions.lastTracerouteTimes, activeNodeId) { map, id -> id?.let { map[it] } }
.stateInWhileSubscribed(null)
val lastTraceRouteTime: StateFlow<Long?> = nodeRequestActions.lastTracerouteTime

val lastRequestNeighborsTime: StateFlow<Long?> =
combine(nodeRequestActions.lastRequestNeighborTimes, activeNodeId) { map, id -> id?.let { map[it] } }
Expand Down
Loading