feat: consolidate dialogs (#4506)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-08 16:45:52 -06:00 committed by GitHub
parent 7bcc51863f
commit ea6d1ffa32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 2042 additions and 1659 deletions

View file

@ -28,10 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@ -46,13 +43,7 @@ import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Output
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -107,6 +98,7 @@ import org.meshtastic.core.strings.use_homoglyph_characters_encoding
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
@ -501,14 +493,20 @@ private fun AppVersionButton(
@Composable
private fun LanguagePickerDialog(onDismiss: () -> Unit) {
SettingsDialog(title = stringResource(Res.string.preferences_language), onDismiss = onDismiss) {
languageMap().forEach { (languageTag, languageName) ->
ListItem(text = languageName, trailingIcon = null) {
LanguageUtils.setAppLocale(languageTag)
onDismiss()
MeshtasticDialog(
title = stringResource(Res.string.preferences_language),
onDismiss = onDismiss,
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
languageMap().forEach { (languageTag, languageName) ->
ListItem(text = languageName, trailingIcon = null) {
LanguageUtils.setAppLocale(languageTag)
onDismiss()
}
}
}
}
}
},
)
}
private enum class ThemeOption(val label: StringResource, val mode: Int) {
@ -520,35 +518,18 @@ private enum class ThemeOption(val label: StringResource, val mode: Int) {
@Composable
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
SettingsDialog(title = stringResource(Res.string.choose_theme), onDismiss = onDismiss) {
ThemeOption.entries.forEach { option ->
ListItem(text = stringResource(option.label), trailingIcon = null) {
onClickTheme(option.mode)
onDismiss()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SettingsDialog(title: String, onDismiss: () -> Unit, content: @Composable ColumnScope.() -> Unit) {
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.wrapContentWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.large,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
MeshtasticDialog(
title = stringResource(Res.string.choose_theme),
onDismiss = onDismiss,
text = {
Column {
Text(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
text = title,
style = MaterialTheme.typography.titleLarge,
)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { content() }
ThemeOption.entries.forEach { option ->
ListItem(text = stringResource(option.label), trailingIcon = null) {
onClickTheme(option.mode)
onDismiss()
}
}
}
}
}
},
)
}

View file

@ -85,7 +85,6 @@ import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_clear
import org.meshtastic.core.strings.debug_clear_logs_confirm
import org.meshtastic.core.strings.debug_decoded_payload
import org.meshtastic.core.strings.debug_default_search
import org.meshtastic.core.strings.debug_export_failed
@ -103,7 +102,6 @@ import org.meshtastic.core.strings.log_retention_never
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.theme.AnnotationColor
import org.meshtastic.core.ui.theme.AppTheme
@ -178,7 +176,7 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo
IconButton(onClick = { showSettings = !showSettings }) {
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
}
DebugMenuActions(deleteLogs = { viewModel.deleteAllLogs() })
DebugMenuActions(deleteLogs = { viewModel.requestDeleteAllLogs() })
},
onClickChip = {},
)
@ -413,22 +411,9 @@ private fun rememberAnnotatedLogMessage(log: UiMeshLog, searchText: String): Ann
@Composable
fun DebugMenuActions(deleteLogs: () -> Unit, modifier: Modifier = Modifier) {
var showDeleteLogsDialog by remember { mutableStateOf(false) }
IconButton(onClick = { showDeleteLogsDialog = true }, modifier = modifier.padding(4.dp)) {
IconButton(onClick = deleteLogs, modifier = modifier.padding(4.dp)) {
Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.debug_clear))
}
if (showDeleteLogsDialog) {
SimpleAlertDialog(
title = Res.string.debug_clear,
text = Res.string.debug_clear_logs_confirm,
onConfirm = {
showDeleteLogsDialog = false
deleteLogs()
},
onDismiss = { showDeleteLogsDialog = false },
)
}
}
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<UiMeshLog>) =
@ -487,7 +472,16 @@ private fun DecodedPayloadBlock(
modifier: Modifier = Modifier,
) {
val commonTextStyle =
TextStyle(fontSize = if (isSelected) 10.sp else 8.sp, fontWeight = FontWeight.Bold, color = colorScheme.primary)
TextStyle(
fontSize =
if (isSelected) {
10.sp
} else {
8.sp
},
fontWeight = FontWeight.Bold,
color = colorScheme.primary,
)
Column(modifier = modifier) {
Text(

View file

@ -41,6 +41,10 @@ import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.toReadableString
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_clear
import org.meshtastic.core.strings.debug_clear_logs_confirm
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.MeshPacket
@ -215,6 +219,7 @@ constructor(
private val meshLogRepository: MeshLogRepository,
private val nodeRepository: NodeRepository,
private val meshLogPrefs: MeshLogPrefs,
private val alertManager: AlertManager,
) : ViewModel() {
val meshLog: StateFlow<ImmutableList<UiMeshLog>> =
@ -393,6 +398,14 @@ constructor(
private fun Int.asNodeId(): String = "!%08x".format(Locale.getDefault(), this)
fun requestDeleteAllLogs() {
alertManager.showAlert(
titleRes = Res.string.debug_clear,
messageRes = Res.string.debug_clear_logs_confirm,
onConfirm = { deleteAllLogs() },
)
}
fun deleteAllLogs() = viewModelScope.launch(Dispatchers.IO) { meshLogRepository.deleteAll() }
@Immutable

View file

@ -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 androidx.compose.foundation.layout.Arrangement
@ -27,20 +26,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -48,9 +42,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.are_you_sure
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.clean_node_database_confirmation
import org.meshtastic.core.strings.clean_node_database_description
import org.meshtastic.core.strings.clean_node_database_title
import org.meshtastic.core.strings.clean_nodes_older_than
@ -68,21 +59,9 @@ fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel = hiltViewMode
val olderThanDays by viewModel.olderThanDays.collectAsState()
val onlyUnknownNodes by viewModel.onlyUnknownNodes.collectAsState()
val nodesToDelete by viewModel.nodesToDelete.collectAsState()
var showConfirmationDialog by remember { mutableStateOf(false) }
LaunchedEffect(olderThanDays, onlyUnknownNodes) { viewModel.getNodesToDelete() }
if (showConfirmationDialog) {
ConfirmationDialog(
nodesToDeleteCount = nodesToDelete.size,
onConfirm = {
viewModel.cleanNodes()
showConfirmationDialog = false
},
onDismiss = { showConfirmationDialog = false },
)
}
Column(modifier = Modifier.padding(16.dp).verticalScroll(rememberScrollState())) {
Text(stringResource(Res.string.clean_node_database_title))
Text(stringResource(Res.string.clean_node_database_description), style = MaterialTheme.typography.bodySmall)
@ -105,7 +84,7 @@ fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel = hiltViewMode
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { if (nodesToDelete.isNotEmpty()) showConfirmationDialog = true },
onClick = { if (nodesToDelete.isNotEmpty()) viewModel.requestCleanNodes() },
modifier = Modifier.fillMaxWidth(),
enabled = nodesToDelete.isNotEmpty(),
) {
@ -186,21 +165,3 @@ private fun NodesDeletionPreview(nodesToDelete: List<NodeEntity>) {
}
}
}
/**
* Composable for the confirmation dialog before deleting nodes.
*
* @param nodesToDeleteCount The number of nodes to be deleted.
* @param onConfirm Callback for when the user confirms the deletion.
* @param onDismiss Callback for when the user dismisses the dialog.
*/
@Composable
private fun ConfirmationDialog(nodesToDeleteCount: Int, onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(Res.string.are_you_sure)) },
text = { Text(stringResource(Res.string.clean_node_database_confirmation, nodesToDeleteCount)) },
confirmButton = { Button(onClick = onConfirm) { Text(stringResource(Res.string.clean_now)) } },
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) } },
)
}

