mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-06 22:02:37 -05:00
fix(configs): Improve loading state feedback and dialog behavior (#4271)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -758,6 +758,8 @@
|
||||
<string name="user_id">User ID</string>
|
||||
<string name="uptime">Uptime</string>
|
||||
<string name="load_indexed">Load %1$d</string>
|
||||
<string name="fetching_channel_indexed">Fetching Channel %1$d/%2$d</string>
|
||||
<string name="fetching_config">Fetching %1$s</string>
|
||||
<string name="disk_free_indexed">Disk Free %1$d</string>
|
||||
<string name="timestamp">Timestamp</string>
|
||||
<string name="heading">Heading</string>
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.protobuf.MessageLite
|
||||
import com.meshtastic.core.strings.getString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -64,6 +65,8 @@ import org.meshtastic.core.service.IMeshService
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cant_shutdown
|
||||
import org.meshtastic.core.strings.fetching_channel_indexed
|
||||
import org.meshtastic.core.strings.fetching_config
|
||||
import org.meshtastic.core.ui.util.getChannelList
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.ModuleRoute
|
||||
@@ -131,7 +134,7 @@ constructor(
|
||||
val destNode: StateFlow<Node?>
|
||||
get() = _destNode
|
||||
|
||||
private val requestIds = MutableStateFlow(hashSetOf<Int>())
|
||||
private val requestIds = MutableStateFlow(emptySet<Int>())
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
|
||||
|
||||
@@ -213,11 +216,11 @@ constructor(
|
||||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
requestIds.update { it.apply { add(packetId) } }
|
||||
requestIds.update { it + packetId }
|
||||
_radioConfigState.update { state ->
|
||||
val currentState = state.responseState
|
||||
if (currentState is ResponseState.Loading) {
|
||||
val total = requestIds.value.size.coerceAtLeast(1)
|
||||
val total = maxOf(currentState.total, requestIds.value.size)
|
||||
state.copy(responseState = currentState.copy(total = total))
|
||||
} else {
|
||||
state.copy(
|
||||
@@ -553,13 +556,23 @@ constructor(
|
||||
}
|
||||
|
||||
fun clearPacketResponse() {
|
||||
requestIds.value = hashSetOf()
|
||||
requestIds.value = emptySet()
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
|
||||
}
|
||||
|
||||
private fun getTitleForRoute(route: Enum<*>) = when (route) {
|
||||
is ConfigRoute -> route.title
|
||||
is ModuleRoute -> route.title
|
||||
is AdminRoute -> route.title
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun setResponseStateLoading(route: Enum<*>) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
|
||||
val title = getTitleForRoute(route)
|
||||
|
||||
_radioConfigState.update {
|
||||
RadioConfigState(
|
||||
isLocal = it.isLocal,
|
||||
@@ -567,7 +580,10 @@ constructor(
|
||||
route = route.name,
|
||||
metadata = it.metadata,
|
||||
nodeDbResetPreserveFavorites = it.nodeDbResetPreserveFavorites,
|
||||
responseState = ResponseState.Loading(),
|
||||
responseState =
|
||||
ResponseState.Loading(
|
||||
status = title?.let { t -> getString(Res.string.fetching_config, getString(t)) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -646,11 +662,14 @@ constructor(
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
|
||||
}
|
||||
|
||||
private fun incrementCompleted() {
|
||||
private fun incrementCompleted(status: String? = null) {
|
||||
_radioConfigState.update { state ->
|
||||
if (state.responseState is ResponseState.Loading) {
|
||||
val increment = state.responseState.completed + 1
|
||||
state.copy(responseState = state.responseState.copy(completed = increment))
|
||||
state.copy(
|
||||
responseState =
|
||||
state.responseState.copy(completed = increment, status = status ?: state.responseState.status),
|
||||
)
|
||||
} else {
|
||||
state // Return the unchanged state for other response states
|
||||
}
|
||||
@@ -671,7 +690,7 @@ constructor(
|
||||
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
|
||||
sendError(getStringResFrom(parsed.errorReasonValue))
|
||||
} else if (packet.from == destNum && route.isEmpty()) {
|
||||
requestIds.update { it.apply { remove(data.requestId) } }
|
||||
requestIds.update { it - data.requestId }
|
||||
if (requestIds.value.isEmpty()) {
|
||||
setResponseStateSuccess()
|
||||
} else {
|
||||
@@ -702,7 +721,9 @@ constructor(
|
||||
state.channelList.toMutableList().apply { add(response.index, response.settings) },
|
||||
)
|
||||
}
|
||||
incrementCompleted()
|
||||
incrementCompleted(
|
||||
getString(Res.string.fetching_channel_indexed, response.index + 1, maxChannels),
|
||||
)
|
||||
if (response.index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
|
||||
// Not done yet, request next channel
|
||||
getChannel(destNum, response.index + 1)
|
||||
@@ -761,7 +782,7 @@ constructor(
|
||||
if (AdminRoute.entries.any { it.name == route }) {
|
||||
sendAdminRequest(destNum)
|
||||
}
|
||||
requestIds.update { it.apply { remove(data.requestId) } }
|
||||
requestIds.update { it - data.requestId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
@@ -14,7 +14,6 @@
|
||||
* 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.settings.radio
|
||||
|
||||
import org.meshtastic.feature.settings.util.UiText
|
||||
@@ -23,7 +22,8 @@ import org.meshtastic.feature.settings.util.UiText
|
||||
sealed class ResponseState<out T> {
|
||||
data object Empty : ResponseState<Nothing>()
|
||||
|
||||
data class Loading(var total: Int = 1, var completed: Int = 0) : ResponseState<Nothing>()
|
||||
data class Loading(var total: Int = 1, var completed: Int = 0, var status: String? = null) :
|
||||
ResponseState<Nothing>()
|
||||
|
||||
data class Success<T>(val result: T) : ResponseState<T>()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
@@ -14,7 +14,6 @@
|
||||
* 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.settings.radio.component
|
||||
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
@@ -28,13 +27,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
@@ -42,9 +44,17 @@ import org.meshtastic.core.strings.delivery_confirmed
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
|
||||
private const val AUTO_DISMISS_DELAY_MS = 1500L
|
||||
|
||||
@Composable
|
||||
fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit = {}, onComplete: () -> Unit = {}) {
|
||||
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
LaunchedEffect(state) {
|
||||
if (state is ResponseState.Success) {
|
||||
delay(AUTO_DISMISS_DELAY_MS)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = {},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@@ -58,7 +68,14 @@ fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit
|
||||
)
|
||||
Text("%.0f%%".format(progress * 100))
|
||||
LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
|
||||
if (state.total == state.completed) onComplete()
|
||||
state.status?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
if (state.completed >= state.total) onComplete()
|
||||
}
|
||||
if (state is ResponseState.Success) {
|
||||
Text(text = stringResource(Res.string.delivery_confirmed))
|
||||
|
||||
Reference in New Issue
Block a user