fix: prompt to confirm discarding unsaved changes in config screens

Fixes #5283 by introducing an UnsavedChangesDialog component and integrating it into RadioConfigScreenList and ChannelConfigScreen. Utilizes PlatformBackHandler to guard against accidental system back gestures.
This commit is contained in:
James Rich
2026-04-29 09:02:56 -05:00
parent 5e415120d0
commit 077ac1e9db
4 changed files with 92 additions and 3 deletions

View File

@@ -182,6 +182,8 @@
<string name="accept">Accept</string>
<string name="cancel">Cancel</string>
<string name="discard_changes">Discard</string>
<string name="unsaved_changes_message">You have unsaved changes. Are you sure you want to discard them?</string>
<string name="stay">Stay</string>
<string name="save_changes">Save</string>
<string name="new_channel_rcvd">New Channel URL received</string>
<string name="report">Report</string>

View File

@@ -0,0 +1,36 @@
/*
* 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.core.ui.component
import androidx.compose.runtime.Composable
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.cancel
import org.meshtastic.core.resources.discard_changes
import org.meshtastic.core.resources.stay
import org.meshtastic.core.resources.unsaved_changes_message
@Composable
fun UnsavedChangesDialog(onDiscard: () -> Unit, onStay: () -> Unit) {
MeshtasticDialog(
titleRes = Res.string.cancel,
messageRes = Res.string.unsaved_changes_message,
confirmTextRes = Res.string.discard_changes,
onConfirm = onDiscard,
dismissTextRes = Res.string.stay,
onDismiss = onStay,
)
}

View File

@@ -137,6 +137,27 @@ private fun ChannelConfigScreen(
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
var showChannelLegendDialog by rememberSaveable { mutableStateOf(false) }
var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
val handleBack = {
if (isEditing) {
showExitConfirmation = true
} else {
onBack()
}
}
org.meshtastic.core.ui.util.PlatformBackHandler(enabled = isEditing) { showExitConfirmation = true }
if (showExitConfirmation) {
org.meshtastic.core.ui.component.UnsavedChangesDialog(
onDiscard = {
showExitConfirmation = false
onBack()
},
onStay = { showExitConfirmation = false },
)
}
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
@@ -164,7 +185,7 @@ private fun ChannelConfigScreen(
MainAppBar(
title = title,
canNavigateUp = true,
onNavigateUp = onBack,
onNavigateUp = handleBack,
ourNode = null,
showNodeChip = false,
actions = {},

View File

@@ -31,6 +31,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
@@ -41,6 +45,8 @@ import org.meshtastic.core.resources.discard_changes
import org.meshtastic.core.resources.save_changes
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.PreferenceFooter
import org.meshtastic.core.ui.component.UnsavedChangesDialog
import org.meshtastic.core.ui.util.PlatformBackHandler
import org.meshtastic.feature.settings.radio.ResponseState
@Suppress("LongMethod")
@@ -60,14 +66,38 @@ fun <T : Message<T, *>> RadioConfigScreenList(
content: LazyListScope.() -> Unit,
) {
val focusManager = LocalFocusManager.current
val isEditing = configState.isDirty || additionalDirtyCheck()
var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
val handleBack = {
if (isEditing) {
showExitConfirmation = true
} else {
onBack()
}
}
PlatformBackHandler(enabled = isEditing) { showExitConfirmation = true }
Box(modifier = modifier) {
if (showExitConfirmation) {
UnsavedChangesDialog(
onDiscard = {
showExitConfirmation = false
configState.reset()
onDiscard()
onBack()
},
onStay = { showExitConfirmation = false },
)
}
Scaffold(
topBar = {
MainAppBar(
title = title,
canNavigateUp = true,
onNavigateUp = onBack,
onNavigateUp = handleBack,
ourNode = null,
showNodeChip = false,
actions = actions,
@@ -75,7 +105,7 @@ fun <T : Message<T, *>> RadioConfigScreenList(
)
},
) { innerPadding ->
val showFooterButtons = configState.isDirty || additionalDirtyCheck()
val showFooterButtons = isEditing
LazyColumn(
modifier = Modifier.padding(innerPadding).fillMaxSize(),