View file

@ -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 androidx.lifecycle.ViewModel
@ -23,9 +22,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.are_you_sure
import org.meshtastic.core.strings.clean_node_database_confirmation
import org.meshtastic.core.strings.clean_now
import org.meshtastic.core.ui.util.AlertManager
import javax.inject.Inject
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@ -42,6 +47,7 @@ class CleanNodeDatabaseViewModel
constructor(
private val nodeRepository: NodeRepository,
private val serviceRepository: ServiceRepository,
private val alertManager: AlertManager,
) : ViewModel() {
private val _olderThanDays = MutableStateFlow(30f)
val olderThanDays = _olderThanDays.asStateFlow()
@ -100,6 +106,19 @@ constructor(
}
}
fun requestCleanNodes() {
viewModelScope.launch {
val count = _nodesToDelete.value.size
val message = getString(Res.string.clean_node_database_confirmation, count)
alertManager.showAlert(
titleRes = Res.string.are_you_sure,
message = message,
confirmTextRes = Res.string.clean_now,
onConfirm = { cleanNodes() },
)
}
}
/**
* Deletes the nodes currently queued in [_nodesToDelete] from the database and instructs the mesh service to remove
* them.

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@ -30,12 +29,10 @@ import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -62,6 +59,7 @@ import org.meshtastic.core.strings.secondary_no_telemetry
import org.meshtastic.core.strings.security_icon_help_dismiss
import org.meshtastic.core.strings.uplink_enabled
import org.meshtastic.core.strings.uplink_feature_description
import org.meshtastic.core.ui.component.MeshtasticDialog
@Composable
internal fun ChannelLegend(onClick: () -> Unit) {
@ -109,10 +107,10 @@ internal enum class ChannelIcons(
@Composable
internal fun ChannelLegendDialog(capabilities: Capabilities, onDismiss: () -> Unit) {
AlertDialog(
modifier = Modifier.fillMaxSize(),
onDismissRequest = onDismiss,
title = { Text(stringResource(Res.string.channel_features)) },
MeshtasticDialog(
onDismiss = onDismiss,
title = stringResource(Res.string.channel_features),
dismissText = stringResource(Res.string.security_icon_help_dismiss),
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
@ -152,15 +150,6 @@ internal fun ChannelLegendDialog(capabilities: Capabilities, onDismiss: () -> Un
IconDefinitions()
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.security_icon_help_dismiss)) }
}
},
)
}

View file

@ -16,19 +16,11 @@
*/
package org.meshtastic.feature.settings.radio.channel.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -51,6 +43,7 @@ import org.meshtastic.core.strings.save
import org.meshtastic.core.strings.uplink_enabled
import org.meshtastic.core.ui.component.EditBase64Preference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.PositionPrecisionPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.proto.ChannelSettings
@ -70,9 +63,13 @@ fun EditChannelDialog(
var channelInput by remember(channelSettings) { mutableStateOf(channelSettings) }
AlertDialog(
onDismissRequest = onDismissRequest,
shape = RoundedCornerShape(16.dp),
MeshtasticDialog(
onDismiss = onDismissRequest,
dismissText = stringResource(Res.string.cancel),
confirmText = stringResource(Res.string.save),
onConfirm = { onAddClick(channelInput) },
modifier = modifier,
title = "", // Title is handled internally by specific items if needed, or we could add one
text = {
Column(modifier = Modifier.fillMaxWidth()) {
EditTextPreference(
@ -143,19 +140,6 @@ fun EditChannelDialog(
)
}
},
confirmButton = {
FlowRow(
modifier = modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(modifier = modifier.weight(1f), onClick = onDismissRequest) {
Text(stringResource(Res.string.cancel))
}
Button(modifier = modifier.weight(1f), onClick = { onAddClick(channelInput) }, enabled = true) {
Text(stringResource(Res.string.save))
}
}
},
)
}

View file

@ -16,26 +16,14 @@
*/
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
@ -49,6 +37,7 @@ import org.meshtastic.core.strings.module_settings
import org.meshtastic.core.strings.radio_configuration
import org.meshtastic.core.strings.save
import org.meshtastic.core.strings.short_name
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.proto.DeviceProfile
@ -61,8 +50,7 @@ private enum class ProfileField(val tag: Int, val labelRes: StringResource) {
FIXED_POSITION(6, Res.string.fixed_position),
}
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun EditDeviceProfileDialog(
title: String,
@ -88,20 +76,36 @@ fun EditDeviceProfileDialog(
}
}
AlertDialog(
onDismissRequest = onDismiss,
shape = RoundedCornerShape(16.dp),
text = {
Column(modifier.fillMaxWidth()) {
Text(
text = title,
style =
MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
MeshtasticDialog(
title = title,
onDismiss = onDismiss,
dismissText = stringResource(Res.string.cancel),
confirmText = stringResource(Res.string.save),
onConfirm = {
val result =
DeviceProfile(
long_name = if (state[ProfileField.LONG_NAME] == true) deviceProfile.long_name else null,
short_name = if (state[ProfileField.SHORT_NAME] == true) deviceProfile.short_name else null,
channel_url = if (state[ProfileField.CHANNEL_URL] == true) deviceProfile.channel_url else null,
config = if (state[ProfileField.CONFIG] == true) deviceProfile.config else null,
module_config =
if (state[ProfileField.MODULE_CONFIG] == true) {
deviceProfile.module_config
} else {
null
},
fixed_position =
if (state[ProfileField.FIXED_POSITION] == true) {
deviceProfile.fixed_position
} else {
null
},
)
onConfirm(result)
},
modifier = modifier,
text = {
Column(modifier = Modifier.fillMaxWidth()) {
HorizontalDivider()
ProfileField.entries.forEach { field ->
val isAvailable =
@ -124,47 +128,6 @@ fun EditDeviceProfileDialog(
HorizontalDivider()
}
},
confirmButton = {
FlowRow(
modifier = modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(modifier = modifier.weight(1f), onClick = onDismiss) {
Text(stringResource(Res.string.cancel))
}
Button(
modifier = modifier.weight(1f),
onClick = {
val result =
DeviceProfile(
long_name =
if (state[ProfileField.LONG_NAME] == true) deviceProfile.long_name else null,
short_name =
if (state[ProfileField.SHORT_NAME] == true) deviceProfile.short_name else null,
channel_url =
if (state[ProfileField.CHANNEL_URL] == true) deviceProfile.channel_url else null,
config = if (state[ProfileField.CONFIG] == true) deviceProfile.config else null,
module_config =
if (state[ProfileField.MODULE_CONFIG] == true) {
deviceProfile.module_config
} else {
null
},
fixed_position =
if (state[ProfileField.FIXED_POSITION] == true) {
deviceProfile.fixed_position
} else {
null
},
)
onConfirm(result)
},
enabled = state.values.any { it },
) {
Text(stringResource(Res.string.save))
}
}
},
)
}

View file

@ -21,12 +21,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -80,7 +78,7 @@ import org.meshtastic.core.ui.component.EditIPv4Preference
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.util.openNfcSettings
@ -89,7 +87,7 @@ import org.meshtastic.proto.Config
@Composable
private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
SimpleAlertDialog(title = Res.string.error, text = Res.string.wifi_qr_code_error, onDismiss = onDismiss)
MeshtasticDialog(titleRes = Res.string.error, messageRes = Res.string.wifi_qr_code_error, onDismiss = onDismiss)
@Composable
fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
@ -105,23 +103,16 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBac
var showNfcDisabledDialog: Boolean by rememberSaveable { mutableStateOf(false) }
if (showNfcDisabledDialog) {
AlertDialog(
onDismissRequest = { showNfcDisabledDialog = false },
title = { Text(stringResource(Res.string.scan_nfc)) },
text = { Text(stringResource(Res.string.nfc_disabled)) },
confirmButton = {
TextButton(
onClick = {
context.openNfcSettings()
showNfcDisabledDialog = false
},
) {
Text(stringResource(Res.string.open_settings))
}
},
dismissButton = {
TextButton(onClick = { showNfcDisabledDialog = false }) { Text(stringResource(Res.string.cancel)) }
MeshtasticDialog(
onDismiss = { showNfcDisabledDialog = false },
title = stringResource(Res.string.scan_nfc),
message = stringResource(Res.string.nfc_disabled),
confirmText = stringResource(Res.string.open_settings),
onConfirm = {
context.openNfcSettings()
showNfcDisabledDialog = false
},
dismissText = stringResource(Res.string.cancel),
)
}

View file

@ -18,14 +18,9 @@ package org.meshtastic.feature.settings.radio.component
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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
@ -42,6 +37,7 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.close
import org.meshtastic.core.strings.delivery_confirmed
import org.meshtastic.core.strings.error
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.feature.settings.radio.ResponseState
private const val AUTO_DISMISS_DELAY_MS = 1500L
@ -55,10 +51,11 @@ fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit
onDismiss()
}
}
AlertDialog(
onDismissRequest = {},
shape = RoundedCornerShape(16.dp),
title = {
MeshtasticDialog(
onDismiss = onDismiss,
title = "", // Title is handled in the text block for more control
text = {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
if (state is ResponseState.Loading) {
val progress by
@ -86,24 +83,15 @@ fun <T> PacketResponseStateDialog(state: ResponseState<T>, onDismiss: () -> Unit
}
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
Button(
onClick = {
onDismiss()
if (state is ResponseState.Success || state is ResponseState.Error) {
backDispatcher?.onBackPressed()
}
},
modifier = Modifier.padding(top = 16.dp),
) {
Text(stringResource(Res.string.close))
}
dismissable = false,
onConfirm = {
onDismiss()
if (state is ResponseState.Success || state is ResponseState.Error) {
backDispatcher?.onBackPressed()
}
},
confirmText = stringResource(Res.string.close),
dismissText = null, // Hide dismiss button, only show "Close" confirm button
)
}

View file

@ -24,12 +24,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -49,7 +46,6 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.admin_key
import org.meshtastic.core.strings.admin_keys
import org.meshtastic.core.strings.administration
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.config_security_admin_key
import org.meshtastic.core.strings.config_security_debug_log_api_enabled
import org.meshtastic.core.strings.config_security_is_managed
@ -63,7 +59,6 @@ import org.meshtastic.core.strings.export_keys_confirmation
import org.meshtastic.core.strings.legacy_admin_channel
import org.meshtastic.core.strings.logs
import org.meshtastic.core.strings.managed_mode
import org.meshtastic.core.strings.okay
import org.meshtastic.core.strings.private_key
import org.meshtastic.core.strings.public_key
import org.meshtastic.core.strings.regenerate_keys_confirmation
@ -73,6 +68,7 @@ import org.meshtastic.core.strings.serial_console
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.EditBase64Preference
import org.meshtastic.core.ui.component.EditListPreference
import org.meshtastic.core.ui.component.MeshtasticResourceDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
@ -116,28 +112,22 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
)
var showEditSecurityConfigDialog by rememberSaveable { mutableStateOf(false) }
if (showEditSecurityConfigDialog) {
AlertDialog(
title = { Text(text = stringResource(Res.string.export_keys)) },
text = { Text(text = stringResource(Res.string.export_keys_confirmation)) },
onDismissRequest = { showEditSecurityConfigDialog = false },
confirmButton = {
TextButton(
onClick = {
showEditSecurityConfigDialog = false
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
putExtra(
Intent.EXTRA_TITLE,
"${node?.user?.short_name}_keys_${System.currentTimeMillis()}.json",
)
}
exportConfigLauncher.launch(intent)
},
) {
Text(stringResource(Res.string.okay))
}
MeshtasticResourceDialog(
titleRes = Res.string.export_keys,
messageRes = Res.string.export_keys_confirmation,
onDismiss = { showEditSecurityConfigDialog = false },
onConfirm = {
showEditSecurityConfigDialog = false
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
putExtra(
Intent.EXTRA_TITLE,
"${node?.user?.short_name}_keys_${System.currentTimeMillis()}.json",
)
}
exportConfigLauncher.launch(intent)
},
)
}
@ -268,30 +258,22 @@ fun PrivateKeyRegenerateDialog(
onDismiss: () -> Unit = {},
) {
if (showKeyGenerationDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(Res.string.regenerate_private_key)) },
text = { Text(text = stringResource(Res.string.regenerate_keys_confirmation)) },
confirmButton = {
TextButton(
onClick = {
// Generate a random "f" value
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
val securityInput =
Config.SecurityConfig(private_key = f.toByteString(), public_key = ByteString.EMPTY)
onConfirm(securityInput)
},
) {
Text(stringResource(Res.string.okay))
}
MeshtasticResourceDialog(
onDismiss = onDismiss,
titleRes = Res.string.regenerate_private_key,
messageRes = Res.string.regenerate_keys_confirmation,
onConfirm = {
// Generate a random "f" value
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
val securityInput = Config.SecurityConfig(private_key = f.toByteString(), public_key = ByteString.EMPTY)
onConfirm(securityInput)
},
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) } },
)
}
}

View file

@ -23,12 +23,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -43,6 +39,7 @@ import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.send
import org.meshtastic.core.strings.shutdown_node_name
import org.meshtastic.core.strings.shutdown_warning
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.User
@ -57,22 +54,17 @@ fun ShutdownConfirmationDialog(
) {
val nodeLongName = node?.user?.long_name ?: "Unknown Node"
AlertDialog(
onDismissRequest = {},
icon = { icon?.let { Icon(imageVector = it, contentDescription = null) } },
title = { Text(text = title) },
MeshtasticDialog(
onDismiss = onDismiss,
icon = icon,
title = title,
text = { ShutdownDialogContent(nodeLongName = nodeLongName, isShutdown = isShutdown) },
dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(Res.string.cancel)) } },
confirmButton = {
Button(
onClick = {
onDismiss()
onConfirm()
},
) {
Text(stringResource(Res.string.send))
}
confirmText = stringResource(Res.string.send),
onConfirm = {
onDismiss()
onConfirm()
},
dismissText = stringResource(Res.string.cancel),
)
}

View file

@ -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,16 +14,10 @@
* 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.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
@ -31,6 +25,7 @@ import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.send
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.theme.AppTheme
@Composable
@ -41,22 +36,17 @@ fun WarningDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
onDismissRequest = {},
icon = { icon?.let { Icon(imageVector = it, contentDescription = null) } },
title = { Text(text = title) },
MeshtasticDialog(
onDismiss = onDismiss,
icon = icon,
title = title,
text = text,
dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(Res.string.cancel)) } },
confirmButton = {
Button(
onClick = {
onDismiss()
onConfirm()
},
) {
Text(stringResource(Res.string.send))
}
confirmText = stringResource(Res.string.send),
onConfirm = {
onDismiss()
onConfirm()
},
dismissText = stringResource(Res.string.cancel),
)
}